diff --git a/.gitignore b/.gitignore index d5e16cb60..8cce379dd 100644 --- a/.gitignore +++ b/.gitignore @@ -83,7 +83,6 @@ gen-external-apklibs .leanplum_token .adjust_token .sentry_token -.digital_asset_links_token .mls_token diff --git a/app/build.gradle b/app/build.gradle index ddf6ce535..7a1c54f9b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -354,21 +354,6 @@ android.applicationVariants.all { variant -> println("X_X") } -// ------------------------------------------------------------------------------------------------- -// Digital Asset Links: Read token from local file if it exists -// ------------------------------------------------------------------------------------------------- - - print("Digital Asset Links token: ") - - try { - def token = new File("${rootDir}/.digital_asset_links_token").text.trim() - buildConfigField 'String', 'DIGITAL_ASSET_LINKS_TOKEN', '"' + token + '"' - println "(Added from .digital_asset_links_token file)" - } catch (FileNotFoundException ignored) { - buildConfigField 'String', 'DIGITAL_ASSET_LINKS_TOKEN', 'null' - println("X_X") - } - // ------------------------------------------------------------------------------------------------- // MLS: Read token from local file if it exists // ------------------------------------------------------------------------------------------------- @@ -413,6 +398,7 @@ dependencies { implementation Deps.leanplum_fcm implementation Deps.mozilla_concept_engine + implementation Deps.mozilla_concept_menu implementation Deps.mozilla_concept_push implementation Deps.mozilla_concept_storage implementation Deps.mozilla_concept_sync diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/IdlingResourceHelper.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/IdlingResourceHelper.kt new file mode 100644 index 000000000..860aeda76 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/IdlingResourceHelper.kt @@ -0,0 +1,26 @@ +package org.mozilla.fenix.helpers + +import androidx.test.espresso.IdlingRegistry +import androidx.test.rule.ActivityTestRule +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.helpers.idlingresource.AddonsInstallingIdlingResource + +object IdlingResourceHelper { + + // Idling Resource to manage installing an addon + fun registerAddonInstallingIdlingResource(activityTestRule: ActivityTestRule) { + IdlingRegistry.getInstance().register( + AddonsInstallingIdlingResource( + activityTestRule.activity.supportFragmentManager + ) + ) + } + + fun unregisterAddonInstallingIdlingResource(activityTestRule: ActivityTestRule) { + IdlingRegistry.getInstance().unregister( + AddonsInstallingIdlingResource( + activityTestRule.activity.supportFragmentManager + ) + ) + } +} diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/AddonsInstallingIdlingResource.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/AddonsInstallingIdlingResource.kt new file mode 100644 index 000000000..a10fc21c8 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/AddonsInstallingIdlingResource.kt @@ -0,0 +1,42 @@ +package org.mozilla.fenix.helpers.idlingresource + +import androidx.fragment.app.FragmentManager +import androidx.navigation.fragment.NavHostFragment +import androidx.test.espresso.IdlingResource +import mozilla.components.feature.addons.ui.AddonInstallationDialogFragment + +class AddonsInstallingIdlingResource( + val fragmentManager: FragmentManager +) : + IdlingResource { + private var resourceCallback: IdlingResource.ResourceCallback? = null + private var isAddonInstalled = false + + override fun getName(): String { + return this::javaClass.name + } + + override fun isIdleNow(): Boolean { + return isInstalledAddonDialogShown() + } + + override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) { + if (callback != null) + resourceCallback = callback + } + + private fun isInstalledAddonDialogShown(): Boolean { + val activityChildFragments = + (fragmentManager.fragments.first() as NavHostFragment) + .childFragmentManager.fragments + + for (childFragment in activityChildFragments.indices) { + if (activityChildFragments[childFragment] is AddonInstallationDialogFragment) { + resourceCallback?.onTransitionToIdle() + isAddonInstalled = true + return isAddonInstalled + } + } + return isAddonInstalled + } +} diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/AddonsLoadingIdlingResource.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/AddonsLoadingIdlingResource.kt new file mode 100644 index 000000000..56ff6cc06 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/idlingresource/AddonsLoadingIdlingResource.kt @@ -0,0 +1,41 @@ +package org.mozilla.fenix.helpers.idlingresource + +import android.view.View +import android.view.View.VISIBLE +import androidx.fragment.app.FragmentManager +import androidx.test.espresso.IdlingResource +import org.mozilla.fenix.R +import org.mozilla.fenix.addons.AddonsManagementFragment + +class AddonsLoadingIdlingResource(val fragmentManager: FragmentManager) : IdlingResource { + private var resourceCallback: IdlingResource.ResourceCallback? = null + + override fun getName(): String { + return this::javaClass.name + } + + override fun isIdleNow(): Boolean { + val idle = addonsFinishedLoading() + if (idle) + resourceCallback?.onTransitionToIdle() + return idle + } + + override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) { + if (callback != null) + resourceCallback = callback + } + + private fun addonsFinishedLoading(): Boolean { + val progressbar = fragmentManager.findFragmentById(R.id.container)?.let { + val addonsManagementFragment = + it.childFragmentManager.fragments.first { it is AddonsManagementFragment } + addonsManagementFragment.view?.findViewById(R.id.add_ons_progress_bar) + } ?: return true + + if (progressbar.visibility == VISIBLE) + return false + + return true + } +} diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt index 53247bb0b..67125a717 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt @@ -96,6 +96,7 @@ class ContextMenusTest { } } + @Ignore("Test failures: https://github.com/mozilla-mobile/fenix/issues/12473") @Test fun verifyContextCopyLink() { val pageLinks = diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/DeepLinkTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/DeepLinkTest.kt index f73a452d0..21d6fdb6a 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/DeepLinkTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/DeepLinkTest.kt @@ -9,6 +9,7 @@ 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.helpers.AndroidAssetDispatcher @@ -124,6 +125,7 @@ class DeepLinkTest { } } + @Ignore("Crashing, see: https://github.com/mozilla-mobile/fenix/issues/11239") @Test fun openSettingsSearchEngine() { robot.openSettingsSearchEngine { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt index c1b65d5f2..011eee7d7 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt @@ -11,6 +11,7 @@ import mozilla.components.browser.storage.sync.PlacesHistoryStorage 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 @@ -164,6 +165,7 @@ class HistoryTest { } } + @Ignore("Failing test: https://github.com/mozilla-mobile/fenix/issues/12893") @Test fun deleteAllHistoryTest() { val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) 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 15f76597c..65e850860 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt @@ -66,6 +66,7 @@ class NavigationToolbarTest { } } + @Ignore("Flaky test: https://github.com/mozilla-mobile/fenix/issues/12894") @Test fun goForwardTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SearchTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SearchTest.kt index a57ec57c7..f06d5b623 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SearchTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SearchTest.kt @@ -66,6 +66,7 @@ class SearchTest { } } + @Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/12968") @Test fun shortcutSearchEngineSettingsTest() { homeScreen { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt new file mode 100644 index 000000000..7dd20c944 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt @@ -0,0 +1,96 @@ +package org.mozilla.fenix.ui + +import org.mozilla.fenix.helpers.TestAssetHelper + +/* 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/. */ + +import okhttp3.mockwebserver.MockWebServer +import org.junit.Rule +import org.junit.Before +import org.junit.After +import org.junit.Test +import org.mozilla.fenix.helpers.AndroidAssetDispatcher +import org.mozilla.fenix.helpers.HomeActivityTestRule +import org.mozilla.fenix.ui.robots.homeScreen +import org.mozilla.fenix.ui.robots.navigationToolbar + +/** + * Tests for verifying the functionality of installing or removing addons + * + */ + +class SettingsAddonsTest { + /* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping. + + private lateinit var mockWebServer: MockWebServer + + @get:Rule + val activityTestRule = HomeActivityTestRule() + + @Before + fun setUp() { + mockWebServer = MockWebServer().apply { + setDispatcher(AndroidAssetDispatcher()) + start() + } + } + + @After + fun tearDown() { + mockWebServer.shutdown() + } + + // Walks through settings add-ons menu to ensure all items are present + @Test + fun settingsAddonsItemsTest() { + homeScreen { + }.openThreeDotMenu { + }.openSettings { + verifyAdvancedHeading() + verifyAddons() + }.openAddonsManagerMenu { + verifyAddonsItems() + } + } + + // Opens a webpage and installs an add-on from the three-dot menu + @Test + fun installAddonFromThreeDotMenu() { + val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + val addonName = "uBlock Origin" + + navigationToolbar { + }.openNewTabAndEnterToBrowser(defaultWebPage.url) { + }.openThreeDotMenu { + }.openAddonsManagerMenu { + clickInstallAddon(addonName) + verifyAddonPrompt(addonName) + cancelInstallAddon() + clickInstallAddon(addonName) + acceptInstallAddon() + verifyDownloadAddonPrompt(addonName, activityTestRule) + } + } + + // Opens the addons settings menu, installs an addon, then uninstalls + @Test + fun verifyAddonsCanBeUninstalled() { + val addonName = "uBlock Origin" + + homeScreen { + }.openThreeDotMenu { + }.openSettings { + verifyAdvancedHeading() + verifyAddons() + }.openAddonsManagerMenu { + clickInstallAddon(addonName) + acceptInstallAddon() + verifyDownloadAddonPrompt(addonName, activityTestRule) + }.openDetailedMenuForAddon(addonName) { + verifyCurrentAddonMenu() + }.removeAddon { + } + } +} 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 ee20a2c55..de4ddd79f 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsBasicsTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsBasicsTest.kt @@ -60,6 +60,7 @@ class SettingsBasicsTest { } } + @Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/12968") @Test // Walks through settings menu and sub-menus to ensure all items are present fun settingsMenuBasicsItemsTests() { @@ -90,6 +91,7 @@ class SettingsBasicsTest { } } + @Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/12968") @Test fun selectNewDefaultSearchEngine() { // Goes through the settings and changes the default search engine, then verifies it has changed. 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 2a122ac8d..54c5b1d95 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt @@ -10,6 +10,7 @@ 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.helpers.AndroidAssetDispatcher @@ -216,6 +217,7 @@ class SmokeTest { } } + @Ignore("Flaky test: https://github.com/mozilla-mobile/fenix/issues/12899") @Test fun verifyETPToolbarShieldIconIsNotDisplayedIfETPIsOFFGloballyTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) 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 576c6d01a..964f65d45 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 @@ -366,7 +366,7 @@ class BrowserRobot { } fun openNavigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition { - + mDevice.waitForIdle(waitingTime) navURLBar().click() NavigationToolbarRobot().interact() diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt index 6d11be804..073148cde 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt @@ -19,6 +19,7 @@ import androidx.test.espresso.intent.matcher.IntentMatchers.toPackage import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.Visibility import androidx.test.espresso.matcher.ViewMatchers.hasDescendant +import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText @@ -74,7 +75,9 @@ class SettingsRobot { // ADVANCED SECTION fun verifyAdvancedHeading() = assertAdvancedHeading() - fun verifyAddons() = assertAddons() + fun verifyAddons() = assertAddonsButton() + + // DEVELOPER TOOLS SECTION fun verifyRemoteDebug() = assertRemoteDebug() fun verifyLeakCanaryButton() = assertLeakCanaryButton() @@ -211,6 +214,13 @@ class SettingsRobot { SettingsSubMenuDataCollectionRobot().interact() return SettingsSubMenuDataCollectionRobot.Transition() } + + fun openAddonsManagerMenu(interact: SettingsSubMenuAddonsManagerRobot.() -> Unit): SettingsSubMenuAddonsManagerRobot.Transition { + addonsManagerButton().click() + + SettingsSubMenuAddonsManagerRobot().interact() + return SettingsSubMenuAddonsManagerRobot.Transition() + } } companion object { @@ -349,15 +359,25 @@ private fun assertDeveloperToolsHeading() { // ADVANCED SECTION private fun assertAdvancedHeading() { - scrollToElementByText("Advanced") - onView(withText("Advanced")) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + onView(withId(R.id.recycler_view)).perform( + RecyclerViewActions.scrollTo( + hasDescendant(withText("Add-ons")) + ) + ) + + onView(withText("Add-ons")) + .check(matches(isCompletelyDisplayed())) } -private fun assertAddons() { - scrollToElementByText("Add-ons") - onView(withText("Add-ons")) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) +private fun assertAddonsButton() { + onView(withId(R.id.recycler_view)).perform( + RecyclerViewActions.scrollTo( + hasDescendant(withText("Add-ons")) + ) + ) + + addonsManagerButton() + .check(matches(isCompletelyDisplayed())) } private fun assertRemoteDebug() { @@ -414,5 +434,7 @@ fun isPackageInstalled(packageName: String): Boolean { } } +private fun addonsManagerButton() = onView(withText(R.string.preferences_addons)) + private fun goBackButton() = onView(CoreMatchers.allOf(ViewMatchers.withContentDescription("Navigate up"))) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerAddonDetailedMenuRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerAddonDetailedMenuRobot.kt new file mode 100644 index 000000000..93ca6f939 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerAddonDetailedMenuRobot.kt @@ -0,0 +1,59 @@ +package org.mozilla.fenix.ui.robots + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import androidx.test.espresso.matcher.ViewMatchers.withId +import org.hamcrest.CoreMatchers.allOf +import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.click + +/** + * Implementation of Robot Pattern for a single Addon inside of the Addons Management Settings. + */ + +class SettingsSubMenuAddonsManagerAddonDetailedMenuRobot { + + fun verifyCurrentAddonMenu() = assertAddonMenuItems() + + class Transition { + fun goBack(interact: SettingsSubMenuAddonsManagerRobot.() -> Unit): SettingsSubMenuAddonsManagerRobot.Transition { + fun goBackButton() = onView(allOf(withContentDescription("Navigate up"))) + goBackButton().click() + + SettingsSubMenuAddonsManagerRobot().interact() + return SettingsSubMenuAddonsManagerRobot.Transition() + } + + fun removeAddon(interact: SettingsSubMenuAddonsManagerRobot.() -> Unit): SettingsSubMenuAddonsManagerRobot.Transition { + removeAddonButton().click() + + SettingsSubMenuAddonsManagerRobot().interact() + return SettingsSubMenuAddonsManagerRobot.Transition() + } + } + + private fun assertAddonMenuItems() { + enableSwitchButton().check(matches(isCompletelyDisplayed())) + settingsButton().check(matches(isCompletelyDisplayed())) + detailsButton().check(matches(isCompletelyDisplayed())) + permissionsButton().check(matches(isCompletelyDisplayed())) + removeAddonButton().check(matches(isCompletelyDisplayed())) + } +} + +private fun enableSwitchButton() = + onView(withId(R.id.enable_switch)) + +private fun settingsButton() = + onView(withId(R.id.settings)) + +private fun detailsButton() = + onView(withId(R.id.details)) + +private fun permissionsButton() = + onView(withId(R.id.permissions)) + +private fun removeAddonButton() = + onView(withId(R.id.remove_add_on)) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerRobot.kt new file mode 100644 index 000000000..102367770 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerRobot.kt @@ -0,0 +1,228 @@ +package org.mozilla.fenix.ui.robots + +import android.widget.RelativeLayout +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.hasDescendant +import androidx.test.espresso.matcher.ViewMatchers.hasSibling +import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom +import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility +import androidx.test.espresso.matcher.ViewMatchers.Visibility +import androidx.test.espresso.matcher.ViewMatchers.withParent +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Until +import org.hamcrest.CoreMatchers.allOf +import org.hamcrest.CoreMatchers.containsString +import org.hamcrest.CoreMatchers.instanceOf +import org.hamcrest.CoreMatchers.not +import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.HomeActivityTestRule +import org.mozilla.fenix.helpers.IdlingResourceHelper.registerAddonInstallingIdlingResource +import org.mozilla.fenix.helpers.IdlingResourceHelper.unregisterAddonInstallingIdlingResource +import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime +import org.mozilla.fenix.helpers.TestHelper +import org.mozilla.fenix.helpers.click +import org.mozilla.fenix.helpers.ext.waitNotNull + +/** + * Implementation of Robot Pattern for the Addons Management Settings. + */ + +class SettingsSubMenuAddonsManagerRobot { + fun verifyAddonPrompt(addonName: String) = assertAddonPrompt(addonName) + fun clickInstallAddon(addonName: String) = selectInstallAddon(addonName) + fun verifyDownloadAddonPrompt(addonName: String, activityTestRule: HomeActivityTestRule) = + assertDownloadingAddonPrompt(addonName, activityTestRule) + + fun cancelInstallAddon() = cancelInstall() + fun acceptInstallAddon() = allowInstall() + fun verifyAddonsItems() = assertAddonsItems() + fun verifyAddonCanBeInstalled(addonName: String) = assertAddonCanBeInstalled(addonName) + + class Transition { + fun goBack(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition { + fun goBackButton() = onView(allOf(withContentDescription("Navigate up"))) + goBackButton().click() + + HomeScreenRobot().interact() + return HomeScreenRobot.Transition() + } + + fun openDetailedMenuForAddon( + addonName: String, + interact: SettingsSubMenuAddonsManagerAddonDetailedMenuRobot.() -> Unit + ): SettingsSubMenuAddonsManagerAddonDetailedMenuRobot.Transition { + addonName.chars() + + onView( + allOf( + withId(R.id.add_on_item), + hasDescendant( + allOf( + withId(R.id.add_on_name), + withText(addonName) + ) + ) + ) + ).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + .perform(click()) + + SettingsSubMenuAddonsManagerAddonDetailedMenuRobot().interact() + return SettingsSubMenuAddonsManagerAddonDetailedMenuRobot.Transition() + } + } + + private fun installButtonForAddon(addonName: String) = + onView( + allOf( + withContentDescription(R.string.mozac_feature_addons_install_addon_content_description), + isDescendantOfA(withId(R.id.add_on_item)), + hasSibling(hasDescendant(withText(addonName))) + ) + ) + + private fun selectInstallAddon(addonName: String) { + mDevice.waitNotNull( + Until.findObject(By.textContains(addonName)), + waitingTime + ) + + installButtonForAddon(addonName) + .check(matches(isCompletelyDisplayed())) + .perform(click()) + } + + private fun assertAddonIsEnabled(addonName: String) { + installButtonForAddon(addonName) + .check(matches(not(isCompletelyDisplayed()))) + } + + private fun assertAddonPrompt(addonName: String) { + onView(allOf(withId(R.id.title), withText("Add $addonName?"))) + .check(matches(isCompletelyDisplayed())) + + onView( + allOf( + withId(R.id.permissions), + withText(containsString("It requires your permission to:")) + ) + ) + .check(matches(isCompletelyDisplayed())) + + onView(allOf(withId(R.id.allow_button), withText("Add"))) + .check(matches(isCompletelyDisplayed())) + + onView(allOf(withId(R.id.deny_button), withText("Cancel"))) + .check(matches(isCompletelyDisplayed())) + } + + private fun assertDownloadingAddonPrompt( + addonName: String, + activityTestRule: HomeActivityTestRule + ) { + registerAddonInstallingIdlingResource(activityTestRule) + + onView( + allOf( + withText("Okay, Got it"), + withParent(instanceOf(RelativeLayout::class.java)), + hasSibling(withText("$addonName has been added to Firefox Preview")), + hasSibling(withText("Open it in the menu")), + hasSibling(withText("Allow in private browsing")) + ) + ) + .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + .perform(click()) + + unregisterAddonInstallingIdlingResource(activityTestRule) + + TestHelper.scrollToElementByText(addonName) + + assertAddonIsInstalled(addonName) + } + + private fun assertAddonIsInstalled(addonName: String) { + onView( + allOf( + withId(R.id.add_button), + isDescendantOfA(withId(R.id.add_on_item)), + hasSibling(hasDescendant(withText(addonName))) + ) + ).check(matches(withEffectiveVisibility(Visibility.GONE))) + } + + private fun cancelInstall() { + onView(allOf(withId(R.id.deny_button), withText("Cancel"))) + .check(matches(isCompletelyDisplayed())) + .perform(click()) + } + + private fun allowInstall() { + onView(allOf(withId(R.id.allow_button), withText("Add"))) + .check(matches(isCompletelyDisplayed())) + .perform(click()) + } + + private fun assertAddonsItems() { + assertRecommendedTitleDisplayed() + assertAddons() + } + + private fun assertRecommendedTitleDisplayed() { + onView(allOf(withId(R.id.title), withText("Recommended"))) + .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + } + + private fun assertEnabledTitleDisplayed() { + onView(withText("Enabled")) + .check(matches(isCompletelyDisplayed())) + } + + private fun assertAddons() { + assertAddonUblock() + } + + private fun assertAddonUblock() { + onView( + allOf( + isAssignableFrom(RelativeLayout::class.java), + withId(R.id.add_on_item), + hasDescendant(allOf(withId(R.id.add_on_icon), isCompletelyDisplayed())), + hasDescendant( + allOf( + withId(R.id.details_container), + hasDescendant(withText("uBlock Origin")), + hasDescendant(withText("Finally, an efficient wide-spectrum content blocker. Easy on CPU and memory.")), + hasDescendant(withId(R.id.rating)), + hasDescendant(withId(R.id.users_count)) + ) + ), + hasDescendant(withId(R.id.add_button)) + ) + ).check(matches(isCompletelyDisplayed())) + } + + private fun assertAddonCanBeInstalled(addonName: String) { + device.waitNotNull(Until.findObject(By.text(addonName)), waitingTime) + + onView( + allOf( + withId(R.id.add_button), + hasSibling( + hasDescendant( + allOf( + withId(R.id.add_on_name), + withText(addonName) + ) + ) + ) + ) + ).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + } +} 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 638674192..8acea8478 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 @@ -29,6 +29,8 @@ import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until.findObject import org.hamcrest.CoreMatchers.allOf +import org.hamcrest.CoreMatchers.anyOf +import org.hamcrest.CoreMatchers.containsString import org.mozilla.fenix.R import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime @@ -179,10 +181,21 @@ private fun tabMediaControlButton() = onView(withId(R.id.play_pause_button)) private fun closeTabButton() = onView(withId(R.id.mozac_browser_tabstray_close)) private fun assertCloseTabsButton(title: String) = - onView(allOf(withId(R.id.mozac_browser_tabstray_close), withContentDescription("Close tab $title"))) + onView( + allOf( + withId(R.id.mozac_browser_tabstray_close), + withContentDescription("Close tab $title") + ) + ) .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) -private fun normalBrowsingButton() = onView(withContentDescription("Open tabs")) +private fun normalBrowsingButton() = onView( + anyOf( + withContentDescription(containsString("open tabs. Tap to switch tabs.")), + withContentDescription(containsString("open tab. Tap to switch tabs.")) + ) +) + private fun privateBrowsingButton() = onView(withContentDescription("Private tabs")) private fun newTabButton() = onView(withId(R.id.new_tab_button)) private fun threeDotMenu() = onView(withId(R.id.tab_tray_overflow)) 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 51665856b..fe335c79c 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 @@ -347,6 +347,17 @@ class ThreeDotMenuMainRobot { ThreeDotMenuMainRobot().interact() return ThreeDotMenuMainRobot.Transition() } + + fun openAddonsManagerMenu(interact: SettingsSubMenuAddonsManagerRobot.() -> Unit): SettingsSubMenuAddonsManagerRobot.Transition { + clickAddonsManagerButton() + mDevice.waitNotNull( + Until.findObject(By.text("Recommended")), + waitingTime + ) + + SettingsSubMenuAddonsManagerRobot().interact() + return SettingsSubMenuAddonsManagerRobot.Transition() + } } } @@ -423,7 +434,9 @@ private fun addNewCollectionButton() = onView(allOf(withText("Add new collection private fun assertaddNewCollectionButton() = addNewCollectionButton() .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -private fun collectionNameTextField() = onView(allOf(withResourceName("name_collection_edittext"))) +private fun collectionNameTextField() = + onView(allOf(withResourceName("name_collection_edittext"))) + private fun assertCollectionNameTextField() = collectionNameTextField() .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) @@ -473,6 +486,7 @@ private fun assertReaderViewAppearanceButton(visible: Boolean) = readerViewAppea private fun addToFirefoxHomeButton() = onView(allOf(withText(R.string.browser_menu_add_to_top_sites))) + private fun assertAddToFirefoxHome() { onView(withId(R.id.mozac_browser_menu_recyclerView)) .perform( @@ -514,3 +528,10 @@ private fun assertOpenInAppButton() { ) ).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) } + +private fun addonsManagerButton() = onView(withText("Add-ons Manager")) + +private fun clickAddonsManagerButton() { + onView(withText("Add-ons")).click() + addonsManagerButton().click() +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a0869d63d..d523a44c5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,6 +23,7 @@ android:allowBackup="false" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:requestLegacyExternalStorage="true" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/NormalTheme" diff --git a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt index 6e6d4a04d..0804ca5ed 100644 --- a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt +++ b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt @@ -1,3 +1,7 @@ +/* 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 /** diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index 254898460..ed1a811c9 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -28,7 +28,6 @@ import androidx.navigation.NavDirections import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.NavigationUI -import androidx.recyclerview.widget.LinearLayoutManager import kotlinx.android.synthetic.main.activity_home.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -41,10 +40,7 @@ import mozilla.components.browser.session.SessionManager import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.WebExtensionState import mozilla.components.browser.state.store.BrowserStore -import mozilla.components.browser.tabstray.BrowserTabsTray -import mozilla.components.browser.thumbnails.loader.ThumbnailLoader import mozilla.components.concept.engine.EngineView -import mozilla.components.concept.tabstray.TabsTray import mozilla.components.feature.contextmenu.DefaultSelectionActionDelegate import mozilla.components.feature.search.BrowserStoreSearchAdapter import mozilla.components.feature.search.SearchAdapter @@ -97,7 +93,6 @@ import org.mozilla.fenix.settings.search.AddSearchEngineFragmentDirections import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirections import org.mozilla.fenix.share.AddNewDeviceFragmentDirections import org.mozilla.fenix.sync.SyncedTabsFragmentDirections -import org.mozilla.fenix.tabtray.FenixTabsAdapter import org.mozilla.fenix.tabtray.TabTrayDialogFragment import org.mozilla.fenix.theme.DefaultThemeManager import org.mozilla.fenix.theme.ThemeManager @@ -315,17 +310,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { actionSorter = ::actionSorter ) }.asView() - TabsTray::class.java.name -> { - val layout = LinearLayoutManager(context).apply { - reverseLayout = true - stackFromEnd = true - } - - val thumbnailLoader = ThumbnailLoader(components.core.thumbnailStorage) - val adapter = FenixTabsAdapter(context, thumbnailLoader) - - BrowserTabsTray(context, attrs, 0, adapter, layout) - } else -> super.onCreateView(parent, name, context, attrs) } 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 0c5387476..831acd06c 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -12,6 +12,7 @@ import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.accessibility.AccessibilityManager import androidx.annotation.CallSuper import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.net.toUri @@ -93,8 +94,10 @@ import org.mozilla.fenix.components.toolbar.BrowserToolbarViewInteractor import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarController import org.mozilla.fenix.components.toolbar.SwipeRefreshScrollingViewBehavior 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.accessibilityManager import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.enterToImmersiveMode import org.mozilla.fenix.ext.hideToolbar @@ -104,7 +107,6 @@ import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.sessionsOfType import org.mozilla.fenix.ext.settings import org.mozilla.fenix.home.SharedViewModel -import org.mozilla.fenix.tabtray.TabTrayDialogFragment import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.utils.allowUndo import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration @@ -118,7 +120,7 @@ import java.lang.ref.WeakReference @ExperimentalCoroutinesApi @Suppress("TooManyFunctions", "LargeClass") abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, SessionManager.Observer, - OnBackLongPressedListener { + OnBackLongPressedListener, AccessibilityManager.AccessibilityStateChangeListener { private lateinit var browserFragmentStore: BrowserFragmentStore private lateinit var browserAnimator: BrowserAnimator @@ -228,7 +230,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session tabCollectionStorage = requireComponents.core.tabCollectionStorage, topSiteStorage = requireComponents.core.topSiteStorage, onTabCounterClicked = { - TabTrayDialogFragment.show(parentFragmentManager) + findNavController().nav( + R.id.browserFragment, + BrowserFragmentDirections.actionGlobalTabTrayDialogFragment() + ) }, onCloseTab = { val snapshot = sessionManager.createSessionSnapshot(it) @@ -243,7 +248,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session } viewLifecycleOwner.lifecycleScope.allowUndo( - requireView(), + requireView().browserLayout, snackbarMessage, requireContext().getString(R.string.snackbar_deleted_undo), { @@ -264,7 +269,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session _browserToolbarView = BrowserToolbarView( container = view.browserLayout, - shouldUseBottomToolbar = context.settings().shouldUseBottomToolbar, + toolbarPosition = context.settings().toolbarPosition, interactor = browserInteractor, customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, lifecycleOwner = viewLifecycleOwner @@ -307,7 +312,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session feature = ContextMenuFeature( fragmentManager = parentFragmentManager, store = store, - candidates = getContextMenuCandidates(context, view), + candidates = getContextMenuCandidates(context, view.browserLayout), engineView = view.engineView, useCases = context.components.useCases.contextMenuUseCases, tabId = customTabSessionId @@ -677,10 +682,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session private fun initializeEngineView(toolbarHeight: Int) { engineView.setDynamicToolbarMaxHeight(toolbarHeight) - val behavior = if (requireContext().settings().shouldUseBottomToolbar) { - EngineViewBottomBehavior(context, null) - } else { - SwipeRefreshScrollingViewBehavior(requireContext(), null, engineView, browserToolbarView) + val context = requireContext() + val behavior = when (context.settings().toolbarPosition) { + ToolbarPosition.BOTTOM -> EngineViewBottomBehavior(context, null) + ToolbarPosition.TOP -> SwipeRefreshScrollingViewBehavior(context, null, engineView, browserToolbarView) } (swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams).behavior = behavior @@ -713,6 +718,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session super.onStart() requireComponents.core.sessionManager.register(this, this, autoPause = true) sitePermissionWifiIntegration.get()?.maybeAddWifiConnectedListener() + requireContext().accessibilityManager.addAccessibilityStateChangeListener(this) } @CallSuper @@ -825,7 +831,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session if (session.hasParentSession) { sessionManager.remove(session, true) } - val goToOverview = isLastSession || !session.hasParentSession + // We want to return to home if this removed session was the last session of its type + // and didn't have a parent session to select. + val goToOverview = isLastSession && !session.hasParentSession !goToOverview } } @@ -843,7 +851,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session * Returns the layout [android.view.Gravity] for the quick settings and ETP dialog. */ protected fun getAppropriateLayoutGravity(): Int = - if (context?.settings()?.shouldUseBottomToolbar == true) Gravity.BOTTOM else Gravity.TOP + context?.settings()?.toolbarPosition?.androidGravity ?: Gravity.BOTTOM /** * Updates the site permissions rules based on user settings. @@ -1011,6 +1019,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session super.onDestroyView() _browserToolbarView = null _browserInteractor = null + requireContext().accessibilityManager.removeAccessibilityStateChangeListener(this) } companion object { @@ -1019,4 +1028,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session private const val REQUEST_CODE_PROMPT_PERMISSIONS = 2 private const val REQUEST_CODE_APP_PERMISSIONS = 3 } + + override fun onAccessibilityStateChanged(enabled: Boolean) { + browserToolbarView.setScrollFlags(enabled) + } } diff --git a/app/src/main/java/org/mozilla/fenix/browser/BrowserAnimator.kt b/app/src/main/java/org/mozilla/fenix/browser/BrowserAnimator.kt index 5bb2fc83f..02a94f2d1 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserAnimator.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserAnimator.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import mozilla.components.concept.engine.EngineView import org.mozilla.fenix.R +import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.ext.settings import java.lang.ref.WeakReference @@ -155,12 +156,15 @@ class BrowserAnimator( fun getToolbarNavOptions(context: Context): NavOptions { val navOptions = NavOptions.Builder() - if (!context.settings().shouldUseBottomToolbar) { - navOptions.setEnterAnim(R.anim.fade_in) - navOptions.setExitAnim(R.anim.fade_out) - } else { - navOptions.setEnterAnim(R.anim.fade_in_up) - navOptions.setExitAnim(R.anim.fade_out_down) + when (context.settings().toolbarPosition) { + ToolbarPosition.TOP -> { + navOptions.setEnterAnim(R.anim.fade_in) + navOptions.setExitAnim(R.anim.fade_out) + } + ToolbarPosition.BOTTOM -> { + navOptions.setEnterAnim(R.anim.fade_in_up) + navOptions.setExitAnim(R.anim.fade_out_down) + } } return navOptions.build() diff --git a/app/src/main/java/org/mozilla/fenix/browser/CustomTabContextMenuCandidate.kt b/app/src/main/java/org/mozilla/fenix/browser/CustomTabContextMenuCandidate.kt index e9b9c9771..ed96a50cd 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/CustomTabContextMenuCandidate.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/CustomTabContextMenuCandidate.kt @@ -1,6 +1,6 @@ /* 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/. */ + * 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 diff --git a/app/src/main/java/org/mozilla/fenix/browser/UriOpenedObserver.kt b/app/src/main/java/org/mozilla/fenix/browser/UriOpenedObserver.kt index 213bec6a9..3c42b1f1e 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/UriOpenedObserver.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/UriOpenedObserver.kt @@ -12,6 +12,7 @@ import mozilla.components.browser.session.SessionManager import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.metrics +import org.mozilla.fenix.ext.sessionsOfType import org.mozilla.fenix.ext.settings import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry import org.mozilla.fenix.utils.Settings @@ -44,25 +45,29 @@ class UriOpenedObserver( session.register(singleSessionObserver, owner) } + private fun saveOpenTabsCount() { + settings.setOpenTabsCount(sessionManager.sessionsOfType(private = false).count()) + } + override fun onAllSessionsRemoved() { - settings.setOpenTabsCount(sessionManager.sessions.filter { !it.private }.size) + saveOpenTabsCount() sessionManager.sessions.forEach { it.unregister(singleSessionObserver) } } override fun onSessionAdded(session: Session) { - settings.setOpenTabsCount(sessionManager.sessions.filter { !it.private }.size) + saveOpenTabsCount() session.register(singleSessionObserver, owner) } override fun onSessionRemoved(session: Session) { - settings.setOpenTabsCount(sessionManager.sessions.filter { !it.private }.size) + saveOpenTabsCount() session.unregister(singleSessionObserver) } override fun onSessionsRestored() { - settings.setOpenTabsCount(sessionManager.sessions.filter { !it.private }.size) + saveOpenTabsCount() sessionManager.sessions.forEach { it.register(singleSessionObserver, owner) } diff --git a/app/src/main/java/org/mozilla/fenix/cfr/SearchWidgetCFR.kt b/app/src/main/java/org/mozilla/fenix/cfr/SearchWidgetCFR.kt index 1bbe94e05..07d017ce0 100644 --- a/app/src/main/java/org/mozilla/fenix/cfr/SearchWidgetCFR.kt +++ b/app/src/main/java/org/mozilla/fenix/cfr/SearchWidgetCFR.kt @@ -11,7 +11,6 @@ import android.graphics.drawable.ColorDrawable import android.view.Gravity import android.view.LayoutInflater import android.view.View -import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.marginTop import kotlinx.android.synthetic.main.search_widget_cfr.view.* @@ -21,6 +20,7 @@ import org.mozilla.fenix.R import org.mozilla.fenix.components.SearchWidgetCreator import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController +import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.utils.Settings /** @@ -50,18 +50,14 @@ class SearchWidgetCFR( val searchWidgetCFRDialog = Dialog(context) val layout = LayoutInflater.from(context) .inflate(R.layout.search_widget_cfr, null) - val isBottomToolbar = settings.shouldUseBottomToolbar + val toolbarPosition = settings.toolbarPosition - layout.drop_down_triangle.isGone = isBottomToolbar - layout.pop_up_triangle.isVisible = isBottomToolbar + layout.drop_down_triangle.isVisible = toolbarPosition == ToolbarPosition.TOP + layout.pop_up_triangle.isVisible = toolbarPosition == ToolbarPosition.BOTTOM val toolbar = getToolbar() - val gravity = if (isBottomToolbar) { - Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM - } else { - Gravity.CENTER_HORIZONTAL or Gravity.TOP - } + val gravity = Gravity.CENTER_HORIZONTAL or toolbarPosition.androidGravity layout.cfr_neg_button.setOnClickListener { metrics.track(Event.SearchWidgetCFRNotNowPressed) 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 eecb1f761..b93775caa 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationController.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationController.kt @@ -15,6 +15,8 @@ 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 { @@ -92,7 +94,7 @@ class DefaultCollectionCreationController( } metrics.track( - Event.CollectionSaved(normalSessionSize(sessionManager), sessionBundle.size) + Event.CollectionSaved(sessionManager.normalSessionSize(), sessionBundle.size) ) } @@ -134,7 +136,7 @@ class DefaultCollectionCreationController( } metrics.track( - Event.CollectionTabsAdded(normalSessionSize(sessionManager), sessionBundle.size) + Event.CollectionTabsAdded(sessionManager.normalSessionSize(), sessionBundle.size) ) } @@ -146,7 +148,7 @@ class DefaultCollectionCreationController( } else { SaveCollectionStep.SelectCollection }, - defaultCollectionNumber = getDefaultCollectionNumber() + defaultCollectionNumber = store.state.tabCollections.getDefaultCollectionNumber() ) ) } @@ -155,26 +157,11 @@ class DefaultCollectionCreationController( store.dispatch( CollectionCreationAction.StepChanged( SaveCollectionStep.NameCollection, - getDefaultCollectionNumber() + store.state.tabCollections.getDefaultCollectionNumber() ) ) } - /** - * Returns the new default name recommendation for a collection - * - * Algorithm: Go through all collections, make a list of their names and keep only the default ones. - * Then get the numbers from all these default names, compute the maximum number and add one. - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - fun getDefaultCollectionNumber(): Int { - return (store.state.tabCollections - .map { it.title } - .filter { it.matches(Regex("Collection\\s\\d+")) } - .map { Integer.valueOf(it.split(" ")[DEFAULT_COLLECTION_NUMBER_POSITION]) } - .max() ?: 0) + DEFAULT_INCREMENT_VALUE - } - override fun addTabToSelection(tab: Tab) { store.dispatch(CollectionCreationAction.TabAdded(tab)) } @@ -209,14 +196,4 @@ class DefaultCollectionCreationController( SaveCollectionStep.SelectTabs, SaveCollectionStep.RenameCollection -> null } } - - /** - * @return the number of currently active sessions that are neither custom nor private - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - fun normalSessionSize(sessionManager: SessionManager): Int { - return sessionManager.sessions.filter { session -> - (!session.isCustomTabSession() && !session.private) - }.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 2149d6bc3..264426c27 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationFragment.kt @@ -17,14 +17,16 @@ import kotlinx.android.synthetic.main.fragment_create_collection.view.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.plus -import mozilla.components.browser.session.SessionManager -import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.browser.state.selector.findTab +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.TabSessionState import mozilla.components.lib.publicsuffixlist.PublicSuffixList import mozilla.components.lib.state.ext.consumeFrom import org.mozilla.fenix.R import org.mozilla.fenix.components.StoreProvider +import org.mozilla.fenix.ext.getMediaStateForSession import org.mozilla.fenix.ext.requireComponents -import org.mozilla.fenix.ext.toTab +import org.mozilla.fenix.ext.toShortUrl import org.mozilla.fenix.home.Tab @ExperimentalCoroutinesApi @@ -47,12 +49,11 @@ class CollectionCreationFragment : DialogFragment() { val view = inflater.inflate(R.layout.fragment_create_collection, container, false) val args: CollectionCreationFragmentArgs by navArgs() - val sessionManager = requireComponents.core.sessionManager val store = requireComponents.core.store val publicSuffixList = requireComponents.publicSuffixList - val tabs = sessionManager.getTabs(args.tabIds, store, publicSuffixList) + val tabs = store.state.getTabs(args.tabIds, publicSuffixList) val selectedTabs = if (args.selectedTabIds != null) { - sessionManager.getTabs(args.selectedTabIds, store, publicSuffixList).toSet() + store.state.getTabs(args.selectedTabIds, publicSuffixList).toSet() } else { if (tabs.size == 1) setOf(tabs.first()) else emptySet() } @@ -112,14 +113,30 @@ class CollectionCreationFragment : DialogFragment() { } } -@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) -fun SessionManager.getTabs( +@VisibleForTesting +internal fun BrowserState.getTabs( tabIds: Array?, - store: BrowserStore, publicSuffixList: PublicSuffixList ): List { return tabIds - ?.mapNotNull { this.findSessionById(it) } - ?.map { it.toTab(store, publicSuffixList) } - ?: emptyList() + ?.mapNotNull { id -> findTab(id) } + ?.map { it.toTab(this, publicSuffixList) } + .orEmpty() +} + +private fun TabSessionState.toTab( + state: BrowserState, + publicSuffixList: PublicSuffixList, + selected: Boolean? = null +): Tab { + val url = readerState.activeUrl ?: content.url + return Tab( + sessionId = this.id, + url = url, + hostname = url.toShortUrl(publicSuffixList), + title = content.title, + selected = selected, + icon = content.icon, + mediaState = state.getMediaStateForSession(this.id) + ) } diff --git a/app/src/main/java/org/mozilla/fenix/collections/TabDiffUtil.kt b/app/src/main/java/org/mozilla/fenix/collections/TabDiffUtil.kt index 35c87c9b6..a555dd8f0 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/TabDiffUtil.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/TabDiffUtil.kt @@ -10,7 +10,6 @@ import org.mozilla.fenix.home.Tab /** * Diff callback for comparing tab lists with selected state. */ -@Suppress("LongParameterList") internal class TabDiffUtil( private val old: List, private val new: List, diff --git a/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt b/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt index 4ac0145d9..f4b5a7710 100644 --- a/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt +++ b/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt @@ -47,6 +47,7 @@ import org.mozilla.fenix.utils.Settings * background worker. */ @Mockable +@Suppress("LongParameterList") class BackgroundServices( private val context: Context, private val push: Push, 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 5574844f1..d6ecf2c16 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Core.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Core.kt @@ -43,10 +43,10 @@ import mozilla.components.feature.webnotifications.WebNotificationFeature import mozilla.components.lib.dataprotect.SecureAbove22Preferences import mozilla.components.lib.dataprotect.generateEncryptionKey import mozilla.components.service.digitalassetlinks.RelationChecker -import mozilla.components.service.digitalassetlinks.api.DigitalAssetLinksApi +import mozilla.components.service.digitalassetlinks.local.StatementApi +import mozilla.components.service.digitalassetlinks.local.StatementRelationChecker import mozilla.components.service.sync.logins.SyncableLoginsStorage import org.mozilla.fenix.AppRequestInterceptor -import org.mozilla.fenix.BuildConfig.DIGITAL_ASSET_LINKS_TOKEN import org.mozilla.fenix.Config import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R @@ -147,7 +147,7 @@ class Core(private val context: Context) { * The [RelationChecker] checks Digital Asset Links relationships for Trusted Web Activities. */ val relationChecker: RelationChecker by lazy { - DigitalAssetLinksApi(client, DIGITAL_ASSET_LINKS_TOKEN) + StatementRelationChecker(StatementApi(client)) } /** diff --git a/app/src/main/java/org/mozilla/fenix/components/InflationAwareFeature.kt b/app/src/main/java/org/mozilla/fenix/components/InflationAwareFeature.kt index 79375c77b..66540b7a6 100644 --- a/app/src/main/java/org/mozilla/fenix/components/InflationAwareFeature.kt +++ b/app/src/main/java/org/mozilla/fenix/components/InflationAwareFeature.kt @@ -1,3 +1,7 @@ +/* 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 android.view.View 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 793de49da..21fd6be42 100644 --- a/app/src/main/java/org/mozilla/fenix/components/IntentProcessors.kt +++ b/app/src/main/java/org/mozilla/fenix/components/IntentProcessors.kt @@ -25,6 +25,7 @@ import org.mozilla.fenix.utils.Mockable * Component group for miscellaneous components. */ @Mockable +@Suppress("LongParameterList") class IntentProcessors( private val context: Context, private val sessionManager: SessionManager, diff --git a/app/src/main/java/org/mozilla/fenix/components/Push.kt b/app/src/main/java/org/mozilla/fenix/components/Push.kt index 183157a61..7f712a7ae 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Push.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Push.kt @@ -1,3 +1,7 @@ +/* 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 android.content.Context diff --git a/app/src/main/java/org/mozilla/fenix/components/StoreProvider.kt b/app/src/main/java/org/mozilla/fenix/components/StoreProvider.kt index d84cdc672..da0ebafd7 100644 --- a/app/src/main/java/org/mozilla/fenix/components/StoreProvider.kt +++ b/app/src/main/java/org/mozilla/fenix/components/StoreProvider.kt @@ -1,6 +1,6 @@ /* 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/. */ + * 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 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 fba92eaaa..2cec0c741 100644 --- a/app/src/main/java/org/mozilla/fenix/components/UseCases.kt +++ b/app/src/main/java/org/mozilla/fenix/components/UseCases.kt @@ -27,6 +27,7 @@ import org.mozilla.fenix.utils.Mockable * modules and can be triggered by UI interactions. */ @Mockable +@Suppress("LongParameterList") class UseCases( private val context: Context, private val engine: Engine, 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 677e0aba7..4bf89abbd 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 @@ -50,6 +50,7 @@ import org.mozilla.fenix.GleanMetrics.TopSites import org.mozilla.fenix.GleanMetrics.TrackingProtection import org.mozilla.fenix.GleanMetrics.UserSpecifiedSearchEngines import org.mozilla.fenix.GleanMetrics.VoiceSearch +import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings import org.mozilla.fenix.utils.BrowsersCache @@ -724,10 +725,9 @@ class GleanMetricsService(private val context: Context) : MetricsService { } toolbarPosition.set( - if (context.settings().shouldUseBottomToolbar) { - Event.ToolbarPositionChanged.Position.BOTTOM.name - } else { - Event.ToolbarPositionChanged.Position.TOP.name + when (context.settings().toolbarPosition) { + ToolbarPosition.BOTTOM -> Event.ToolbarPositionChanged.Position.BOTTOM.name + ToolbarPosition.TOP -> Event.ToolbarPositionChanged.Position.TOP.name } ) } 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 c5011f1e5..419cd5e9d 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 @@ -4,47 +4,38 @@ package org.mozilla.fenix.components.toolbar -import android.graphics.Color -import android.graphics.drawable.ColorDrawable -import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.LinearLayout -import android.widget.PopupWindow import androidx.annotation.LayoutRes -import androidx.annotation.VisibleForTesting import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat -import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams import androidx.lifecycle.LifecycleOwner import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP -import com.google.android.material.snackbar.Snackbar import kotlinx.android.extensions.LayoutContainer -import kotlinx.android.synthetic.main.browser_toolbar_popup_window.view.* import kotlinx.android.synthetic.main.component_browser_top_toolbar.* import kotlinx.android.synthetic.main.component_browser_top_toolbar.view.* 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.store.BrowserStore import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.behavior.BrowserToolbarBottomBehavior import mozilla.components.browser.toolbar.display.DisplayToolbar import mozilla.components.support.ktx.android.util.dpToFloat import mozilla.components.support.utils.URLStringUtils import org.mozilla.fenix.R -import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.customtabs.CustomTabToolbarIntegration import org.mozilla.fenix.customtabs.CustomTabToolbarMenu import org.mozilla.fenix.ext.bookmarkStorage import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings import org.mozilla.fenix.theme.ThemeManager +import org.mozilla.fenix.utils.ToolbarPopupWindow +import java.lang.ref.WeakReference interface BrowserToolbarViewInteractor { fun onBrowserToolbarPaste(text: String) @@ -56,10 +47,11 @@ interface BrowserToolbarViewInteractor { fun onScrolled(offset: Int) fun onReaderModePressed(enabled: Boolean) } + @SuppressWarnings("LargeClass") class BrowserToolbarView( private val container: ViewGroup, - private val shouldUseBottomToolbar: Boolean, + private val toolbarPosition: ToolbarPosition, private val interactor: BrowserToolbarViewInteractor, private val customTabSession: Session?, private val lifecycleOwner: LifecycleOwner @@ -71,9 +63,9 @@ class BrowserToolbarView( private val settings = container.context.settings() @LayoutRes - private val toolbarLayout = when { - settings.shouldUseBottomToolbar -> R.layout.component_bottom_browser_toolbar - else -> R.layout.component_browser_top_toolbar + private val toolbarLayout = when (settings.toolbarPosition) { + ToolbarPosition.BOTTOM -> R.layout.component_bottom_browser_toolbar + ToolbarPosition.TOP -> R.layout.component_browser_top_toolbar } private val layout = LayoutInflater.from(container.context) @@ -88,63 +80,19 @@ class BrowserToolbarView( val isCustomTabSession = customTabSession != null view.display.setOnUrlLongClickListener { - val clipboard = view.context.components.clipboardHandler - val customView = LayoutInflater.from(view.context) - .inflate(R.layout.browser_toolbar_popup_window, null) - val popupWindow = PopupWindow( - customView, - LinearLayout.LayoutParams.WRAP_CONTENT, - view.context.resources.getDimensionPixelSize(R.dimen.context_menu_height), - true + ToolbarPopupWindow.show( + WeakReference(view), + customTabSession, + interactor::onBrowserToolbarPasteAndGo, + interactor::onBrowserToolbarPaste ) - popupWindow.elevation = - view.context.resources.getDimension(R.dimen.mozac_browser_menu_elevation) - - // This is a workaround for SDK<23 to allow popup dismissal on outside or back button press - // See: https://github.com/mozilla-mobile/fenix/issues/10027 - popupWindow.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - - customView.paste.isVisible = !clipboard.text.isNullOrEmpty() && !isCustomTabSession - customView.paste_and_go.isVisible = - !clipboard.text.isNullOrEmpty() && !isCustomTabSession - - customView.copy.setOnClickListener { - popupWindow.dismiss() - clipboard.text = getUrlForClipboard(it.context.components.core.store, customTabSession) - - FenixSnackbar.make( - view = view, - duration = Snackbar.LENGTH_SHORT, - isDisplayedWithBrowserToolbar = true - ) - .setText(view.context.getString(R.string.browser_toolbar_url_copied_to_clipboard_snackbar)) - .show() - } - - customView.paste.setOnClickListener { - popupWindow.dismiss() - interactor.onBrowserToolbarPaste(clipboard.text!!) - } - - customView.paste_and_go.setOnClickListener { - popupWindow.dismiss() - interactor.onBrowserToolbarPasteAndGo(clipboard.text!!) - } - - popupWindow.showAsDropDown( - view, - view.context.resources.getDimensionPixelSize(R.dimen.context_menu_x_offset), - 0, - Gravity.START - ) - true } with(container.context) { val sessionManager = components.core.sessionManager - if (!shouldUseBottomToolbar) { + if (toolbarPosition == ToolbarPosition.TOP) { val offsetChangedListener = AppBarLayout.OnOffsetChangedListener { _: AppBarLayout?, verticalOffset: Int -> interactor.onScrolled(verticalOffset) @@ -167,10 +115,9 @@ class BrowserToolbarView( false } - display.progressGravity = if (shouldUseBottomToolbar) { - DisplayToolbar.Gravity.TOP - } else { - DisplayToolbar.Gravity.BOTTOM + display.progressGravity = when (toolbarPosition) { + ToolbarPosition.BOTTOM -> DisplayToolbar.Gravity.TOP + ToolbarPosition.TOP -> DisplayToolbar.Gravity.BOTTOM } val primaryTextColor = ContextCompat.getColor( @@ -207,7 +154,7 @@ class BrowserToolbarView( this, sessionManager, customTabSession?.id, - shouldReverseItems = !shouldUseBottomToolbar, + shouldReverseItems = toolbarPosition == ToolbarPosition.TOP, onItemTapped = { interactor.onBrowserToolbarMenuItemTapped(it) } @@ -216,7 +163,7 @@ class BrowserToolbarView( menuToolbar = DefaultToolbarMenu( context = this, hasAccountProblem = components.backgroundServices.accountManager.accountNeedsReauth(), - shouldReverseItems = !shouldUseBottomToolbar, + shouldReverseItems = toolbarPosition == ToolbarPosition.TOP, onItemTapped = { interactor.onBrowserToolbarMenuItemTapped(it) }, lifecycleOwner = lifecycleOwner, sessionManager = sessionManager, @@ -243,7 +190,7 @@ class BrowserToolbarView( menuToolbar, ShippedDomainsProvider().also { it.initialize(this) }, components.core.historyStorage, - components.core.sessionManager, + lifecycleOwner, sessionId = null, isPrivate = sessionManager.selectedSession?.private ?: false, interactor = interactor, @@ -254,12 +201,15 @@ class BrowserToolbarView( } fun expand() { - if (settings.shouldUseBottomToolbar) { - (view.layoutParams as CoordinatorLayout.LayoutParams).apply { - (behavior as BrowserToolbarBottomBehavior).forceExpand(view) + when (settings.toolbarPosition) { + ToolbarPosition.BOTTOM -> { + (view.layoutParams as CoordinatorLayout.LayoutParams).apply { + (behavior as BrowserToolbarBottomBehavior).forceExpand(view) + } + } + ToolbarPosition.TOP -> { + layout.app_bar?.setExpanded(true) } - } else if (!settings.shouldUseBottomToolbar) { - layout.app_bar?.setExpanded(true) } } @@ -268,43 +218,30 @@ class BrowserToolbarView( * Note that the bottom toolbar has a feature flag for being dynamic, so it may not get flags set. */ fun setScrollFlags(shouldDisableScroll: Boolean = false) { - if (view.context.settings().shouldUseBottomToolbar) { - if (view.layoutParams is CoordinatorLayout.LayoutParams) { - (view.layoutParams as CoordinatorLayout.LayoutParams).apply { + when (settings.toolbarPosition) { + ToolbarPosition.BOTTOM -> { + (view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply { behavior = BrowserToolbarBottomBehavior(view.context, null) } } - - return - } - - val params = view.layoutParams as AppBarLayout.LayoutParams - - params.scrollFlags = when (view.context.settings().shouldUseFixedTopToolbar || shouldDisableScroll) { - true -> { - // Force expand the toolbar so the user is not stuck with a hidden toolbar - expand() - 0 - } - false -> { - SCROLL_FLAG_SCROLL or SCROLL_FLAG_ENTER_ALWAYS or SCROLL_FLAG_SNAP or SCROLL_FLAG_EXIT_UNTIL_COLLAPSED + ToolbarPosition.TOP -> { + view.updateLayoutParams { + scrollFlags = if (settings.shouldUseFixedTopToolbar || shouldDisableScroll) { + // Force expand the toolbar so the user is not stuck with a hidden toolbar + expand() + 0 + } else { + SCROLL_FLAG_SCROLL or + SCROLL_FLAG_ENTER_ALWAYS or + SCROLL_FLAG_SNAP or + SCROLL_FLAG_EXIT_UNTIL_COLLAPSED + } + } } } - - view.layoutParams = params } companion object { private const val TOOLBAR_ELEVATION = 16 - - @VisibleForTesting - internal fun getUrlForClipboard(store: BrowserStore, customTabSession: Session? = null): String? { - return if (customTabSession != null) { - customTabSession.url - } else { - val selectedTab = store.state.selectedTab - selectedTab?.readerState?.activeUrl ?: selectedTab?.content?.url - } - } } } diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt index caf922208..49251ba2e 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt @@ -42,7 +42,7 @@ import org.mozilla.fenix.theme.ThemeManager * @param lifecycleOwner View lifecycle owner used to determine when to cancel UI jobs. * @param bookmarksStorage Used to check if a page is bookmarked. */ -@Suppress("LargeClass") +@Suppress("LargeClass", "LongParameterList") class DefaultToolbarMenu( private val context: Context, private val sessionManager: SessionManager, diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/TabCounterMenuItem.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/TabCounterMenuItem.kt index 89d5c5b99..a74853af5 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/TabCounterMenuItem.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/TabCounterMenuItem.kt @@ -1,3 +1,7 @@ +/* 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.toolbar sealed class TabCounterMenuItem { diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/TabCounterToolbarButton.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/TabCounterToolbarButton.kt index f8739c2ad..e841d7c69 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/TabCounterToolbarButton.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/TabCounterToolbarButton.kt @@ -8,17 +8,21 @@ import android.content.Context import android.util.TypedValue import android.view.View import android.view.ViewGroup +import androidx.lifecycle.LifecycleOwner +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.map import mozilla.components.browser.menu.BrowserMenu import mozilla.components.browser.menu.BrowserMenuBuilder import mozilla.components.browser.menu.item.BrowserMenuDivider import mozilla.components.browser.menu.item.BrowserMenuImageText -import mozilla.components.browser.session.Session -import mozilla.components.browser.session.SessionManager +import mozilla.components.browser.state.selector.getNormalOrPrivateTabs import mozilla.components.concept.toolbar.Toolbar +import mozilla.components.lib.state.ext.flowScoped +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.components -import org.mozilla.fenix.ext.sessionsOfType import org.mozilla.fenix.ext.settings import org.mozilla.fenix.theme.ThemeManager import java.lang.ref.WeakReference @@ -26,8 +30,9 @@ import java.lang.ref.WeakReference /** * A [Toolbar.Action] implementation that shows a [TabCounter]. */ +@OptIn(ExperimentalCoroutinesApi::class) class TabCounterToolbarButton( - private val sessionManager: SessionManager, + private val lifecycleOwner: LifecycleOwner, private val isPrivate: Boolean, private val onItemTapped: (TabCounterMenuItem) -> Unit = {}, private val showTabs: () -> Unit @@ -35,7 +40,11 @@ class TabCounterToolbarButton( private var reference: WeakReference = WeakReference(null) override fun createView(parent: ViewGroup): View { - sessionManager.register(sessionManagerObserver, view = parent) + parent.context.components.core.store.flowScoped(lifecycleOwner) { flow -> + flow.map { state -> state.getNormalOrPrivateTabs(isPrivate).size } + .ifChanged() + .collect { tabs -> updateCount(tabs) } + } val view = TabCounter(parent.context).apply { reference = WeakReference(this) @@ -50,10 +59,11 @@ class TabCounterToolbarButton( addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View?) { - setCount(sessionManager.sessionsOfType(private = isPrivate).count()) + setCount(context.components.core.store.state.getNormalOrPrivateTabs(isPrivate).size) } - override fun onViewDetachedFromWindow(v: View?) { /* no-op */ } + override fun onViewDetachedFromWindow(v: View?) { /* no-op */ + } }) } @@ -70,12 +80,8 @@ class TabCounterToolbarButton( override fun bind(view: View) = Unit - private fun updateCount() { - val count = sessionManager.sessionsOfType(private = isPrivate).count() - - reference.get()?.let { - it.setCountWithAnimation(count) - } + private fun updateCount(count: Int) { + reference.get()?.setCountWithAnimation(count) } private fun getTabContextMenu(context: Context): BrowserMenu { @@ -113,29 +119,10 @@ class TabCounterToolbarButton( ) return BrowserMenuBuilder( - if (context.settings().shouldUseBottomToolbar) { - menuItems.reversed() - } else { - menuItems + when (context.settings().toolbarPosition) { + ToolbarPosition.BOTTOM -> menuItems.reversed() + ToolbarPosition.TOP -> menuItems } ).build(context) } - - private val sessionManagerObserver = object : SessionManager.Observer { - override fun onSessionAdded(session: Session) { - updateCount() - } - - override fun onSessionRemoved(session: Session) { - updateCount() - } - - override fun onSessionsRestored() { - updateCount() - } - - override fun onAllSessionsRemoved() { - updateCount() - } - } } 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 fa183a89a..ceb2c21a0 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 @@ -6,10 +6,10 @@ package org.mozilla.fenix.components.toolbar import android.content.Context import androidx.appcompat.content.res.AppCompatResources +import androidx.lifecycle.LifecycleOwner import com.airbnb.lottie.LottieCompositionFactory import com.airbnb.lottie.LottieDrawable import mozilla.components.browser.domains.autocomplete.DomainAutocompleteProvider -import mozilla.components.browser.session.SessionManager import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.display.DisplayToolbar import mozilla.components.concept.engine.Engine @@ -74,7 +74,7 @@ class DefaultToolbarIntegration( toolbarMenu: ToolbarMenu, domainAutocompleteProvider: DomainAutocompleteProvider, historyStorage: HistoryStorage, - sessionManager: SessionManager, + lifecycleOwner: LifecycleOwner, sessionId: String? = null, isPrivate: Boolean, interactor: BrowserToolbarViewInteractor, @@ -135,10 +135,11 @@ class DefaultToolbarIntegration( val onTabCounterMenuItemTapped = { item: TabCounterMenuItem -> interactor.onTabCounterMenuItemTapped(item) } - val tabsAction = TabCounterToolbarButton(sessionManager, isPrivate, onTabCounterMenuItemTapped) { - toolbar.hideKeyboard() - interactor.onTabCounterClicked() - } + val tabsAction = + TabCounterToolbarButton(lifecycleOwner, isPrivate, onTabCounterMenuItemTapped) { + toolbar.hideKeyboard() + interactor.onTabCounterClicked() + } toolbar.addBrowserAction(tabsAction) val engineForSpeculativeConnects = if (!isPrivate) engine else null diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarPosition.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarPosition.kt new file mode 100644 index 000000000..b69b6fef8 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarPosition.kt @@ -0,0 +1,19 @@ +/* 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.toolbar + +import android.view.Gravity + +/** + * Fenix lets the browser toolbar be placed at either the top or the bottom of the screen. + * This enum represents the posible positions. + * + * @property androidGravity [Gravity] value corresponding to the position. + * Used to position related elements such as a CFR tooltip. + */ +enum class ToolbarPosition(val androidGravity: Int) { + BOTTOM(Gravity.BOTTOM), + TOP(Gravity.TOP) +} diff --git a/app/src/main/java/org/mozilla/fenix/downloads/DynamicDownloadDialog.kt b/app/src/main/java/org/mozilla/fenix/downloads/DynamicDownloadDialog.kt index 23f11f3e6..cbf6b949c 100644 --- a/app/src/main/java/org/mozilla/fenix/downloads/DynamicDownloadDialog.kt +++ b/app/src/main/java/org/mozilla/fenix/downloads/DynamicDownloadDialog.kt @@ -21,8 +21,8 @@ import org.mozilla.fenix.ext.settings * [DynamicDownloadDialog] is used to show a view in the current tab to the user, triggered when * downloadFeature.onDownloadStopped gets invoked. It uses [DynamicDownloadDialogBehavior] to * hide when the users scrolls through a website as to not impede his activities. - * */ - + */ +@Suppress("LongParameterList") class DynamicDownloadDialog( private val container: ViewGroup, private val downloadState: DownloadState?, diff --git a/app/src/main/java/org/mozilla/fenix/ext/Activity.kt b/app/src/main/java/org/mozilla/fenix/ext/Activity.kt index ab0ce02fe..7dd2b3dce 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/Activity.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/Activity.kt @@ -7,8 +7,6 @@ package org.mozilla.fenix.ext import android.app.Activity import android.view.View import android.view.WindowManager -import mozilla.components.support.base.log.Log -import org.mozilla.fenix.perf.Performance /** * Attempts to call immersive mode using the View to hide the status bar and navigation buttons. @@ -24,17 +22,3 @@ fun Activity.enterToImmersiveMode() { or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) } - -/** - * Calls [Activity.reportFullyDrawn] while also preventing crashes under some circumstances. - */ -fun Activity.reportFullyDrawnSafe() { - try { - reportFullyDrawn() - } catch (e: SecurityException) { - // This exception is throw on some Samsung devices. We were unable to identify the root - // cause but suspect it's related to Samsung security features. See - // https://github.com/mozilla-mobile/fenix/issues/12345#issuecomment-655058864 for details. - Log.log(Log.Priority.ERROR, Performance.TAG, e, "Unable to call reportFullyDrawn") - } -} diff --git a/app/src/main/java/org/mozilla/fenix/ext/BrowserState.kt b/app/src/main/java/org/mozilla/fenix/ext/BrowserState.kt new file mode 100644 index 000000000..3339b01b1 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/ext/BrowserState.kt @@ -0,0 +1,16 @@ +/* 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.ext + +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.MediaState + +fun BrowserState.getMediaStateForSession(sessionId: String): MediaState.State { + return if (media.aggregate.activeTabId == sessionId) { + media.aggregate.state + } else { + MediaState.State.NONE + } +} diff --git a/app/src/main/java/org/mozilla/fenix/ext/Context.kt b/app/src/main/java/org/mozilla/fenix/ext/Context.kt index b9f60dafe..1e3d3b782 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/Context.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/Context.kt @@ -9,6 +9,7 @@ import android.content.Context import android.view.ContextThemeWrapper import android.view.View import android.view.ViewGroup +import android.view.accessibility.AccessibilityManager import androidx.annotation.StringRes import mozilla.components.browser.search.SearchEngineManager import mozilla.components.support.locale.LocaleManager @@ -81,3 +82,10 @@ fun Context.getStringWithArgSafe(@StringRes resId: Int, formatArg: String): Stri return format(localizedContext.getString(resId), formatArg) } } + +/** + * Used to obtain a reference to an AccessibilityManager + * @return accessibilityManager + */ +val Context.accessibilityManager: AccessibilityManager get() = + getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager diff --git a/app/src/main/java/org/mozilla/fenix/ext/Session.kt b/app/src/main/java/org/mozilla/fenix/ext/Session.kt deleted file mode 100644 index 2c830a214..000000000 --- a/app/src/main/java/org/mozilla/fenix/ext/Session.kt +++ /dev/null @@ -1,43 +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.ext - -import android.content.Context -import mozilla.components.browser.session.Session -import mozilla.components.browser.state.selector.findTab -import mozilla.components.browser.state.state.MediaState -import mozilla.components.browser.state.store.BrowserStore -import mozilla.components.lib.publicsuffixlist.PublicSuffixList -import org.mozilla.fenix.home.Tab - -fun Session.toTab(context: Context, selected: Boolean? = null): Tab = - this.toTab( - context.components.core.store, - context.components.publicSuffixList, - selected - ) - -fun Session.toTab(store: BrowserStore, publicSuffixList: PublicSuffixList, selected: Boolean? = null): Tab { - val url = store.state.findTab(this.id)?.readerState?.activeUrl ?: this.url - return Tab( - sessionId = this.id, - url = url, - hostname = url.toShortUrl(publicSuffixList), - title = this.title, - selected = selected, - icon = this.icon, - mediaState = getMediaStateForSession(store, this) - ) -} - -private fun getMediaStateForSession(store: BrowserStore, session: Session): MediaState.State { - // For now we are looking up the media state for this session in the BrowserStore. Eventually - // we will migrate away from Session(Manager) and can use BrowserStore and BrowserState directly. - return if (store.state.media.aggregate.activeTabId == session.id) { - store.state.media.aggregate.state - } else { - MediaState.State.NONE - } -} diff --git a/app/src/main/java/org/mozilla/fenix/ext/SessionManager.kt b/app/src/main/java/org/mozilla/fenix/ext/SessionManager.kt index 67bfdec74..ba25e340d 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/SessionManager.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/SessionManager.kt @@ -11,3 +11,12 @@ import mozilla.components.browser.session.SessionManager */ fun SessionManager.sessionsOfType(private: Boolean) = sessions.asSequence().filter { it.private == private } + +/** + * @return the number of currently active sessions that are neither custom nor private + */ +fun SessionManager.normalSessionSize(): Int { + return this.sessions.filter { session -> + (!session.isCustomTabSession() && !session.private) + }.size +} diff --git a/app/src/main/java/org/mozilla/fenix/ext/String.kt b/app/src/main/java/org/mozilla/fenix/ext/String.kt index 56744f49e..5bf8bae38 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/String.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/String.kt @@ -4,13 +4,11 @@ package org.mozilla.fenix.ext -import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import android.util.Patterns import android.webkit.URLUtil -import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.core.net.toUri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking @@ -94,8 +92,8 @@ private fun Uri.isIpv6(): Boolean { /** * Trim a host's prefix and suffix */ -fun String.urlToTrimmedHost(context: Context): String = runBlocking { - urlToTrimmedHost(context.components.publicSuffixList).await() +fun String.urlToTrimmedHost(publicSuffixList: PublicSuffixList): String = runBlocking { + urlToTrimmedHost(publicSuffixList).await() } /** @@ -115,16 +113,6 @@ fun String.simplifiedUrl(): String { return afterScheme } -/** - * Gets a rounded drawable from a URL if possible, else null. - */ -suspend fun String.toRoundedDrawable(context: Context, client: Client) = bitmapForUrl(this, client)?.let { bitmap -> - RoundedBitmapDrawableFactory.create(context.resources, bitmap).also { - it.isCircular = true - it.setAntiAlias(true) - } -} - suspend fun bitmapForUrl(url: String, client: Client): Bitmap? = withContext(Dispatchers.IO) { // Code below will cache it in Gecko's cache, which ensures that as long as we've fetched it once, // we will be able to display this avatar as long as the cache isn't purged (e.g. via 'clear user data'). diff --git a/app/src/main/java/org/mozilla/fenix/ext/TabCollection.kt b/app/src/main/java/org/mozilla/fenix/ext/TabCollection.kt index 8f66bd933..ad13cab6b 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/TabCollection.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/TabCollection.kt @@ -9,6 +9,7 @@ import androidx.annotation.ColorInt import androidx.core.content.ContextCompat import mozilla.components.feature.tab.collections.TabCollection import org.mozilla.fenix.R +import org.mozilla.fenix.collections.DefaultCollectionCreationController import kotlin.math.abs /** @@ -22,3 +23,17 @@ fun TabCollection.getIconColor(context: Context): Int { iconColors.recycle() return color } + +/** + * Returns the new default name recommendation for a collection + * + * Algorithm: Go through all collections, make a list of their names and keep only the default ones. + * Then get the numbers from all these default names, compute the maximum number and add one. + */ +fun List.getDefaultCollectionNumber(): Int { + return (this + .map { it.title } + .filter { it.matches(Regex("Collection\\s\\d+")) } + .map { Integer.valueOf(it.split(" ")[DefaultCollectionCreationController.DEFAULT_COLLECTION_NUMBER_POSITION]) } + .max() ?: 0) + DefaultCollectionCreationController.DEFAULT_INCREMENT_VALUE +} 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 0f3d75878..d6e01c7c5 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -42,7 +42,6 @@ import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE -import com.google.android.material.appbar.AppBarLayout import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.fragment_home.* import kotlinx.android.synthetic.main.fragment_home.view.* @@ -60,9 +59,11 @@ import mozilla.components.browser.menu.item.BrowserMenuImageText import mozilla.components.browser.menu.view.MenuButton import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager +import mozilla.components.browser.state.selector.getNormalOrPrivateTabs 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.store.BrowserStore import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.OAuthAccount @@ -83,6 +84,7 @@ import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.tips.FenixTipManager import org.mozilla.fenix.components.tips.providers.MigrationTipProvider +import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.hideToolbar import org.mozilla.fenix.ext.metrics @@ -91,7 +93,6 @@ import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.resetPoliciesAfter import org.mozilla.fenix.ext.sessionsOfType import org.mozilla.fenix.ext.settings -import org.mozilla.fenix.ext.toTab import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor import org.mozilla.fenix.home.sessioncontrol.SessionControlView @@ -100,13 +101,12 @@ import org.mozilla.fenix.onboarding.FenixOnboarding import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.SupportUtils.SumoTopic.HELP import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit -import org.mozilla.fenix.tabtray.TabTrayDialogFragment import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.utils.FragmentPreDrawManager +import org.mozilla.fenix.utils.ToolbarPopupWindow import org.mozilla.fenix.utils.allowUndo import org.mozilla.fenix.whatsnew.WhatsNew import java.lang.ref.WeakReference -import kotlin.math.abs import kotlin.math.min @ExperimentalCoroutinesApi @@ -120,16 +120,12 @@ class HomeFragment : Fragment() { } private val snackbarAnchorView: View? - get() { - return if (requireContext().settings().shouldUseBottomToolbar) { - toolbarLayout - } else { - null - } + get() = when (requireContext().settings().toolbarPosition) { + ToolbarPosition.BOTTOM -> toolbarLayout + ToolbarPosition.TOP -> null } private val browsingModeManager get() = (activity as HomeActivity).browsingModeManager - private var homeAppBarOffset = 0 private val collectionStorageObserver = object : TabCollectionStorage.Observer { override fun onCollectionCreated(title: String, sessions: List) { @@ -147,8 +143,9 @@ class HomeFragment : Fragment() { private val sessionManager: SessionManager get() = requireComponents.core.sessionManager + private val store: BrowserStore + get() = requireComponents.core.store - private lateinit var homeAppBarOffSetListener: AppBarLayout.OnOffsetChangedListener private val onboarding by lazy { FenixOnboarding(requireContext()) } private lateinit var homeFragmentStore: HomeFragmentStore private var _sessionControlInteractor: SessionControlInteractor? = null @@ -218,7 +215,6 @@ class HomeFragment : Fragment() { ) ) updateLayout(view) - setOffset(view) sessionControlView = SessionControlView( view.sessionControlRecyclerView, sessionControlInteractor, @@ -253,45 +249,36 @@ class HomeFragment : Fragment() { } private fun updateLayout(view: View) { - val shouldUseBottomToolbar = view.context.settings().shouldUseBottomToolbar - - if (!shouldUseBottomToolbar) { - view.toolbarLayout.layoutParams = CoordinatorLayout.LayoutParams( - ConstraintLayout.LayoutParams.MATCH_PARENT, - ConstraintLayout.LayoutParams.WRAP_CONTENT - ) - .apply { + when (view.context.settings().toolbarPosition) { + ToolbarPosition.TOP -> { + view.toolbarLayout.layoutParams = CoordinatorLayout.LayoutParams( + ConstraintLayout.LayoutParams.MATCH_PARENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ).apply { gravity = Gravity.TOP } - ConstraintSet().apply { - clone(view.toolbarLayout) - clear(view.bottom_bar.id, BOTTOM) - clear(view.bottomBarShadow.id, BOTTOM) - connect(view.bottom_bar.id, TOP, PARENT_ID, TOP) - connect(view.bottomBarShadow.id, TOP, view.bottom_bar.id, BOTTOM) - connect(view.bottomBarShadow.id, BOTTOM, PARENT_ID, BOTTOM) - applyTo(view.toolbarLayout) + ConstraintSet().apply { + clone(view.toolbarLayout) + clear(view.bottom_bar.id, BOTTOM) + clear(view.bottomBarShadow.id, BOTTOM) + connect(view.bottom_bar.id, TOP, PARENT_ID, TOP) + connect(view.bottomBarShadow.id, TOP, view.bottom_bar.id, BOTTOM) + connect(view.bottomBarShadow.id, BOTTOM, PARENT_ID, BOTTOM) + applyTo(view.toolbarLayout) + } + + view.bottom_bar.background = resources.getDrawable( + ThemeManager.resolveAttribute(R.attr.bottomBarBackgroundTop, requireContext()), + null + ) + + view.homeAppBar.updateLayoutParams { + topMargin = HEADER_MARGIN.dpToPx(resources.displayMetrics) + } } - - view.bottom_bar.background = resources.getDrawable( - ThemeManager.resolveAttribute(R.attr.bottomBarBackgroundTop, requireContext()), - null - ) - - view.homeAppBar.updateLayoutParams { - topMargin = HEADER_MARGIN.dpToPx(resources.displayMetrics) + ToolbarPosition.BOTTOM -> { } - - createNewAppBarListener(HEADER_MARGIN.dpToPx(resources.displayMetrics).toFloat()) - view.homeAppBar.addOnOffsetChangedListener( - homeAppBarOffSetListener - ) - } else { - createNewAppBarListener(0F) - view.homeAppBar.addOnOffsetChangedListener( - homeAppBarOffSetListener - ) } } @@ -352,6 +339,16 @@ class HomeFragment : Fragment() { requireComponents.analytics.metrics.track(Event.SearchBarTapped(Event.SearchBarTapped.Source.HOME)) } + view.toolbar_wrapper.setOnLongClickListener { + ToolbarPopupWindow.show( + WeakReference(view), + handlePasteAndGo = sessionControlInteractor::onPasteAndGo, + handlePaste = sessionControlInteractor::onPaste, + copyVisible = false + ) + true + } + view.tab_button.setOnClickListener { openTabTray() } @@ -407,46 +404,95 @@ class HomeFragment : Fragment() { } bundleArgs.getString(SESSION_TO_DELETE)?.also { - sessionManager.findSessionById(it)?.let { session -> - val snapshot = sessionManager.createSessionSnapshot(session) - val state = snapshot.engineSession?.saveState() - val isSelected = - session.id == requireComponents.core.store.state.selectedTabId ?: false - - val snackbarMessage = if (snapshot.session.private) { - requireContext().getString(R.string.snackbar_private_tab_closed) - } else { - requireContext().getString(R.string.snackbar_tab_closed) - } - - viewLifecycleOwner.lifecycleScope.allowUndo( - requireView(), - snackbarMessage, - requireContext().getString(R.string.snackbar_deleted_undo), - { - sessionManager.add( - snapshot.session, - isSelected, - engineSessionState = state - ) - findNavController().navigate(HomeFragmentDirections.actionHomeFragmentToBrowserFragment(null)) - }, - operation = { }, - anchorView = snackbarAnchorView - ) - requireComponents.useCases.tabsUseCases.removeTab.invoke(session) + if (it == ALL_NORMAL_TABS || it == ALL_PRIVATE_TABS) { + removeAllTabsAndShowSnackbar(it) + } else { + removeTabAndShowSnackbar(it) } } updateTabCounter(requireComponents.core.store.state) } + private fun removeAllTabsAndShowSnackbar(sessionCode: String) { + val tabs = sessionManager.sessionsOfType(private = sessionCode == ALL_PRIVATE_TABS).toList() + val selectedIndex = sessionManager + .selectedSession?.let { sessionManager.sessions.indexOf(it) } ?: 0 + + val snapshot = tabs + .map(sessionManager::createSessionSnapshot) + .map { + it.copy( + engineSession = null, + engineSessionState = it.engineSession?.saveState() + ) + } + .let { SessionManager.Snapshot(it, selectedIndex) } + + tabs.forEach { + sessionManager.remove(it) + } + + val snackbarMessage = if (sessionCode == ALL_PRIVATE_TABS) { + getString(R.string.snackbar_private_tabs_closed) + } else { + getString(R.string.snackbar_tabs_closed) + } + + viewLifecycleOwner.lifecycleScope.allowUndo( + requireView(), + snackbarMessage, + requireContext().getString(R.string.snackbar_deleted_undo), + { + sessionManager.restore(snapshot) + }, + operation = { }, + anchorView = snackbarAnchorView + ) + } + + private fun removeTabAndShowSnackbar(sessionId: String) { + sessionManager.findSessionById(sessionId)?.let { session -> + val snapshot = sessionManager.createSessionSnapshot(session) + val state = snapshot.engineSession?.saveState() + val isSelected = + session.id == requireComponents.core.store.state.selectedTabId ?: false + + sessionManager.remove(session) + + val snackbarMessage = if (snapshot.session.private) { + requireContext().getString(R.string.snackbar_private_tab_closed) + } else { + requireContext().getString(R.string.snackbar_tab_closed) + } + + viewLifecycleOwner.lifecycleScope.allowUndo( + requireView(), + snackbarMessage, + requireContext().getString(R.string.snackbar_deleted_undo), + { + sessionManager.add( + snapshot.session, + isSelected, + engineSessionState = state + ) + findNavController().navigate( + HomeFragmentDirections.actionHomeFragmentToBrowserFragment( + null + ) + ) + }, + operation = { }, + anchorView = snackbarAnchorView + ) + } + } + override fun onDestroyView() { super.onDestroyView() _sessionControlInteractor = null sessionControlView = null bundleArgs.clear() - requireView().homeAppBar.removeOnOffsetChangedListener(homeAppBarOffSetListener) requireActivity().window.clearFlags(FLAG_SECURE) } @@ -561,7 +607,6 @@ class HomeFragment : Fragment() { ) ) } - calculateNewOffset() } private fun recommendPrivateBrowsingShortcut() { @@ -776,10 +821,6 @@ class HomeFragment : Fragment() { } } - private fun getNumberOfSessions(private: Boolean = browsingModeManager.mode.isPrivate): Int { - return sessionManager.sessionsOfType(private = private).count() - } - private fun registerCollectionStorageObserver() { requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this) } @@ -791,7 +832,9 @@ class HomeFragment : Fragment() { viewLifecycleOwner.lifecycleScope.launch { val recyclerView = sessionControlView!!.view delay(ANIM_SCROLL_DELAY) - val tabsSize = getNumberOfSessions() + val tabsSize = store.state + .getNormalOrPrivateTabs(browsingModeManager.mode.isPrivate) + .size var indexOfCollection = tabsSize + NON_TAB_ITEM_NUM changedCollection?.let { changedCollection -> @@ -890,40 +933,11 @@ class HomeFragment : Fragment() { } } - private fun List.toTabs(): List { - val selected = sessionManager.selectedSession - - return map { - it.toTab(requireContext(), it == selected) - } - } - - private fun calculateNewOffset() { - homeAppBarOffset = ((homeAppBar.layoutParams as CoordinatorLayout.LayoutParams) - .behavior as AppBarLayout.Behavior).topAndBottomOffset - } - - private fun setOffset(currentView: View) { - if (homeAppBarOffset <= 0) { - (currentView.homeAppBar.layoutParams as CoordinatorLayout.LayoutParams) - .behavior = AppBarLayout.Behavior().apply { - topAndBottomOffset = this@HomeFragment.homeAppBarOffset - } - } else { - currentView.homeAppBar.setExpanded(false) - } - } - - private fun createNewAppBarListener(margin: Float) { - homeAppBarOffSetListener = - AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset -> - val reduceScrollRanged = appBarLayout.totalScrollRange.toFloat() - margin - appBarLayout.alpha = 1.0f - abs(verticalOffset / reduceScrollRanged) - } - } - private fun openTabTray() { - TabTrayDialogFragment.show(parentFragmentManager) + findNavController().nav( + R.id.homeFragment, + HomeFragmentDirections.actionGlobalTabTrayDialogFragment() + ) } private fun updateTabCounter(browserState: BrowserState) { @@ -938,6 +952,9 @@ class HomeFragment : Fragment() { } companion object { + const val ALL_NORMAL_TABS = "all_normal" + const val ALL_PRIVATE_TABS = "all_private" + private const val FOCUS_ON_ADDRESS_BAR = "focusOnAddressBar" private const val SESSION_TO_DELETE = "session_to_delete" private const val ANIMATION_DELAY = 100L 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 2cf7a5471..85c267842 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 @@ -15,6 +15,7 @@ import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.ext.restore import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.feature.top.sites.TopSite +import mozilla.components.support.ktx.kotlin.isUrl import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R @@ -24,9 +25,13 @@ import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.TopSiteStorage import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController +import org.mozilla.fenix.components.metrics.MetricsUtils 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.ext.settings import org.mozilla.fenix.home.HomeFragment import org.mozilla.fenix.home.HomeFragmentAction import org.mozilla.fenix.home.HomeFragmentDirections @@ -38,7 +43,7 @@ import mozilla.components.feature.tab.collections.Tab as ComponentTab * [HomeFragment] controller. An interface that handles the view manipulation of the Tabs triggered * by the Interactor. */ -@SuppressWarnings("TooManyFunctions") +@Suppress("TooManyFunctions") interface SessionControlController { /** * @see [CollectionInteractor.onCollectionAddTabTapped] @@ -120,15 +125,28 @@ interface SessionControlController { */ fun handleToggleCollectionExpanded(collection: TabCollection, expand: Boolean) + /** + * @see [TipInteractor.onCloseTip] + */ fun handleCloseTip(tip: Tip) + /** + * @see [ToolbarInteractor.onPasteAndGo] + */ + fun handlePasteAndGo(clipboardText: String) + + /** + * @see [ToolbarInteractor.onPaste] + */ + fun handlePaste(clipboardText: String) + /** * @see [CollectionInteractor.onAddTabsToCollectionTapped] */ fun handleCreateCollection() } -@SuppressWarnings("TooManyFunctions", "LargeClass", "LongParameterList") +@Suppress("TooManyFunctions", "LargeClass") class DefaultSessionControlController( private val activity: HomeActivity, private val engine: Engine, @@ -192,8 +210,12 @@ class DefaultSessionControlController( metrics.track(Event.CollectionTabRemoved) if (collection.tabs.size == 1) { - val title = activity.resources.getString(R.string.delete_tab_and_collection_dialog_title, collection.title) - val message = activity.resources.getString(R.string.delete_tab_and_collection_dialog_message) + val title = activity.resources.getString( + R.string.delete_tab_and_collection_dialog_title, + collection.title + ) + val message = + activity.resources.getString(R.string.delete_tab_and_collection_dialog_message) showDeleteCollectionPrompt(collection, title, message) } else { viewLifecycleScope.launch(Dispatchers.IO) { @@ -208,7 +230,8 @@ class DefaultSessionControlController( } override fun handleDeleteCollectionTapped(collection: TabCollection) { - val message = activity.resources.getString(R.string.tab_collection_dialog_message, collection.title) + val message = + activity.resources.getString(R.string.tab_collection_dialog_message, collection.title) showDeleteCollectionPrompt(collection, null, message) } @@ -254,8 +277,12 @@ class DefaultSessionControlController( override fun handleSelectTopSite(url: String, isDefault: Boolean) { metrics.track(Event.TopSiteOpenInNewTab) - if (isDefault) { metrics.track(Event.TopSiteOpenDefault) } - if (url == SupportUtils.POCKET_TRENDING_URL) { metrics.track(Event.PocketTopSiteClicked) } + if (isDefault) { + metrics.track(Event.TopSiteOpenDefault) + } + if (url == SupportUtils.POCKET_TRENDING_URL) { + metrics.track(Event.PocketTopSiteClicked) + } addTabUseCase.invoke( url = url, selectTab = true, @@ -297,6 +324,13 @@ class DefaultSessionControlController( fragmentStore.dispatch(HomeFragmentAction.RemoveTip(tip)) } + private fun showTabTrayCollectionCreation() { + val directions = HomeFragmentDirections.actionGlobalTabTrayDialogFragment( + enterMultiselect = true + ) + navController.nav(R.id.homeFragment, directions) + } + private fun showCollectionCreationFragment( step: SaveCollectionStep, selectedTabIds: Array? = null, @@ -322,7 +356,7 @@ class DefaultSessionControlController( } override fun handleCreateCollection() { - showCollectionCreationFragment(step = SaveCollectionStep.SelectTabs) + showTabTrayCollectionCreation() } private fun showShareFragment(data: List) { @@ -331,4 +365,37 @@ class DefaultSessionControlController( ) navController.nav(R.id.homeFragment, directions) } + + override fun handlePasteAndGo(clipboardText: String) { + activity.openToBrowserAndLoad( + searchTermOrURL = clipboardText, + newTab = true, + from = BrowserDirection.FromHome, + engine = activity.components.search.provider.getDefaultEngine(activity) + ) + + val event = if (clipboardText.isUrl()) { + Event.EnteredUrl(false) + } else { + val searchAccessPoint = Event.PerformedSearch.SearchAccessPoint.ACTION + activity.settings().incrementActiveSearchCount() + searchAccessPoint.let { sap -> + MetricsUtils.createSearchEvent( + activity.components.search.provider.getDefaultEngine(activity), + activity, + sap + ) + } + } + + event?.let { activity.metrics.track(it) } + } + + override fun handlePaste(clipboardText: String) { + val directions = HomeFragmentDirections.actionGlobalSearch( + sessionId = null, + pastedText = clipboardText + ) + navController.nav(R.id.homeFragment, directions) + } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt index 8481fbe68..f89881d4f 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt @@ -95,6 +95,18 @@ interface CollectionInteractor { fun onAddTabsToCollectionTapped() } +interface ToolbarInteractor { + /** + * Navigates to browser with clipboard text. + */ + fun onPasteAndGo(clipboardText: String) + + /** + * Navigates to search with clipboard text. + */ + fun onPaste(clipboardText: String) +} + /** * Interface for onboarding related actions in the [SessionControlInteractor]. */ @@ -163,7 +175,8 @@ interface TopSiteInteractor { @SuppressWarnings("TooManyFunctions") class SessionControlInteractor( private val controller: SessionControlController -) : CollectionInteractor, OnboardingInteractor, TopSiteInteractor, TipInteractor, TabSessionInteractor { +) : CollectionInteractor, OnboardingInteractor, TopSiteInteractor, TipInteractor, + TabSessionInteractor, ToolbarInteractor { override fun onCollectionAddTabTapped(collection: TabCollection) { controller.handleCollectionAddTabTapped(collection) } @@ -235,4 +248,12 @@ class SessionControlInteractor( override fun onPrivateBrowsingLearnMoreClicked() { controller.handlePrivateBrowsingLearnMoreClicked() } + + override fun onPasteAndGo(clipboardText: String) { + controller.handlePasteAndGo(clipboardText) + } + + override fun onPaste(clipboardText: String) { + controller.handlePaste(clipboardText) + } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt index 7f1a1450d..c2f363140 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt @@ -20,7 +20,7 @@ import org.mozilla.fenix.home.OnboardingState // This method got a little complex with the addition of the tab tray feature flag // When we remove the tabs from the home screen this will get much simpler again. -@SuppressWarnings("LongParameterList", "ComplexMethod") +@Suppress("ComplexMethod") private fun normalModeAdapterItems( topSites: List, collections: List, diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingToolbarPositionPickerViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingToolbarPositionPickerViewHolder.kt index 87c3453ee..b484a8d3b 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingToolbarPositionPickerViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingToolbarPositionPickerViewHolder.kt @@ -10,6 +10,7 @@ import kotlinx.android.synthetic.main.onboarding_toolbar_position_picker.view.* import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event.OnboardingToolbarPosition.Position +import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.ext.asActivity import org.mozilla.fenix.ext.components import org.mozilla.fenix.onboarding.OnboardingRadioButton @@ -29,10 +30,9 @@ class OnboardingToolbarPositionPickerViewHolder(view: View) : RecyclerView.ViewH radioBottomToolbar.addIllustration(view.toolbar_bottom_image) val settings = view.context.components.settings - radio = if (settings.shouldUseBottomToolbar) { - radioBottomToolbar - } else { - radioTopToolbar + radio = when (settings.toolbarPosition) { + ToolbarPosition.BOTTOM -> radioBottomToolbar + ToolbarPosition.TOP -> radioTopToolbar } radio.updateRadioValue(true) diff --git a/app/src/main/java/org/mozilla/fenix/home/tips/ButtonTipViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/tips/ButtonTipViewHolder.kt index 1711ff071..17b31b35b 100644 --- a/app/src/main/java/org/mozilla/fenix/home/tips/ButtonTipViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/tips/ButtonTipViewHolder.kt @@ -1,23 +1,32 @@ +/* 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.tips import android.view.View -import androidx.recyclerview.widget.RecyclerView -import kotlinx.android.synthetic.main.button_tip_item.view.* +import androidx.core.view.isVisible +import kotlinx.android.synthetic.main.button_tip_item.* import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.tips.Tip import org.mozilla.fenix.components.tips.TipType import org.mozilla.fenix.ext.addUnderline import org.mozilla.fenix.ext.components -import org.mozilla.fenix.ext.settings import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor +import org.mozilla.fenix.utils.Settings +import org.mozilla.fenix.utils.view.ViewHolder class ButtonTipViewHolder( - val view: View, - val interactor: SessionControlInteractor -) : RecyclerView.ViewHolder(view) { + view: View, + private val interactor: SessionControlInteractor, + private val metrics: MetricController = view.context.components.analytics.metrics, + private val settings: Settings = view.context.components.settings +) : ViewHolder(view) { + var tip: Tip? = null fun bind(tip: Tip) { @@ -25,44 +34,39 @@ class ButtonTipViewHolder( this.tip = tip - view.apply { - context.components.analytics.metrics.track(Event.TipDisplayed(tip.identifier)) + metrics.track(Event.TipDisplayed(tip.identifier)) - tip_header_text.text = tip.title - tip_description_text.text = tip.description - tip_button.text = tip.type.text + tip_header_text.text = tip.title + tip_description_text.text = tip.description + tip_button.text = tip.type.text - if (tip.learnMoreURL == null) { - tip_learn_more.visibility = View.GONE - } else { - tip_learn_more.addUnderline() + tip_learn_more.isVisible = tip.learnMoreURL != null + if (tip.learnMoreURL != null) { + tip_learn_more.addUnderline() - tip_learn_more.setOnClickListener { - (context as HomeActivity).openToBrowserAndLoad( - searchTermOrURL = tip.learnMoreURL, - newTab = true, - from = BrowserDirection.FromHome - ) - } - } - - tip_button.setOnClickListener { - tip.type.action.invoke() - context.components.analytics.metrics.track( - Event.TipPressed(tip.identifier) + tip_learn_more.setOnClickListener { + (itemView.context as HomeActivity).openToBrowserAndLoad( + searchTermOrURL = tip.learnMoreURL, + newTab = true, + from = BrowserDirection.FromHome ) } + } - tip_close.setOnClickListener { - context.components.analytics.metrics.track(Event.TipClosed(tip.identifier)) + tip_button.setOnClickListener { + tip.type.action.invoke() + metrics.track(Event.TipPressed(tip.identifier)) + } - context.settings().preferences - .edit() - .putBoolean(tip.identifier, false) - .apply() + tip_close.setOnClickListener { + metrics.track(Event.TipClosed(tip.identifier)) - interactor.onCloseTip(tip) - } + settings.preferences + .edit() + .putBoolean(tip.identifier, false) + .apply() + + interactor.onCloseTip(tip) } } diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkController.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkController.kt index f8503653e..add69b004 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkController.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkController.kt @@ -28,7 +28,7 @@ import org.mozilla.fenix.ext.nav * [BookmarkFragment] controller. * Delegated by View Interactors, handles container business logic and operates changes on it. */ -@SuppressWarnings("TooManyFunctions") +@Suppress("TooManyFunctions") interface BookmarkController { fun handleBookmarkChanged(item: BookmarkNode) fun handleBookmarkTapped(item: BookmarkNode) @@ -47,7 +47,7 @@ interface BookmarkController { fun handleBackPressed() } -@SuppressWarnings("TooManyFunctions", "LongParameterList") +@Suppress("TooManyFunctions") class DefaultBookmarkController( private val activity: HomeActivity, private val navController: NavController, diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt index b0bce78ee..18c19943e 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt @@ -51,7 +51,6 @@ import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.toShortUrl import org.mozilla.fenix.library.LibraryPageFragment -import org.mozilla.fenix.tabtray.TabTrayDialogFragment import org.mozilla.fenix.utils.allowUndo /** @@ -240,7 +239,7 @@ class BookmarkFragment : LibraryPageFragment(), UserInteractionHan private fun showTabTray() { invokePendingDeletion() - TabTrayDialogFragment.show(parentFragmentManager) + navigate(BookmarkFragmentDirections.actionGlobalTabTrayDialogFragment()) } private fun navigate(directions: NavDirections) { diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryController.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryController.kt index 2bd32c705..46a99952e 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryController.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryController.kt @@ -1,8 +1,6 @@ -/* - * 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/. - */ +/* 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.library.history 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 8b8179fa0..3b5f6d7d8 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 @@ -44,7 +44,6 @@ import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.ext.toShortUrl import org.mozilla.fenix.library.LibraryPageFragment -import org.mozilla.fenix.tabtray.TabTrayDialogFragment import org.mozilla.fenix.utils.allowUndo @SuppressWarnings("TooManyFunctions", "LargeClass") @@ -207,7 +206,10 @@ class HistoryFragment : LibraryPageFragment(), UserInteractionHandl private fun showTabTray() { invokePendingDeletion() - TabTrayDialogFragment.show(parentFragmentManager) + findNavController().nav( + R.id.historyFragment, + HistoryFragmentDirections.actionGlobalTabTrayDialogFragment() + ) } private fun getMultiSelectSnackBarMessage(historyItems: Set): String { @@ -259,7 +261,10 @@ class HistoryFragment : LibraryPageFragment(), UserInteractionHandl launch(Main) { viewModel.invalidate() historyStore.dispatch(HistoryFragmentAction.ExitDeletionMode) - showSnackBar(requireView(), getString(R.string.preferences_delete_browsing_data_snackbar)) + showSnackBar( + requireView(), + getString(R.string.preferences_delete_browsing_data_snackbar) + ) } } diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragmentStore.kt index f19ffc41c..08d6f3682 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragmentStore.kt @@ -1,6 +1,6 @@ /* 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/. */ + * 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.library.history diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryInteractor.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryInteractor.kt index 48d0c7a1e..4ddf147a7 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryInteractor.kt @@ -1,6 +1,6 @@ /* 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/. */ + * 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.library.history diff --git a/app/src/main/java/org/mozilla/fenix/loginexceptions/ExceptionsFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/loginexceptions/ExceptionsFragmentStore.kt index 88ddfbfea..2493d3f68 100644 --- a/app/src/main/java/org/mozilla/fenix/loginexceptions/ExceptionsFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/loginexceptions/ExceptionsFragmentStore.kt @@ -1,6 +1,6 @@ /* 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/. */ + * 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.loginexceptions diff --git a/app/src/main/java/org/mozilla/fenix/loginexceptions/LoginExceptionsInteractor.kt b/app/src/main/java/org/mozilla/fenix/loginexceptions/LoginExceptionsInteractor.kt index 8a5881c05..688ca4437 100644 --- a/app/src/main/java/org/mozilla/fenix/loginexceptions/LoginExceptionsInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/loginexceptions/LoginExceptionsInteractor.kt @@ -1,6 +1,6 @@ /* 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/. */ + * 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.loginexceptions diff --git a/app/src/main/java/org/mozilla/fenix/perf/Performance.kt b/app/src/main/java/org/mozilla/fenix/perf/Performance.kt index 410295577..6fe32e802 100644 --- a/app/src/main/java/org/mozilla/fenix/perf/Performance.kt +++ b/app/src/main/java/org/mozilla/fenix/perf/Performance.kt @@ -8,6 +8,7 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.BatteryManager +import mozilla.components.support.base.log.logger.Logger import org.mozilla.fenix.ext.components import org.mozilla.fenix.onboarding.FenixOnboarding import android.provider.Settings as AndroidSettings @@ -17,6 +18,8 @@ import android.provider.Settings as AndroidSettings */ object Performance { const val TAG = "FenixPerf" + val logger = Logger(TAG) + private const val EXTRA_IS_PERFORMANCE_TEST = "performancetest" /** diff --git a/app/src/main/java/org/mozilla/fenix/perf/StartupReportFullyDrawn.kt b/app/src/main/java/org/mozilla/fenix/perf/StartupReportFullyDrawn.kt index c30720fec..004feb0ff 100644 --- a/app/src/main/java/org/mozilla/fenix/perf/StartupReportFullyDrawn.kt +++ b/app/src/main/java/org/mozilla/fenix/perf/StartupReportFullyDrawn.kt @@ -7,9 +7,9 @@ package org.mozilla.fenix.perf import android.app.Activity import android.view.View import androidx.core.view.doOnPreDraw +import mozilla.components.support.ktx.android.view.reportFullyDrawnSafe import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R -import org.mozilla.fenix.ext.reportFullyDrawnSafe import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.TopSiteItemViewHolder import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupDestination.APP_LINK import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupDestination.HOMESCREEN @@ -65,6 +65,6 @@ class StartupReportFullyDrawn { // - the difference in timing is minimal (< 7ms on Pixel 2) // - if we compare against another app using a preDrawListener, as we are with Fennec, it // should be comparable - view.doOnPreDraw { activity.reportFullyDrawnSafe() } + view.doOnPreDraw { activity.reportFullyDrawnSafe(Performance.logger) } } } diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchController.kt b/app/src/main/java/org/mozilla/fenix/search/SearchController.kt index 3a0e2172a..b2fe99221 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchController.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchController.kt @@ -1,7 +1,6 @@ -/* - * 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/. */ +/* 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.search @@ -9,6 +8,7 @@ import android.content.Intent import androidx.navigation.NavController import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.session.Session +import mozilla.components.browser.session.SessionManager import mozilla.components.support.ktx.kotlin.isUrl import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity @@ -17,15 +17,14 @@ import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event.PerformedSearch.SearchAccessPoint.ACTION import org.mozilla.fenix.components.metrics.Event.PerformedSearch.SearchAccessPoint.NONE import org.mozilla.fenix.components.metrics.Event.PerformedSearch.SearchAccessPoint.SUGGESTION +import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricsUtils import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore import org.mozilla.fenix.crashes.CrashListActivity -import org.mozilla.fenix.ext.components -import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.navigateSafe -import org.mozilla.fenix.ext.settings import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.SupportUtils.MozillaPage.MANIFESTO +import org.mozilla.fenix.utils.Settings /** * An interface that handles the view manipulation of the Search, triggered by the Interactor @@ -43,11 +42,14 @@ interface SearchController { fun handleSearchShortcutsButtonClicked() } -@Suppress("TooManyFunctions") +@Suppress("TooManyFunctions", "LongParameterList") class DefaultSearchController( private val activity: HomeActivity, + private val sessionManager: SessionManager, private val store: SearchFragmentStore, private val navController: NavController, + private val settings: Settings, + private val metrics: MetricController, private val clearToolbarFocus: () -> Unit ) : SearchController { @@ -77,7 +79,7 @@ class DefaultSearchController( val event = if (url.isUrl()) { Event.EnteredUrl(false) } else { - activity.settings().incrementActiveSearchCount() + settings.incrementActiveSearchCount() val searchAccessPoint = when (store.state.searchAccessPoint) { NONE -> ACTION @@ -93,7 +95,7 @@ class DefaultSearchController( } } - event?.let { activity.metrics.track(it) } + event?.let { metrics.track(it) } } override fun handleEditingCancelled() { @@ -101,7 +103,6 @@ class DefaultSearchController( } override fun handleTextChanged(text: String) { - val settings = activity.settings() // Display the search shortcuts on each entry of the search fragment (see #5308) val textMatchesCurrentUrl = store.state.url == text val textMatchesCurrentSearch = store.state.searchTerms == text @@ -130,11 +131,11 @@ class DefaultSearchController( from = BrowserDirection.FromSearch ) - activity.metrics.track(Event.EnteredUrl(false)) + metrics.track(Event.EnteredUrl(false)) } override fun handleSearchTermsTapped(searchTerms: String) { - activity.settings().incrementActiveSearchCount() + settings.incrementActiveSearchCount() activity.openToBrowserAndLoad( searchTermOrURL = searchTerms, @@ -156,14 +157,14 @@ class DefaultSearchController( sap ) } - event?.let { activity.metrics.track(it) } + event?.let { metrics.track(it) } } override fun handleSearchShortcutEngineSelected(searchEngine: SearchEngine) { store.dispatch(SearchFragmentAction.SearchShortcutEngineSelected(searchEngine)) val isCustom = CustomSearchEngineStore.isCustomSearchEngine(activity, searchEngine.identifier) - activity.metrics.track(Event.SearchShortcutSelected(searchEngine, isCustom)) + metrics.track(Event.SearchShortcutSelected(searchEngine, isCustom)) } override fun handleSearchShortcutsButtonClicked() { @@ -177,14 +178,14 @@ class DefaultSearchController( } override fun handleExistingSessionSelected(session: Session) { - activity.components.core.sessionManager.select(session) + sessionManager.select(session) activity.openToBrowser( from = BrowserDirection.FromSearch ) } override fun handleExistingSessionSelected(tabId: String) { - val session = activity.components.core.sessionManager.findSessionById(tabId) + val session = sessionManager.findSessionById(tabId) if (session != null) { handleExistingSessionSelected(session) } diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt b/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt index 6250eb2cb..f0ca9bff3 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt @@ -86,6 +86,7 @@ class SearchFragment : Fragment(), UserInteractionHandler { savedInstanceState: Bundle? ): View? { val activity = activity as HomeActivity + val settings = activity.settings() val args by navArgs() val tabId = args.sessionId @@ -112,13 +113,13 @@ class SearchFragment : Fragment(), UserInteractionHandler { defaultEngineSource = currentSearchEngine, showSearchSuggestions = shouldShowSearchSuggestions(isPrivate), showSearchSuggestionsHint = false, - showSearchShortcuts = requireContext().settings().shouldShowSearchShortcuts && + showSearchShortcuts = settings.shouldShowSearchShortcuts && url.isEmpty() && areShortcutsAvailable, areShortcutsAvailable = areShortcutsAvailable, - showClipboardSuggestions = requireContext().settings().shouldShowClipboardSuggestions, - showHistorySuggestions = requireContext().settings().shouldShowHistorySuggestions, - showBookmarkSuggestions = requireContext().settings().shouldShowBookmarkSuggestions, + showClipboardSuggestions = settings.shouldShowClipboardSuggestions, + showHistorySuggestions = settings.shouldShowHistorySuggestions, + showBookmarkSuggestions = settings.shouldShowBookmarkSuggestions, tabId = tabId, pastedText = args.pastedText, searchAccessPoint = args.searchAccessPoint @@ -128,8 +129,11 @@ class SearchFragment : Fragment(), UserInteractionHandler { val searchController = DefaultSearchController( activity = activity, + sessionManager = requireComponents.core.sessionManager, store = searchStore, navController = findNavController(), + settings = settings, + metrics = requireComponents.analytics.metrics, clearToolbarFocus = ::clearToolbarFocus ) @@ -160,7 +164,11 @@ class SearchFragment : Fragment(), UserInteractionHandler { BrowserToolbar.Button( ContextCompat.getDrawable(requireContext(), R.drawable.ic_microphone)!!, requireContext().getString(R.string.voice_search_content_description), - visible = { requireContext().settings().shouldShowVoiceSearch && speechIsAvailable() }, + visible = { + currentSearchEngine.searchEngine.identifier.contains("google") && + speechIsAvailable() && + settings.shouldShowVoiceSearch + }, listener = ::launchVoiceSearch ) ) diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/search/SearchFragmentStore.kt index 7268f45eb..4bf57c522 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchFragmentStore.kt @@ -1,6 +1,6 @@ /* 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/. */ + * 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.search diff --git a/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt b/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt index 06efcafb1..09d4fa2c9 100644 --- a/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt +++ b/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt @@ -1,6 +1,6 @@ /* 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/. */ + * 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.search.toolbar diff --git a/app/src/main/java/org/mozilla/fenix/settings/CustomizationFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/CustomizationFragment.kt index d894e5514..3d586befd 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/CustomizationFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/CustomizationFragment.kt @@ -12,6 +12,7 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.preference.PreferenceFragmentCompat import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings @@ -122,8 +123,9 @@ class CustomizationFragment : PreferenceFragmentCompat() { )) } - topPreference.setCheckedWithoutClickListener(!requireContext().settings().shouldUseBottomToolbar) - bottomPreference.setCheckedWithoutClickListener(requireContext().settings().shouldUseBottomToolbar) + val toolbarPosition = requireContext().settings().toolbarPosition + topPreference.setCheckedWithoutClickListener(toolbarPosition == ToolbarPosition.TOP) + bottomPreference.setCheckedWithoutClickListener(toolbarPosition == ToolbarPosition.BOTTOM) addToRadioGroup(topPreference, bottomPreference) } 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 1a1a2cc2f..4b8a25e88 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt @@ -5,7 +5,6 @@ package org.mozilla.fenix.settings import android.content.ActivityNotFoundException -import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build @@ -13,16 +12,13 @@ import android.os.Bundle import android.os.Handler import android.provider.Settings import android.widget.Toast -import androidx.appcompat.content.res.AppCompatResources import androidx.lifecycle.lifecycleScope import androidx.navigation.NavDirections import androidx.navigation.findNavController import androidx.preference.Preference -import androidx.preference.PreferenceCategory import androidx.preference.PreferenceFragmentCompat import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.delay import kotlinx.coroutines.launch import mozilla.components.concept.sync.AccountObserver @@ -41,19 +37,19 @@ import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar -import org.mozilla.fenix.ext.toRoundedDrawable -import org.mozilla.fenix.settings.account.AccountAuthErrorPreference -import org.mozilla.fenix.settings.account.AccountPreference +import org.mozilla.fenix.settings.account.AccountUiView import kotlin.system.exitProcess @Suppress("LargeClass", "TooManyFunctions") class SettingsFragment : PreferenceFragmentCompat() { + private lateinit var accountUiView: AccountUiView + private val accountObserver = object : AccountObserver { private fun updateAccountUi(profile: Profile? = null) { val context = context ?: return lifecycleScope.launch { - updateAccountUIState( + accountUiView.updateAccountUIState( context = context, profile = profile ?: context.components.backgroundServices.accountManager.accountProfile() @@ -75,6 +71,13 @@ class SettingsFragment : PreferenceFragmentCompat() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + accountUiView = AccountUiView( + fragment = this, + accountManager = requireComponents.backgroundServices.accountManager, + httpClient = requireComponents.core.client, + updateFxASyncOverrideMenu = ::updateFxASyncOverrideMenu + ) + // Observe account changes to keep the UI up-to-date. requireComponents.backgroundServices.accountManager.register( accountObserver, @@ -88,7 +91,7 @@ class SettingsFragment : PreferenceFragmentCompat() { // For example, if user is signed-in, and we don't perform this call in onCreate, we'll briefly // display a "Sign In" preference, which will then get replaced by the correct account information // once this call is ran in onResume shortly after. - updateAccountUIState( + accountUiView.updateAccountUIState( requireContext(), requireComponents.backgroundServices.accountManager.accountProfile() ) @@ -162,7 +165,7 @@ class SettingsFragment : PreferenceFragmentCompat() { setupPreferences() if (shouldUpdateAccountUIState) { - updateAccountUIState( + accountUiView.updateAccountUIState( requireContext(), requireComponents.backgroundServices.accountManager.accountProfile() ) @@ -295,9 +298,9 @@ class SettingsFragment : PreferenceFragmentCompat() { } } - preferenceRemoteDebugging?.setOnPreferenceChangeListener { preference, newValue -> + preferenceRemoteDebugging?.setOnPreferenceChangeListener { preference, newValue -> preference.context.settings().preferences.edit() - .putBoolean(preference.key, newValue as Boolean).apply() + .putBoolean(preference.key, newValue).apply() requireComponents.core.engine.settings.remoteDebuggingEnabled = newValue true } @@ -378,68 +381,6 @@ class SettingsFragment : PreferenceFragmentCompat() { } } - /** - * Updates the UI to reflect current account state. - * Possible conditions are logged-in without problems, logged-out, and logged-in but needs to re-authenticate. - */ - private fun updateAccountUIState(context: Context, profile: Profile?) { - val preferenceSignIn = - requirePreference(R.string.pref_key_sign_in) - val preferenceFirefoxAccount = - requirePreference(R.string.pref_key_account) - val preferenceFirefoxAccountAuthError = - requirePreference(R.string.pref_key_account_auth_error) - val accountPreferenceCategory = - requirePreference(R.string.pref_key_account_category) - - val accountManager = requireComponents.backgroundServices.accountManager - val account = accountManager.authenticatedAccount() - - updateFxASyncOverrideMenu() - - // Signed-in, no problems. - if (account != null && !accountManager.accountNeedsReauth()) { - preferenceSignIn.isVisible = false - - profile?.avatar?.url?.let { avatarUrl -> - lifecycleScope.launch(Main) { - val roundedDrawable = - avatarUrl.toRoundedDrawable(context, requireComponents.core.client) - preferenceFirefoxAccount.icon = - roundedDrawable ?: AppCompatResources.getDrawable( - context, - R.drawable.ic_account - ) - } - } - preferenceSignIn.onPreferenceClickListener = null - preferenceFirefoxAccountAuthError.isVisible = false - preferenceFirefoxAccount.isVisible = true - accountPreferenceCategory.isVisible = true - - preferenceFirefoxAccount.displayName = profile?.displayName - preferenceFirefoxAccount.email = profile?.email - - // Signed-in, need to re-authenticate. - } else if (account != null && accountManager.accountNeedsReauth()) { - preferenceFirefoxAccount.isVisible = false - preferenceFirefoxAccountAuthError.isVisible = true - accountPreferenceCategory.isVisible = true - - preferenceSignIn.isVisible = false - preferenceSignIn.onPreferenceClickListener = null - - preferenceFirefoxAccountAuthError.email = profile?.email - - // Signed-out. - } else { - preferenceSignIn.isVisible = true - preferenceFirefoxAccount.isVisible = false - preferenceFirefoxAccountAuthError.isVisible = false - accountPreferenceCategory.isVisible = false - } - } - private fun updateFxASyncOverrideMenu() { val preferenceFxAOverride = findPreference(getPreferenceKey(R.string.pref_key_override_fxa_server)) diff --git a/app/src/main/java/org/mozilla/fenix/settings/SharedPreferenceUpdater.kt b/app/src/main/java/org/mozilla/fenix/settings/SharedPreferenceUpdater.kt index 7396eb212..e43688b03 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SharedPreferenceUpdater.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SharedPreferenceUpdater.kt @@ -1,3 +1,7 @@ +/* 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 import androidx.core.content.edit diff --git a/app/src/main/java/org/mozilla/fenix/settings/about/viewholders/AboutItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/settings/about/viewholders/AboutItemViewHolder.kt index d42ee1a97..220af45c2 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/about/viewholders/AboutItemViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/about/viewholders/AboutItemViewHolder.kt @@ -1,4 +1,4 @@ - /* This Source Code Form is subject to the terms of the Mozilla Public +/* 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/. */ diff --git a/app/src/main/java/org/mozilla/fenix/settings/account/AccountSettingsFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/settings/account/AccountSettingsFragmentStore.kt index 6cc70e57b..c0c32b658 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/account/AccountSettingsFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/account/AccountSettingsFragmentStore.kt @@ -1,6 +1,6 @@ /* 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/. */ + * 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.account diff --git a/app/src/main/java/org/mozilla/fenix/settings/account/AccountSettingsInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/account/AccountSettingsInteractor.kt index 2d414d260..faa79895d 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/account/AccountSettingsInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/account/AccountSettingsInteractor.kt @@ -1,6 +1,6 @@ /* 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/. */ + * 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.account diff --git a/app/src/main/java/org/mozilla/fenix/settings/account/AccountUiView.kt b/app/src/main/java/org/mozilla/fenix/settings/account/AccountUiView.kt new file mode 100644 index 000000000..bb5fa64c9 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/account/AccountUiView.kt @@ -0,0 +1,103 @@ +/* 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.account + +import android.content.Context +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory +import androidx.lifecycle.lifecycleScope +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat +import kotlinx.coroutines.launch +import mozilla.components.concept.fetch.Client +import mozilla.components.concept.sync.Profile +import mozilla.components.service.fxa.manager.FxaAccountManager +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.bitmapForUrl +import org.mozilla.fenix.settings.requirePreference + +class AccountUiView( + fragment: PreferenceFragmentCompat, + private val accountManager: FxaAccountManager, + private val httpClient: Client, + private val updateFxASyncOverrideMenu: () -> Unit +) { + + private val lifecycleScope = fragment.viewLifecycleOwner.lifecycleScope + private val preferenceSignIn = + fragment.requirePreference(R.string.pref_key_sign_in) + private val preferenceFirefoxAccount = + fragment.requirePreference(R.string.pref_key_account) + private val preferenceFirefoxAccountAuthError = + fragment.requirePreference(R.string.pref_key_account_auth_error) + private val accountPreferenceCategory = + fragment.requirePreference(R.string.pref_key_account_category) + + /** + * Updates the UI to reflect current account state. + * Possible conditions are logged-in without problems, logged-out, and logged-in but needs to re-authenticate. + */ + fun updateAccountUIState(context: Context, profile: Profile?) { + val account = accountManager.authenticatedAccount() + + updateFxASyncOverrideMenu() + + // Signed-in, no problems. + if (account != null && !accountManager.accountNeedsReauth()) { + preferenceSignIn.isVisible = false + + profile?.avatar?.url?.let { avatarUrl -> + lifecycleScope.launch { + val roundedDrawable = toRoundedDrawable(avatarUrl, context) + preferenceFirefoxAccount.icon = + roundedDrawable ?: AppCompatResources.getDrawable( + context, + R.drawable.ic_account + ) + } + } + + preferenceSignIn.onPreferenceClickListener = null + preferenceFirefoxAccountAuthError.isVisible = false + preferenceFirefoxAccount.isVisible = true + accountPreferenceCategory.isVisible = true + + preferenceFirefoxAccount.displayName = profile?.displayName + preferenceFirefoxAccount.email = profile?.email + + // Signed-in, need to re-authenticate. + } else if (account != null && accountManager.accountNeedsReauth()) { + preferenceFirefoxAccount.isVisible = false + preferenceFirefoxAccountAuthError.isVisible = true + accountPreferenceCategory.isVisible = true + + preferenceSignIn.isVisible = false + preferenceSignIn.onPreferenceClickListener = null + + preferenceFirefoxAccountAuthError.email = profile?.email + + // Signed-out. + } else { + preferenceSignIn.isVisible = true + preferenceFirefoxAccount.isVisible = false + preferenceFirefoxAccountAuthError.isVisible = false + accountPreferenceCategory.isVisible = false + } + } + + /** + * Gets a rounded drawable from a URL if possible, else null. + */ + private suspend fun toRoundedDrawable( + url: String, + context: Context + ) = bitmapForUrl(url, httpClient)?.let { bitmap -> + RoundedBitmapDrawableFactory.create(context.resources, bitmap).apply { + isCircular = true + setAntiAlias(true) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/SortingStrategy.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/SortingStrategy.kt index 2cecfd973..eae2ff2ce 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/SortingStrategy.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/SortingStrategy.kt @@ -4,20 +4,19 @@ package org.mozilla.fenix.settings.logins -import android.content.Context +import mozilla.components.lib.publicsuffixlist.PublicSuffixList import org.mozilla.fenix.ext.urlToTrimmedHost sealed class SortingStrategy { abstract operator fun invoke(logins: List): List - abstract val appContext: Context - data class Alphabetically(override val appContext: Context) : SortingStrategy() { + data class Alphabetically(private val publicSuffixList: PublicSuffixList) : SortingStrategy() { override fun invoke(logins: List): List { - return logins.sortedBy { it.origin.urlToTrimmedHost(appContext) } + return logins.sortedBy { it.origin.urlToTrimmedHost(publicSuffixList) } } } - data class LastUsed(override val appContext: Context) : SortingStrategy() { + object LastUsed : SortingStrategy() { override fun invoke(logins: List): List { return logins.sortedByDescending { it.timeLastUsed } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsFragment.kt index 16a4a8974..182a366af 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsFragment.kt @@ -6,12 +6,12 @@ package org.mozilla.fenix.settings.logins.fragment import android.os.Bundle import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.view.inputmethod.EditorInfo -import android.view.Menu -import android.view.MenuInflater import android.widget.FrameLayout import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.SearchView @@ -31,17 +31,18 @@ import org.mozilla.fenix.R import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.redirectToReAuth +import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.settings.logins.LoginsAction import org.mozilla.fenix.settings.logins.LoginsFragmentStore -import org.mozilla.fenix.settings.logins.controller.LoginsListController import org.mozilla.fenix.settings.logins.LoginsListState -import org.mozilla.fenix.settings.logins.interactor.SavedLoginsInteractor import org.mozilla.fenix.settings.logins.SavedLoginsSortingStrategyMenu -import org.mozilla.fenix.settings.logins.view.SavedLoginsListView import org.mozilla.fenix.settings.logins.SortingStrategy +import org.mozilla.fenix.settings.logins.controller.LoginsListController import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController +import org.mozilla.fenix.settings.logins.interactor.SavedLoginsInteractor +import org.mozilla.fenix.settings.logins.view.SavedLoginsListView @SuppressWarnings("TooManyFunctions") class SavedLoginsFragment : Fragment() { @@ -228,16 +229,14 @@ class SavedLoginsFragment : Fragment() { SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort -> { savedLoginsInteractor.onSortingStrategyChanged( SortingStrategy.Alphabetically( - requireContext().applicationContext + requireComponents.publicSuffixList ) ) } SavedLoginsSortingStrategyMenu.Item.LastUsedSort -> { savedLoginsInteractor.onSortingStrategyChanged( - SortingStrategy.LastUsed( - requireContext().applicationContext - ) + SortingStrategy.LastUsed ) } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ext/PhoneFeatureExt.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ext/PhoneFeatureExt.kt index 0958209f8..4169e1eb6 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ext/PhoneFeatureExt.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ext/PhoneFeatureExt.kt @@ -17,7 +17,15 @@ import org.mozilla.fenix.utils.Settings fun PhoneFeature.shouldBeVisible( sitePermissions: SitePermissions?, settings: Settings -) = getStatus(sitePermissions, settings) != SitePermissions.Status.NO_DECISION +): Boolean { + // We have to check if the site have a site permission exception, + // if it doesn't the feature shouldn't be visible + return if (sitePermissions == null) { + false + } else { + getStatus(sitePermissions, settings) != SitePermissions.Status.NO_DECISION + } +} /** * Common [PhoneFeature] extensions used for **quicksettings**. diff --git a/app/src/main/java/org/mozilla/fenix/settings/search/AddSearchEngineFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/search/AddSearchEngineFragment.kt index f210513db..ba40743f8 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/search/AddSearchEngineFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/search/AddSearchEngineFragment.kt @@ -14,6 +14,7 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.CompoundButton +import android.widget.LinearLayout import androidx.constraintlayout.widget.ConstraintLayout import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope @@ -93,7 +94,7 @@ class AddSearchEngineFragment : Fragment(), CompoundButton.OnCheckedChangeListen availableEngines.forEachIndexed(setupSearchEngineItem) - val engineItem = makeCustomButton(layoutInflater, res = resources) + val engineItem = makeCustomButton(layoutInflater) engineItem.id = CUSTOM_INDEX engineItem.radio_button.isChecked = selectedIndex == CUSTOM_INDEX engineViews.add(engineItem) @@ -249,12 +250,11 @@ class AddSearchEngineFragment : Fragment(), CompoundButton.OnCheckedChangeListen toggleCustomForm(selectedIndex == -1) } - private fun makeCustomButton(layoutInflater: LayoutInflater, res: Resources): View { + private fun makeCustomButton(layoutInflater: LayoutInflater): View { val wrapper = layoutInflater .inflate(R.layout.custom_search_engine_radio_button, null) as ConstraintLayout wrapper.setOnClickListener { wrapper.radio_button.isChecked = true } wrapper.radio_button.setOnCheckedChangeListener(this) - wrapper.minHeight = res.getDimensionPixelSize(R.dimen.radio_button_preference_height) return wrapper } @@ -271,7 +271,7 @@ class AddSearchEngineFragment : Fragment(), CompoundButton.OnCheckedChangeListen res: Resources ): View { val wrapper = layoutInflater - .inflate(R.layout.search_engine_radio_button, null) as ConstraintLayout + .inflate(R.layout.search_engine_radio_button, null) as LinearLayout wrapper.setOnClickListener { wrapper.radio_button.isChecked = true } wrapper.radio_button.setOnCheckedChangeListener(this) wrapper.engine_text.text = engine.name @@ -280,7 +280,6 @@ class AddSearchEngineFragment : Fragment(), CompoundButton.OnCheckedChangeListen engineIcon.setBounds(0, 0, iconSize, iconSize) wrapper.engine_icon.setImageDrawable(engineIcon) wrapper.overflow_menu.visibility = View.GONE - wrapper.minHeight = res.getDimensionPixelSize(R.dimen.radio_button_preference_height) return wrapper } diff --git a/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineListPreference.kt b/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineListPreference.kt index 63a88d1f2..e06f12571 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineListPreference.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineListPreference.kt @@ -12,8 +12,8 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.CompoundButton +import android.widget.LinearLayout import android.widget.RadioGroup -import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible import androidx.navigation.Navigation import androidx.preference.Preference @@ -117,9 +117,10 @@ abstract class SearchEngineListPreference @JvmOverloads constructor( res: Resources, allowDeletion: Boolean ): View { - val isCustomSearchEngine = CustomSearchEngineStore.isCustomSearchEngine(context, engine.identifier) + val isCustomSearchEngine = + CustomSearchEngineStore.isCustomSearchEngine(context, engine.identifier) - val wrapper = layoutInflater.inflate(itemResId, null) as ConstraintLayout + val wrapper = layoutInflater.inflate(itemResId, null) as LinearLayout wrapper.setOnClickListener { wrapper.radio_button.isChecked = true } wrapper.radio_button.setOnCheckedChangeListener(this) wrapper.engine_text.text = engine.name @@ -132,7 +133,11 @@ abstract class SearchEngineListPreference @JvmOverloads constructor( onItemTapped = { when (it) { is SearchEngineMenu.Item.Edit -> editCustomSearchEngine(engine) - is SearchEngineMenu.Item.Delete -> deleteSearchEngine(context, engine, isCustomSearchEngine) + is SearchEngineMenu.Item.Delete -> deleteSearchEngine( + context, + engine, + isCustomSearchEngine + ) } } ).menuBuilder.build(context).show(wrapper.overflow_menu) @@ -146,7 +151,8 @@ abstract class SearchEngineListPreference @JvmOverloads constructor( override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { searchEngineList.list.forEach { engine -> - val wrapper: ConstraintLayout = searchEngineGroup?.findViewWithTag(engine.identifier) ?: return + val wrapper: LinearLayout = + searchEngineGroup?.findViewWithTag(engine.identifier) ?: return when (wrapper.radio_button == buttonView) { true -> onSearchEngineSelected(engine) @@ -165,12 +171,20 @@ abstract class SearchEngineListPreference @JvmOverloads constructor( Navigation.findNavController(searchEngineGroup!!).navigate(directions) } - private fun deleteSearchEngine(context: Context, engine: SearchEngine, isCustomSearchEngine: Boolean) { + private fun deleteSearchEngine( + context: Context, + engine: SearchEngine, + isCustomSearchEngine: Boolean + ) { val isDefaultEngine = engine == context.components.search.provider.getDefaultEngine(context) val initialEngineList = searchEngineList.copy() val initialDefaultEngine = searchEngineList.default - context.components.search.provider.uninstallSearchEngine(context, engine, isCustomSearchEngine) + context.components.search.provider.uninstallSearchEngine( + context, + engine, + isCustomSearchEngine + ) MainScope().allowUndo( view = context.getRootView()!!, @@ -178,7 +192,11 @@ abstract class SearchEngineListPreference @JvmOverloads constructor( .getString(R.string.search_delete_search_engine_success_message, engine.name), undoActionTitle = context.getString(R.string.snackbar_deleted_undo), onCancel = { - context.components.search.provider.installSearchEngine(context, engine, isCustomSearchEngine) + context.components.search.provider.installSearchEngine( + context, + engine, + isCustomSearchEngine + ) searchEngineList = initialEngineList.copy( default = initialDefaultEngine diff --git a/app/src/main/java/org/mozilla/fenix/shortcut/PwaOnboardingObserver.kt b/app/src/main/java/org/mozilla/fenix/shortcut/PwaOnboardingObserver.kt index 5e312df86..10dd4132f 100644 --- a/app/src/main/java/org/mozilla/fenix/shortcut/PwaOnboardingObserver.kt +++ b/app/src/main/java/org/mozilla/fenix/shortcut/PwaOnboardingObserver.kt @@ -6,7 +6,6 @@ package org.mozilla.fenix.shortcut import androidx.navigation.NavController import mozilla.components.browser.session.Session -import mozilla.components.concept.engine.manifest.WebAppManifest import mozilla.components.feature.pwa.WebAppUseCases import org.mozilla.fenix.R import org.mozilla.fenix.browser.BrowserFragmentDirections @@ -22,8 +21,8 @@ class PwaOnboardingObserver( private val webAppUseCases: WebAppUseCases ) : Session.Observer { - override fun onWebAppManifestChanged(session: Session, manifest: WebAppManifest?) { - if (webAppUseCases.isInstallable() && !settings.userKnowsAboutPwas) { + override fun onLoadingStateChanged(session: Session, loading: Boolean) { + if (!loading && webAppUseCases.isInstallable() && !settings.userKnowsAboutPwas) { settings.incrementVisitedInstallableCount() if (settings.shouldShowPwaOnboarding) { val directions = diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsAdapter.kt index d45c9e8c7..69ace066f 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsAdapter.kt @@ -8,16 +8,15 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter -import mozilla.components.concept.sync.Device as SyncDevice -import mozilla.components.browser.storage.sync.Tab as SyncTab +import mozilla.components.browser.storage.sync.SyncedDeviceTabs import org.mozilla.fenix.sync.SyncedTabsViewHolder.DeviceViewHolder import org.mozilla.fenix.sync.SyncedTabsViewHolder.TabViewHolder +import mozilla.components.browser.storage.sync.Tab as SyncTab +import mozilla.components.concept.sync.Device as SyncDevice class SyncedTabsAdapter( private val listener: (SyncTab) -> Unit -) : ListAdapter( - DiffCallback -) { +) : ListAdapter(DiffCallback) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SyncedTabsViewHolder { val itemView = LayoutInflater.from(parent.context).inflate(viewType, parent, false) @@ -30,23 +29,35 @@ class SyncedTabsAdapter( } override fun onBindViewHolder(holder: SyncedTabsViewHolder, position: Int) { - val item = when (holder) { - is DeviceViewHolder -> getItem(position) as AdapterItem.Device - is TabViewHolder -> getItem(position) as AdapterItem.Tab - } - holder.bind(item, listener) + holder.bind(getItem(position), listener) } - override fun getItemViewType(position: Int): Int { - return when (getItem(position)) { - is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID - is AdapterItem.Tab -> TabViewHolder.LAYOUT_ID + override fun getItemViewType(position: Int) = when (getItem(position)) { + is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID + is AdapterItem.Tab -> TabViewHolder.LAYOUT_ID + } + + fun updateData(syncedTabs: List) { + val allDeviceTabs = mutableListOf() + + syncedTabs.forEach { (device, tabs) -> + if (tabs.isNotEmpty()) { + allDeviceTabs.add(AdapterItem.Device(device)) + tabs.mapTo(allDeviceTabs) { AdapterItem.Tab(it) } + } } + + submitList(allDeviceTabs) } private object DiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = - areContentsTheSame(oldItem, newItem) + when (oldItem) { + is AdapterItem.Device -> + newItem is AdapterItem.Device && oldItem.device.id == newItem.device.id + is AdapterItem.Tab -> + oldItem == newItem + } @Suppress("DiffUtilEquals") override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt index c5bbf4d4e..0889ccb50 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt @@ -10,10 +10,10 @@ import android.view.View import android.widget.FrameLayout import androidx.recyclerview.widget.LinearLayoutManager import kotlinx.android.synthetic.main.component_sync_tabs.view.* -import kotlinx.coroutines.launch import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch import mozilla.components.browser.storage.sync.SyncedDeviceTabs import mozilla.components.feature.syncedtabs.view.SyncedTabsView import org.mozilla.fenix.R @@ -43,15 +43,7 @@ class SyncedTabsLayout @JvmOverloads constructor( // We may still be displaying a "loading" spinner, hide it. stopLoading() - val stringResId = when (error) { - SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE -> R.string.synced_tabs_connect_another_device - SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE -> R.string.synced_tabs_enable_tab_syncing - SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> R.string.synced_tabs_connect_to_sync_account - SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth - SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> R.string.synced_tabs_no_tabs - } - - sync_tabs_status.text = context.getText(stringResId) + sync_tabs_status.text = context.getText(stringResourceForError(error)) synced_tabs_list.visibility = View.GONE sync_tabs_status.visibility = View.VISIBLE @@ -65,19 +57,7 @@ class SyncedTabsLayout @JvmOverloads constructor( synced_tabs_list.visibility = View.VISIBLE sync_tabs_status.visibility = View.GONE - val allDeviceTabs = emptyList().toMutableList() - - syncedTabs.forEach { (device, tabs) -> - if (tabs.isEmpty()) { - return@forEach - } - - val deviceTabs = tabs.map { SyncedTabsAdapter.AdapterItem.Tab(it) } - - allDeviceTabs += listOf(SyncedTabsAdapter.AdapterItem.Device(device)) + deviceTabs - } - - adapter.submitList(allDeviceTabs) + adapter.updateData(syncedTabs) } } @@ -110,5 +90,13 @@ class SyncedTabsLayout @JvmOverloads constructor( SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE, SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> true } + + internal fun stringResourceForError(error: SyncedTabsView.ErrorType) = when (error) { + SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE -> R.string.synced_tabs_connect_another_device + SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE -> R.string.synced_tabs_enable_tab_syncing + SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> R.string.synced_tabs_connect_to_sync_account + SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth + SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> R.string.synced_tabs_no_tabs + } } } diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/CollectionsAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabtray/CollectionsAdapter.kt new file mode 100644 index 000000000..67eeba43b --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabtray/CollectionsAdapter.kt @@ -0,0 +1,72 @@ +/* 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.tabtray + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.CheckedTextView +import androidx.annotation.VisibleForTesting +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.support.ktx.android.util.dpToPx +import org.mozilla.fenix.R + +internal class CollectionsAdapter( + private val collections: Array, + private val onNewCollectionClicked: () -> Unit +) : RecyclerView.Adapter() { + + @VisibleForTesting + internal var checkedPosition = 1 + + class CollectionItemViewHolder(val textView: CheckedTextView) : + RecyclerView.ViewHolder(textView) + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): CollectionItemViewHolder { + val textView = LayoutInflater.from(parent.context) + .inflate(R.layout.collection_dialog_list_item, parent, false) as CheckedTextView + return CollectionItemViewHolder(textView) + } + + override fun onBindViewHolder(holder: CollectionItemViewHolder, position: Int) { + if (position == 0) { + val displayMetrics = holder.textView.context.resources.displayMetrics + holder.textView.setPadding(NEW_COLLECTION_PADDING_START.dpToPx(displayMetrics), 0, 0, 0) + holder.textView.compoundDrawablePadding = + NEW_COLLECTION_DRAWABLE_PADDING.dpToPx(displayMetrics) + holder.textView.setCompoundDrawablesWithIntrinsicBounds( + ContextCompat.getDrawable( + holder.textView.context, + R.drawable.ic_new + ), null, null, null + ) + } else { + holder.textView.isChecked = checkedPosition == position + } + + holder.textView.setOnClickListener { + if (position == 0) { + onNewCollectionClicked() + } else if (checkedPosition != position) { + notifyItemChanged(position) + notifyItemChanged(checkedPosition) + checkedPosition = position + } + } + holder.textView.text = collections[position] + } + + override fun getItemCount() = collections.size + + fun getSelectedCollection() = checkedPosition - 1 + + companion object { + private const val NEW_COLLECTION_PADDING_START = 24 + private const val NEW_COLLECTION_DRAWABLE_PADDING = 28 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/FenixTabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabtray/FenixTabsAdapter.kt index 1610ffd91..76e85669f 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/FenixTabsAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/FenixTabsAdapter.kt @@ -6,26 +6,41 @@ package org.mozilla.fenix.tabtray import android.content.Context import android.view.LayoutInflater +import android.view.View +import androidx.core.view.isVisible +import kotlinx.android.synthetic.main.tab_tray_item.view.* import mozilla.components.browser.tabstray.TabViewHolder import mozilla.components.browser.tabstray.TabsAdapter +import mozilla.components.concept.tabstray.Tab import mozilla.components.concept.tabstray.Tabs import mozilla.components.support.images.loader.ImageLoader import org.mozilla.fenix.R +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.metrics class FenixTabsAdapter( - context: Context, + private val context: Context, imageLoader: ImageLoader ) : TabsAdapter( - viewHolderProvider = { parentView, _ -> + viewHolderProvider = { parentView -> TabTrayViewHolder( LayoutInflater.from(context).inflate( R.layout.tab_tray_item, parentView, - false), + false + ), imageLoader ) } ) { + var tabTrayInteractor: TabTrayInteractor? = null + + private val mode: TabTrayDialogFragmentState.Mode? + get() = tabTrayInteractor?.onModeRequested() + + val selectedItems get() = mode?.selectedItems ?: setOf() + var onTabsUpdated: (() -> Unit)? = null var tabCount = 0 @@ -35,9 +50,59 @@ class FenixTabsAdapter( tabCount = tabs.list.size } + override fun onBindViewHolder( + holder: TabViewHolder, + position: Int, + payloads: MutableList + ) { + if (payloads.isNullOrEmpty()) { + onBindViewHolder(holder, position) + return + } + + holder.tab?.let { showCheckedIfSelected(it, holder.itemView) } + } + override fun onBindViewHolder(holder: TabViewHolder, position: Int) { super.onBindViewHolder(holder, position) val newIndex = tabCount - position - 1 (holder as TabTrayViewHolder).updateAccessibilityRowIndex(holder.itemView, newIndex) + + holder.tab?.let { tab -> + showCheckedIfSelected(tab, holder.itemView) + + val tabIsPrivate = + context.components.core.sessionManager.findSessionById(tab.id)?.private == true + if (!tabIsPrivate) { + holder.itemView.setOnLongClickListener { + if (mode is TabTrayDialogFragmentState.Mode.Normal) { + context.metrics.track(Event.CollectionTabLongPressed) + tabTrayInteractor?.onAddSelectedTab( + tab + ) + } + true + } + } + + holder.itemView.setOnClickListener { + if (mode is TabTrayDialogFragmentState.Mode.MultiSelect) { + if (mode?.selectedItems?.contains(tab) == true) { + tabTrayInteractor?.onRemoveSelectedTab(tab = tab) + } else { + tabTrayInteractor?.onAddSelectedTab(tab = tab) + } + } else { + tabTrayInteractor?.onOpenTab(tab = tab) + } + } + } + } + + private fun showCheckedIfSelected(tab: Tab, view: View) { + val shouldBeChecked = + mode is TabTrayDialogFragmentState.Mode.MultiSelect && selectedItems.contains(tab) + view.checkmark.isVisible = shouldBeChecked + view.selected_mask.isVisible = shouldBeChecked } } diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt index d2383185d..6a8958573 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt @@ -6,76 +6,98 @@ package org.mozilla.fenix.tabtray import androidx.annotation.VisibleForTesting import androidx.navigation.NavController +import kotlinx.coroutines.ExperimentalCoroutinesApi import mozilla.components.browser.session.Session -import mozilla.components.browser.session.SessionManager import mozilla.components.concept.engine.prompt.ShareData +import mozilla.components.concept.tabstray.Tab +import mozilla.components.feature.tabs.TabsUseCases import org.mozilla.fenix.HomeActivity -import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode -import org.mozilla.fenix.collections.SaveCollectionStep +import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.sessionsOfType +import org.mozilla.fenix.home.HomeFragment /** * [TabTrayDialogFragment] controller. * * Delegated by View Interactors, handles container business logic and operates changes on it. */ +@Suppress("TooManyFunctions") interface TabTrayController { fun onNewTabTapped(private: Boolean) fun onTabTrayDismissed() fun onShareTabsClicked(private: Boolean) - fun onSaveToCollectionClicked() + fun onSaveToCollectionClicked(selectedTabs: Set) fun onCloseAllTabsClicked(private: Boolean) + fun handleBackPressed(): Boolean + fun onModeRequested(): TabTrayDialogFragmentState.Mode + fun handleAddSelectedTab(tab: Tab) + fun handleRemoveSelectedTab(tab: Tab) + fun handleOpenTab(tab: Tab) + fun handleEnterMultiselect() } +/** + * Default behavior of [TabTrayController]. Other implementations are possible. + * + * @param activity [HomeActivity] used for context and other Android interactions. + * @param navController [NavController] used for navigation. + * @param dismissTabTray callback allowing to request this entire Fragment to be dismissed. + * @param tabTrayDialogFragmentStore [TabTrayDialogFragmentStore] holding the State for all Views displayed + * in this Controller's Fragment. + * @param dismissTabTrayAndNavigateHome callback allowing showing an undo snackbar after tab deletion. + * @param selectTabUseCase [TabsUseCases.SelectTabUseCase] callback allowing for selecting a tab. + * @param registerCollectionStorageObserver callback allowing for registering the [TabCollectionStorage.Observer] + * when needed. + * @param showChooseCollectionDialog callback allowing saving a list of sessions to an existing collection. + * @param showAddNewCollectionDialog callback allowing for saving a list of sessions to a new collection. + */ @Suppress("TooManyFunctions") class DefaultTabTrayController( private val activity: HomeActivity, private val navController: NavController, private val dismissTabTray: () -> Unit, - private val showUndoSnackbar: (String, SessionManager.Snapshot) -> Unit, - private val registerCollectionStorageObserver: () -> Unit + private val dismissTabTrayAndNavigateHome: (String) -> Unit, + private val registerCollectionStorageObserver: () -> Unit, + private val tabTrayDialogFragmentStore: TabTrayDialogFragmentStore, + private val selectTabUseCase: TabsUseCases.SelectTabUseCase, + private val showChooseCollectionDialog: (List) -> Unit, + private val showAddNewCollectionDialog: (List) -> Unit ) : TabTrayController { + private val tabCollectionStorage = activity.components.core.tabCollectionStorage + override fun onNewTabTapped(private: Boolean) { val startTime = activity.components.core.engine.profiler?.getProfilerTime() activity.browsingModeManager.mode = BrowsingMode.fromBoolean(private) navController.navigate(TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true)) dismissTabTray() - activity.components.core.engine.profiler?.addMarker("DefaultTabTrayController.onNewTabTapped", startTime) + activity.components.core.engine.profiler?.addMarker( + "DefaultTabTrayController.onNewTabTapped", + startTime + ) } override fun onTabTrayDismissed() { dismissTabTray() } - override fun onSaveToCollectionClicked() { - val tabs = getListOfSessions(false) - val tabIds = tabs.map { it.id }.toList().toTypedArray() - val tabCollectionStorage = activity.components.core.tabCollectionStorage - - val step = when { - // Show the SelectTabs fragment if there are multiple opened tabs to select which tabs - // you want to save to a collection. - tabs.size > 1 -> SaveCollectionStep.SelectTabs - // If there is an existing tab collection, show the SelectCollection fragment to save - // the selected tab to a collection of your choice. - tabCollectionStorage.cachedTabCollections.isNotEmpty() -> SaveCollectionStep.SelectCollection - // Show the NameCollection fragment to create a new collection for the selected tab. - else -> SaveCollectionStep.NameCollection + override fun onSaveToCollectionClicked(selectedTabs: Set) { + val sessionList = selectedTabs.map { + activity.components.core.sessionManager.findSessionById(it.id) ?: return } - if (navController.currentDestination?.id == R.id.collectionCreationFragment) return - // Only register the observer right before moving to collection creation registerCollectionStorageObserver() - val directions = TabTrayDialogFragmentDirections.actionGlobalCollectionCreationFragment( - tabIds = tabIds, - saveCollectionStep = step, - selectedTabIds = tabIds - ) - navController.navigate(directions) + when { + tabCollectionStorage.cachedTabCollections.isNotEmpty() -> { + showChooseCollectionDialog(sessionList) + } + else -> { + showAddNewCollectionDialog(sessionList) + } + } } override fun onShareTabsClicked(private: Boolean) { @@ -89,39 +111,48 @@ class DefaultTabTrayController( navController.navigate(directions) } + @OptIn(ExperimentalCoroutinesApi::class) override fun onCloseAllTabsClicked(private: Boolean) { - val sessionManager = activity.components.core.sessionManager - val tabs = getListOfSessions(private) - - val selectedIndex = sessionManager - .selectedSession?.let { sessionManager.sessions.indexOf(it) } ?: 0 - - val snapshot = tabs - .map(sessionManager::createSessionSnapshot) - .map { - it.copy( - engineSession = null, - engineSessionState = it.engineSession?.saveState() - ) - } - .let { SessionManager.Snapshot(it, selectedIndex) } - - tabs.forEach { - sessionManager.remove(it) - } - - val snackbarMessage = if (private) { - activity.getString(R.string.snackbar_private_tabs_closed) + val sessionsToClose = if (private) { + HomeFragment.ALL_PRIVATE_TABS } else { - activity.getString(R.string.snackbar_tabs_closed) + HomeFragment.ALL_NORMAL_TABS } - showUndoSnackbar(snackbarMessage, snapshot) - dismissTabTray() + dismissTabTrayAndNavigateHome(sessionsToClose) + } + + override fun handleAddSelectedTab(tab: Tab) { + tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.AddItemForCollection(tab)) + } + + override fun handleRemoveSelectedTab(tab: Tab) { + tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.RemoveItemForCollection(tab)) + } + + override fun handleBackPressed(): Boolean { + return if (tabTrayDialogFragmentStore.state.mode is TabTrayDialogFragmentState.Mode.MultiSelect) { + tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode) + true + } else { + false + } } @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) private fun getListOfSessions(private: Boolean): List { return activity.components.core.sessionManager.sessionsOfType(private = private).toList() } + + override fun onModeRequested(): TabTrayDialogFragmentState.Mode { + return tabTrayDialogFragmentStore.state.mode + } + + override fun handleOpenTab(tab: Tab) { + selectTabUseCase.invoke(tab.id) + } + + override fun handleEnterMultiselect() { + tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.EnterMultiSelectMode) + } } diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt index e21e5e3be..f663fdd7c 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt @@ -4,51 +4,66 @@ package org.mozilla.fenix.tabtray +import android.app.Dialog import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.WindowManager +import android.widget.EditText +import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatDialogFragment import androidx.core.view.isVisible import androidx.core.view.updatePadding -import androidx.fragment.app.FragmentManager import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.component_tabstray.view.* import kotlinx.android.synthetic.main.component_tabstray_fab.view.* import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.* import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.view.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch import mozilla.components.browser.session.Session -import mozilla.components.browser.session.SessionManager -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.TabSessionState +import mozilla.components.browser.thumbnails.loader.ThumbnailLoader import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.feature.tabs.tabstray.TabsFeature import mozilla.components.lib.state.ext.consumeFrom +import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.ViewBoundFeatureWrapper +import mozilla.components.support.ktx.android.view.showKeyboard import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R +import org.mozilla.fenix.browser.BrowserFragmentDirections import org.mozilla.fenix.components.FenixSnackbar +import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.components -import org.mozilla.fenix.ext.getRootView +import org.mozilla.fenix.ext.getDefaultCollectionNumber +import org.mozilla.fenix.ext.metrics +import org.mozilla.fenix.ext.normalSessionSize import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.tabtray.TabTrayDialogFragmentState.Mode import org.mozilla.fenix.utils.allowUndo @SuppressWarnings("TooManyFunctions", "LargeClass") -class TabTrayDialogFragment : AppCompatDialogFragment() { +class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { + private val args by navArgs() + private val tabsFeature = ViewBoundFeatureWrapper() private var _tabTrayView: TabTrayView? = null private val tabTrayView: TabTrayView get() = _tabTrayView!! + private lateinit var tabTrayDialogStore: TabTrayDialogFragmentStore private val snackbarAnchor: View? get() = if (tabTrayView.fabView.new_tab_button.isVisible) tabTrayView.fabView.new_tab_button @@ -78,17 +93,36 @@ class TabTrayDialogFragment : AppCompatDialogFragment() { } } + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return object : Dialog(requireContext(), this.theme) { + override fun onBackPressed() { + this@TabTrayDialogFragment.onBackPressed() + } + } + } + private val removeTabUseCase = object : TabsUseCases.RemoveTabUseCase { override fun invoke(sessionId: String) { requireContext().components.analytics.metrics.track(Event.ClosedExistingTab) showUndoSnackbarForTab(sessionId) - requireComponents.useCases.tabsUseCases.removeTab(sessionId) + removeIfNotLastTab(sessionId) } override fun invoke(session: Session) { requireContext().components.analytics.metrics.track(Event.ClosedExistingTab) showUndoSnackbarForTab(session.id) - requireComponents.useCases.tabsUseCases.removeTab(session) + removeIfNotLastTab(session.id) + } + } + + private fun removeIfNotLastTab(sessionId: String) { + // We only want to *immediately* remove a tab if there are more than one in the tab tray + // If there is only one, the HomeFragment handles deleting the tab (to better support snackbars) + val sessionManager = view?.context?.components?.core?.sessionManager + val sessionToRemove = sessionManager?.findSessionById(sessionId) + + if (sessionManager?.sessions?.filter { sessionToRemove?.private == it.private }?.size != 1) { + requireComponents.useCases.tabsUseCases.removeTab(sessionId) } } @@ -101,7 +135,18 @@ class TabTrayDialogFragment : AppCompatDialogFragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? = inflater.inflate(R.layout.fragment_tab_tray_dialog, container, false) + ): View? { + tabTrayDialogStore = StoreProvider.get(this) { + TabTrayDialogFragmentStore( + TabTrayDialogFragmentState( + requireComponents.core.store.state, + if (args.enterMultiselect) Mode.MultiSelect(setOf()) else Mode.Normal + ) + ) + } + + return inflater.inflate(R.layout.fragment_tab_tray_dialog, container, false) + } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) @@ -120,15 +165,23 @@ class TabTrayDialogFragment : AppCompatDialogFragment() { super.onViewCreated(view, savedInstanceState) val isPrivate = (activity as HomeActivity).browsingModeManager.mode.isPrivate + val thumbnailLoader = ThumbnailLoader(requireContext().components.core.thumbnailStorage) + val adapter = FenixTabsAdapter(requireContext(), thumbnailLoader) + _tabTrayView = TabTrayView( view.tabLayout, + adapter, interactor = TabTrayFragmentInteractor( DefaultTabTrayController( activity = (activity as HomeActivity), navController = findNavController(), dismissTabTray = ::dismissAllowingStateLoss, - showUndoSnackbar = ::showUndoSnackbar, - registerCollectionStorageObserver = ::registerCollectionStorageObserver + dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome, + registerCollectionStorageObserver = ::registerCollectionStorageObserver, + tabTrayDialogFragmentStore = tabTrayDialogStore, + selectTabUseCase = selectTabUseCase, + showChooseCollectionDialog = ::showChooseCollectionDialog, + showAddNewCollectionDialog = ::showAddNewCollectionDialog ) ), isPrivate = isPrivate, @@ -145,7 +198,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment() { tabsFeature.set( TabsFeature( - tabTrayView.view.tabsTray, + adapter, view.context.components.core.store, selectTabUseCase, removeTabUseCase, @@ -176,8 +229,11 @@ class TabTrayDialogFragment : AppCompatDialogFragment() { } consumeFrom(requireComponents.core.store) { + tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.BrowserStateChanged(it)) + } + + consumeFrom(tabTrayDialogStore) { tabTrayView.updateState(it) - navigateHomeIfNeeded(it) } } @@ -191,11 +247,21 @@ class TabTrayDialogFragment : AppCompatDialogFragment() { private fun showUndoSnackbarForTab(sessionId: String) { val sessionManager = view?.context?.components?.core?.sessionManager + val snapshot = sessionManager ?.findSessionById(sessionId)?.let { sessionManager.createSessionSnapshot(it) } ?: return + // Check if this is the last tab of this session type + val isLastOpenTab = + sessionManager.sessions.filter { snapshot.session.private == it.private }.size == 1 + + if (isLastOpenTab) { + dismissTabTrayAndNavigateHome(sessionId) + return + } + val state = snapshot.engineSession?.saveState() val isSelected = sessionId == requireComponents.core.store.state.selectedTabId ?: false @@ -205,13 +271,8 @@ class TabTrayDialogFragment : AppCompatDialogFragment() { getString(R.string.snackbar_tab_closed) } - // Check if this is the last tab of this session type - val isLastOpenTab = sessionManager.sessions.filter { snapshot.session.private }.size == 1 - val rootView = if (isLastOpenTab) { requireActivity().getRootView()!! } else { requireView().tabLayout } - val anchorView = if (isLastOpenTab) { null } else { snackbarAnchor } - - requireActivity().lifecycleScope.allowUndo( - rootView, + lifecycleScope.allowUndo( + requireView().tabLayout, snackbarMessage, getString(R.string.snackbar_deleted_undo), { @@ -220,18 +281,14 @@ class TabTrayDialogFragment : AppCompatDialogFragment() { }, operation = { }, elevation = ELEVATION, - paddedForBottomToolbar = isLastOpenTab, - anchorView = anchorView + anchorView = snackbarAnchor ) - - dismissTabTrayIfNecessary() } - private fun dismissTabTrayIfNecessary() { - if (requireComponents.core.sessionManager.sessions.size == 1) { - findNavController().popBackStack(R.id.homeFragment, false) - dismissAllowingStateLoss() - } + private fun dismissTabTrayAndNavigateHome(sessionId: String) { + val directions = BrowserFragmentDirections.actionGlobalHome(sessionToDelete = sessionId) + findNavController().navigate(directions) + dismissAllowingStateLoss() } override fun onDestroyView() { @@ -247,39 +304,10 @@ class TabTrayDialogFragment : AppCompatDialogFragment() { } } - private fun navigateHomeIfNeeded(state: BrowserState) { - val shouldPop = if (tabTrayView.isPrivateModeSelected) { - state.privateTabs.isEmpty() - } else { - state.normalTabs.isEmpty() - } - - if (shouldPop) { - findNavController().popBackStack(R.id.homeFragment, false) - } - } - private fun registerCollectionStorageObserver() { requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this) } - private fun showUndoSnackbar(snackbarMessage: String, snapshot: SessionManager.Snapshot) { - // Warning: removing this definition and using it directly in the onCancel block will fail silently. - val sessionManager = view?.context?.components?.core?.sessionManager - - requireActivity().lifecycleScope.allowUndo( - requireActivity().getRootView()!!, - snackbarMessage, - getString(R.string.snackbar_deleted_undo), - { - sessionManager?.restore(snapshot) - }, - operation = { }, - elevation = ELEVATION, - paddedForBottomToolbar = true - ) - } - private fun showCollectionSnackbar(tabSize: Int, isNewCollection: Boolean = false) { view.let { val messageStringRes = when { @@ -313,21 +341,101 @@ class TabTrayDialogFragment : AppCompatDialogFragment() { } } - companion object { - private const val ELEVATION = 80f - private const val FRAGMENT_TAG = "tabTrayDialogFragment" + override fun onBackPressed(): Boolean { + if (!tabTrayView.onBackPressed()) { + dismiss() + } + return true + } - fun show(fragmentManager: FragmentManager) { - // If we've killed the fragmentManager. Let's not try to show the tabs tray. - if (fragmentManager.isDestroyed) { - return - } + private fun showChooseCollectionDialog(sessionList: List) { + context?.let { + val tabCollectionStorage = it.components.core.tabCollectionStorage + val collections = + tabCollectionStorage.cachedTabCollections.map { it.title }.toTypedArray() + val customLayout = + LayoutInflater.from(it).inflate(R.layout.add_new_collection_dialog, null) + val list = customLayout.findViewById(R.id.recycler_view) + list.layoutManager = LinearLayoutManager(it) - // We want to make sure we don't accidentally show the dialog twice if - // a user somehow manages to trigger `show()` twice before we present the dialog. - if (fragmentManager.findFragmentByTag(FRAGMENT_TAG) == null) { - TabTrayDialogFragment().showNow(fragmentManager, FRAGMENT_TAG) - } + val builder = AlertDialog.Builder(it).setTitle(R.string.tab_tray_select_collection) + .setView(customLayout) + .setPositiveButton(android.R.string.ok) { dialog, _ -> + val selectedCollection = + (list.adapter as CollectionsAdapter).getSelectedCollection() + val collection = tabCollectionStorage.cachedTabCollections[selectedCollection] + viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { + tabCollectionStorage.addTabsToCollection(collection, sessionList) + it.metrics.track( + Event.CollectionTabsAdded( + it.components.core.sessionManager.normalSessionSize(), + sessionList.size + ) + ) + launch(Main) { + tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode) + dialog.dismiss() + } + } + }.setNegativeButton(android.R.string.cancel) { dialog, _ -> + tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode) + dialog.cancel() + } + + val dialog = builder.create() + val adapter = + CollectionsAdapter(arrayOf(it.getString(R.string.tab_tray_add_new_collection)) + collections) { + dialog.dismiss() + showAddNewCollectionDialog(sessionList) + } + list.adapter = adapter + dialog.show() } } + + private fun showAddNewCollectionDialog(sessionList: List) { + context?.let { + val tabCollectionStorage = it.components.core.tabCollectionStorage + val customLayout = + LayoutInflater.from(it).inflate(R.layout.name_collection_dialog, null) + val collectionNameEditText: EditText = + customLayout.findViewById(R.id.collection_name) + collectionNameEditText.setText( + it.getString( + R.string.create_collection_default_name, + tabCollectionStorage.cachedTabCollections.getDefaultCollectionNumber() + ) + ) + + AlertDialog.Builder(it).setTitle(R.string.tab_tray_add_new_collection) + .setView(customLayout).setPositiveButton(android.R.string.ok) { dialog, _ -> + viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { + tabCollectionStorage.createCollection( + collectionNameEditText.text.toString(), + sessionList + ) + it.metrics.track( + Event.CollectionSaved( + it.components.core.sessionManager.normalSessionSize(), + sessionList.size + ) + ) + launch(Main) { + tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode) + dialog.dismiss() + } + } + }.setNegativeButton(android.R.string.cancel) { dialog, _ -> + tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode) + dialog.cancel() + }.create().show().also { + collectionNameEditText.setSelection(0, collectionNameEditText.text.length) + collectionNameEditText.showKeyboard() + } + } + } + + companion object { + private const val ELEVATION = 80f + } } diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragmentStore.kt new file mode 100644 index 000000000..3efb32ea4 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragmentStore.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.tabtray + +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.concept.tabstray.Tab +import mozilla.components.lib.state.Action +import mozilla.components.lib.state.State +import mozilla.components.lib.state.Store + +/** + * The [Store] for holding the [TabTrayDialogFragmentState] and + * applying [TabTrayDialogFragmentAction]s. + */ +class TabTrayDialogFragmentStore(initialState: TabTrayDialogFragmentState) : + Store( + initialState, + ::tabTrayStateReducer + ) + +/** + * Actions to dispatch through the `TabTrayDialogFragmentStore` to modify + * `TabTrayDialogFragmentState` through the reducer. + */ +sealed class TabTrayDialogFragmentAction : Action { + data class BrowserStateChanged(val browserState: BrowserState) : TabTrayDialogFragmentAction() + object EnterMultiSelectMode : TabTrayDialogFragmentAction() + object ExitMultiSelectMode : TabTrayDialogFragmentAction() + data class AddItemForCollection(val item: Tab) : TabTrayDialogFragmentAction() + data class RemoveItemForCollection(val item: Tab) : TabTrayDialogFragmentAction() +} + +/** + * The state for the Tab Tray Dialog Screen + * @property mode Current Mode of Multiselection + */ +data class TabTrayDialogFragmentState(val browserState: BrowserState, val mode: Mode) : State { + sealed class Mode { + open val selectedItems = emptySet() + + object Normal : Mode() + data class MultiSelect(override val selectedItems: Set) : Mode() + } +} + +/** + * The TabTrayDialogFragmentState Reducer. + */ +private fun tabTrayStateReducer( + state: TabTrayDialogFragmentState, + action: TabTrayDialogFragmentAction +): TabTrayDialogFragmentState { + return when (action) { + is TabTrayDialogFragmentAction.BrowserStateChanged -> state.copy(browserState = action.browserState) + is TabTrayDialogFragmentAction.AddItemForCollection -> + state.copy(mode = TabTrayDialogFragmentState.Mode.MultiSelect(state.mode.selectedItems + action.item)) + is TabTrayDialogFragmentAction.RemoveItemForCollection -> { + val selected = state.mode.selectedItems - action.item + state.copy( + mode = if (selected.isEmpty()) { + TabTrayDialogFragmentState.Mode.Normal + } else { + TabTrayDialogFragmentState.Mode.MultiSelect(selected) + } + ) + } + is TabTrayDialogFragmentAction.ExitMultiSelectMode -> state.copy(mode = TabTrayDialogFragmentState.Mode.Normal) + is TabTrayDialogFragmentAction.EnterMultiSelectMode -> state.copy( + mode = TabTrayDialogFragmentState.Mode.MultiSelect( + setOf() + ) + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt index 374292c8e..33bef403a 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt @@ -4,17 +4,70 @@ package org.mozilla.fenix.tabtray +import mozilla.components.concept.tabstray.Tab + +@Suppress("TooManyFunctions") interface TabTrayInteractor { + /** + * Called when user clicks the new tab button. + */ fun onNewTabTapped(private: Boolean) + + /** + * Called when tab tray should be dismissed. + */ fun onTabTrayDismissed() + + /** + * Called when user clicks the share tabs button. + */ fun onShareTabsClicked(private: Boolean) - fun onSaveToCollectionClicked() + + /** + * Called when user clicks button to save selected tabs to a collection. + */ + fun onSaveToCollectionClicked(selectedTabs: Set) + + /** + * Called when user clicks the close all tabs button. + */ fun onCloseAllTabsClicked(private: Boolean) + + /** + * Called when the physical back button is clicked. + */ + fun onBackPressed(): Boolean + + /** + * Called when a requester needs to know the current mode of the tab tray. + */ + fun onModeRequested(): TabTrayDialogFragmentState.Mode + + /** + * Called when a tab should be opened in the browser. + */ + fun onOpenTab(tab: Tab) + + /** + * Called when a tab should be selected in multiselect mode. + */ + fun onAddSelectedTab(tab: Tab) + + /** + * Called when a tab should be unselected in multiselect mode. + */ + fun onRemoveSelectedTab(tab: Tab) + + /** + * Called when multiselect mode should be entered with no tabs selected. + */ + fun onEnterMultiselect() } /** * Interactor for the tab tray fragment. */ +@Suppress("TooManyFunctions") class TabTrayFragmentInteractor(private val controller: TabTrayController) : TabTrayInteractor { override fun onNewTabTapped(private: Boolean) { controller.onNewTabTapped(private) @@ -28,11 +81,35 @@ class TabTrayFragmentInteractor(private val controller: TabTrayController) : Tab controller.onShareTabsClicked(private) } - override fun onSaveToCollectionClicked() { - controller.onSaveToCollectionClicked() + override fun onSaveToCollectionClicked(selectedTabs: Set) { + controller.onSaveToCollectionClicked(selectedTabs) } override fun onCloseAllTabsClicked(private: Boolean) { controller.onCloseAllTabsClicked(private) } + + override fun onBackPressed(): Boolean { + return controller.handleBackPressed() + } + + override fun onModeRequested(): TabTrayDialogFragmentState.Mode { + return controller.onModeRequested() + } + + override fun onAddSelectedTab(tab: Tab) { + controller.handleAddSelectedTab(tab) + } + + override fun onRemoveSelectedTab(tab: Tab) { + controller.handleRemoveSelectedTab(tab) + } + + override fun onOpenTab(tab: Tab) { + controller.handleOpenTab(tab) + } + + override fun onEnterMultiselect() { + controller.handleEnterMultiselect() + } } diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt index a5e87a297..b580a239f 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt @@ -9,14 +9,18 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent +import androidx.annotation.IdRes import androidx.cardview.widget.CardView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet import androidx.core.content.ContextCompat import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams import androidx.lifecycle.LifecycleCoroutineScope +import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.tabs.TabLayout import kotlinx.android.extensions.LayoutContainer -import kotlinx.android.synthetic.main.component_tabstray.* import kotlinx.android.synthetic.main.component_tabstray.view.* import kotlinx.android.synthetic.main.component_tabstray_fab.view.* import kotlinx.android.synthetic.main.tabs_tray_tab_counter.* @@ -29,7 +33,7 @@ import mozilla.components.browser.menu.item.SimpleBrowserMenuItem 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.tabstray.BrowserTabsTray +import mozilla.components.support.ktx.android.util.dpToPx import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.components @@ -41,6 +45,7 @@ import org.mozilla.fenix.ext.settings @Suppress("LongParameterList", "TooManyFunctions", "LargeClass") class TabTrayView( private val container: ViewGroup, + private val tabsAdapter: FenixTabsAdapter, private val interactor: TabTrayInteractor, isPrivate: Boolean, startingInLandscape: Boolean, @@ -50,16 +55,20 @@ class TabTrayView( val fabView = LayoutInflater.from(container.context) .inflate(R.layout.component_tabstray_fab, container, true) + private val hasAccessibilityEnabled = container.context.settings().accessibilityServicesEnabled + val view = LayoutInflater.from(container.context) .inflate(R.layout.component_tabstray, container, true) - val isPrivateModeSelected: Boolean get() = view.tab_layout.selectedTabPosition == PRIVATE_TAB_ID + private val isPrivateModeSelected: Boolean get() = view.tab_layout.selectedTabPosition == PRIVATE_TAB_ID private val behavior = BottomSheetBehavior.from(view.tab_wrapper) private val tabTrayItemMenu: TabTrayItemMenu private var menu: BrowserMenu? = null + private var tabsTouchHelper: TabsTouchHelper + private var hasLoaded = false override val containerView: View? @@ -68,8 +77,6 @@ class TabTrayView( init { container.context.components.analytics.metrics.track(Event.TabsTrayOpened) - val hasAccessibilityEnabled = view.context.settings().accessibilityServicesEnabled - toggleFabText(isPrivate) behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { @@ -118,27 +125,34 @@ class TabTrayView( setTopOffset(startingInLandscape) - (view.tabsTray as? BrowserTabsTray)?.also { tray -> - TabsTouchHelper(tray.tabsAdapter).attachToRecyclerView(tray) - (tray.tabsAdapter as? FenixTabsAdapter)?.also { adapter -> - adapter.onTabsUpdated = { - if (hasAccessibilityEnabled) { - adapter.notifyDataSetChanged() - } - if (!hasLoaded) { - hasLoaded = true - scrollToTab(view.context.components.core.store.state.selectedTabId) - if (view.context.settings().accessibilityServicesEnabled) { - lifecycleScope.launch { - delay(SELECTION_DELAY.toLong()) - lifecycleScope.launch(Main) { - tray.layoutManager?.findViewByPosition(selectedBrowserTabIndex) - ?.requestFocus() - tray.layoutManager?.findViewByPosition(selectedBrowserTabIndex) - ?.sendAccessibilityEvent( - AccessibilityEvent.TYPE_VIEW_FOCUSED - ) - } + view.tabsTray.apply { + layoutManager = LinearLayoutManager(container.context).apply { + reverseLayout = true + stackFromEnd = true + } + adapter = tabsAdapter + + tabsTouchHelper = TabsTouchHelper(tabsAdapter) + tabsTouchHelper.attachToRecyclerView(this) + + tabsAdapter.tabTrayInteractor = interactor + tabsAdapter.onTabsUpdated = { + if (hasAccessibilityEnabled) { + tabsAdapter.notifyDataSetChanged() + } + if (!hasLoaded) { + hasLoaded = true + scrollToTab(view.context.components.core.store.state.selectedTabId) + if (view.context.settings().accessibilityServicesEnabled) { + lifecycleScope.launch { + delay(SELECTION_DELAY.toLong()) + lifecycleScope.launch(Main) { + layoutManager?.findViewByPosition(selectedBrowserTabIndex) + ?.requestFocus() + layoutManager?.findViewByPosition(selectedBrowserTabIndex) + ?.sendAccessibilityEvent( + AccessibilityEvent.TYPE_VIEW_FOCUSED + ) } } } @@ -152,7 +166,7 @@ class TabTrayView( is TabTrayItemMenu.Item.ShareAllTabs -> interactor.onShareTabsClicked( isPrivateModeSelected ) - is TabTrayItemMenu.Item.SaveToCollection -> interactor.onSaveToCollectionClicked() + is TabTrayItemMenu.Item.SaveToCollection -> interactor.onEnterMultiselect() is TabTrayItemMenu.Item.CloseAllTabs -> interactor.onCloseAllTabsClicked( isPrivateModeSelected ) @@ -173,6 +187,10 @@ class TabTrayView( } } + adjustNewTabButtonsForNormalMode() + } + + private fun adjustNewTabButtonsForNormalMode() { view.tab_tray_new_tab.apply { isVisible = hasAccessibilityEnabled setOnClickListener { @@ -208,7 +226,7 @@ class TabTrayView( toggleFabText(isPrivateModeSelected) filterTabs.invoke(isPrivateModeSelected) - updateState(view.context.components.core.store.state) + updateUINormalMode(view.context.components.core.store.state) scrollToTab(view.context.components.core.store.state.selectedTabId) if (isPrivateModeSelected) { @@ -224,32 +242,168 @@ class TabTrayView( override fun onTabUnselected(tab: TabLayout.Tab?) { /*noop*/ } - fun updateState(state: BrowserState) { - view.let { - val hasNoTabs = if (isPrivateModeSelected) { - state.privateTabs.isEmpty() - } else { - state.normalTabs.isEmpty() - } + var mode: TabTrayDialogFragmentState.Mode = TabTrayDialogFragmentState.Mode.Normal + private set - view.tab_tray_empty_view.isVisible = hasNoTabs - if (hasNoTabs) { - view.tab_tray_empty_view.text = if (isPrivateModeSelected) { - view.context.getString(R.string.no_private_tabs_description) - } else { - view.context?.getString(R.string.no_open_tabs_description) + fun updateState(state: TabTrayDialogFragmentState) { + val oldMode = mode + + if (oldMode::class != state.mode::class && view.context.settings().accessibilityServicesEnabled) { + view.announceForAccessibility( + if (state.mode == TabTrayDialogFragmentState.Mode.Normal) view.context.getString( + R.string.tab_tray_exit_multiselect_content_description + ) else view.context.getString(R.string.tab_tray_enter_multiselect_content_description) + ) + } + + mode = state.mode + when (state.mode) { + TabTrayDialogFragmentState.Mode.Normal -> { + view.tabsTray.apply { + tabsTouchHelper.attachToRecyclerView(this) + } + + toggleUIMultiselect(multiselect = false) + + updateUINormalMode(state.browserState) + } + is TabTrayDialogFragmentState.Mode.MultiSelect -> { + // Disable swipe to delete while in multiselect + tabsTouchHelper.attachToRecyclerView(null) + + toggleUIMultiselect(multiselect = true) + + fabView.new_tab_button.isVisible = false + view.tab_tray_new_tab.isVisible = false + view.collect_multi_select.isVisible = state.mode.selectedItems.size > 0 + + view.multiselect_title.text = view.context.getString( + R.string.tab_tray_multi_select_title, + state.mode.selectedItems.size + ) + view.collect_multi_select.setOnClickListener { + interactor.onSaveToCollectionClicked(state.mode.selectedItems) + } + view.exit_multi_select.setOnClickListener { + interactor.onBackPressed() } } + } - view.tabsTray.asView().visibility = if (hasNoTabs) { - View.INVISIBLE - } else { - View.VISIBLE + if (oldMode.selectedItems != state.mode.selectedItems) { + val unselectedItems = oldMode.selectedItems - state.mode.selectedItems + + state.mode.selectedItems.union(unselectedItems).forEach { item -> + if (view.context.settings().accessibilityServicesEnabled) { + view.announceForAccessibility( + if (unselectedItems.contains(item)) view.context.getString( + R.string.tab_tray_item_unselected_multiselect_content_description, + item.title + ) else view.context.getString( + R.string.tab_tray_item_selected_multiselect_content_description, + item.title + ) + ) + } + updateTabsForSelectionChanged(item.id) } - view.tab_tray_overflow.isVisible = !hasNoTabs + } + } - counter_text.text = "${state.normalTabs.size}" - updateTabCounterContentDescription(state.normalTabs.size) + private fun ConstraintLayout.setChildWPercent(percentage: Float, @IdRes childId: Int) { + this.findViewById(childId)?.let { + val constraintSet = ConstraintSet() + constraintSet.clone(this) + constraintSet.constrainPercentWidth(it.id, percentage) + constraintSet.applyTo(this) + it.requestLayout() + } + } + + private fun updateUINormalMode(browserState: BrowserState) { + val hasNoTabs = if (isPrivateModeSelected) { + browserState.privateTabs.isEmpty() + } else { + browserState.normalTabs.isEmpty() + } + + view.tab_tray_empty_view.isVisible = hasNoTabs + if (hasNoTabs) { + view.tab_tray_empty_view.text = if (isPrivateModeSelected) { + view.context.getString(R.string.no_private_tabs_description) + } else { + view.context?.getString(R.string.no_open_tabs_description) + } + } + + view.tabsTray.visibility = if (hasNoTabs) { + View.INVISIBLE + } else { + View.VISIBLE + } + view.tab_tray_overflow.isVisible = !hasNoTabs + + counter_text.text = "${browserState.normalTabs.size}" + updateTabCounterContentDescription(browserState.normalTabs.size) + + adjustNewTabButtonsForNormalMode() + } + + private fun toggleUIMultiselect(multiselect: Boolean) { + view.multiselect_title.isVisible = multiselect + view.collect_multi_select.isVisible = multiselect + view.exit_multi_select.isVisible = multiselect + + view.topBar.setBackgroundColor( + ContextCompat.getColor( + view.context, + if (multiselect) R.color.accent_normal_theme else R.color.foundation_normal_theme + ) + ) + + val displayMetrics = view.context.resources.displayMetrics + + view.handle.updateLayoutParams { + height = + if (multiselect) MULTISELECT_HANDLE_HEIGHT.dpToPx(displayMetrics) else NORMAL_HANDLE_HEIGHT.dpToPx( + displayMetrics + ) + topMargin = if (multiselect) 0.dpToPx(displayMetrics) else NORMAL_TOP_MARGIN.dpToPx( + displayMetrics + ) + } + + view.tab_wrapper.setChildWPercent( + if (multiselect) 1F else NORMAL_HANDLE_PERCENT_WIDTH, + view.handle.id + ) + + view.handle.setBackgroundColor( + ContextCompat.getColor( + view.context, + if (multiselect) R.color.accent_normal_theme else R.color.secondary_text_normal_theme + ) + ) + + view.tab_layout.isVisible = !multiselect + view.tab_tray_empty_view.isVisible = !multiselect + view.tab_tray_overflow.isVisible = !multiselect + view.tab_layout.isVisible = !multiselect + } + + private fun updateTabsForSelectionChanged(itemId: String) { + view.tabsTray.apply { + val tabs = if (isPrivateModeSelected) { + view.context.components.core.store.state.privateTabs + } else { + view.context.components.core.store.state.normalTabs + } + + val selectedBrowserTabIndex = tabs.indexOfFirst { it.id == itemId } + + this.adapter?.notifyItemChanged( + selectedBrowserTabIndex, true + ) } } @@ -287,8 +441,12 @@ class TabTrayView( } } + fun onBackPressed(): Boolean { + return interactor.onBackPressed() + } + fun scrollToTab(sessionId: String?) { - (view.tabsTray as? BrowserTabsTray)?.also { tray -> + view.tabsTray.apply { val tabs = if (isPrivateModeSelected) { view.context.components.core.store.state.privateTabs } else { @@ -298,7 +456,7 @@ class TabTrayView( val selectedBrowserTabIndex = tabs .indexOfFirst { it.id == sessionId } - tray.layoutManager?.scrollToPosition(selectedBrowserTabIndex) + layoutManager?.scrollToPosition(selectedBrowserTabIndex) } } @@ -308,6 +466,10 @@ class TabTrayView( private const val EXPAND_AT_SIZE = 3 private const val SLIDE_OFFSET = 0 private const val SELECTION_DELAY = 500 + private const val MULTISELECT_HANDLE_HEIGHT = 11 + private const val NORMAL_HANDLE_HEIGHT = 3 + private const val NORMAL_TOP_MARGIN = 8 + private const val NORMAL_HANDLE_PERCENT_WIDTH = 0.1F } } diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt index a94b08edd..106041472 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt @@ -10,10 +10,13 @@ import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView import androidx.annotation.VisibleForTesting +import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.widget.AppCompatImageButton import androidx.core.content.ContextCompat import mozilla.components.browser.state.state.MediaState +import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.tabstray.TabViewHolder +import mozilla.components.browser.tabstray.TabsTrayStyling import mozilla.components.browser.tabstray.thumbnail.TabThumbnailView import mozilla.components.browser.toolbar.MAX_URI_LENGTH import mozilla.components.concept.tabstray.Tab @@ -26,12 +29,14 @@ import mozilla.components.support.images.loader.ImageLoader import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.getMediaStateForSession import org.mozilla.fenix.ext.increaseTapArea import org.mozilla.fenix.ext.removeAndDisable import org.mozilla.fenix.ext.removeTouchDelegate import org.mozilla.fenix.ext.showAndEnable -import org.mozilla.fenix.ext.toTab +import org.mozilla.fenix.utils.Do import kotlin.math.max /** @@ -40,8 +45,10 @@ import kotlin.math.max class TabTrayViewHolder( itemView: View, private val imageLoader: ImageLoader, - val getSelectedTabId: () -> String? = { itemView.context.components.core.store.state.selectedTabId } + private val store: BrowserStore = itemView.context.components.core.store, + private val metrics: MetricController = itemView.context.components.analytics.metrics ) : TabViewHolder(itemView) { + private val titleView: TextView = itemView.findViewById(R.id.mozac_browser_tabstray_title) private val closeView: AppCompatImageButton = itemView.findViewById(R.id.mozac_browser_tabstray_close) @@ -57,10 +64,12 @@ class TabTrayViewHolder( /** * Displays the data of the given session and notifies the given observable about events. */ - override fun bind(tab: Tab, isSelected: Boolean, observable: Observable) { - // This is a hack to workaround a bug in a-c. - // https://github.com/mozilla-mobile/android-components/issues/7186 - val isSelected2 = tab.id == getSelectedTabId() + override fun bind( + tab: Tab, + isSelected: Boolean, + styling: TabsTrayStyling, + observable: Observable + ) { this.tab = tab // Basic text @@ -69,7 +78,7 @@ class TabTrayViewHolder( updateCloseButtonDescription(tab.title) // Drawables and theme - updateBackgroundColor(isSelected2) + updateBackgroundColor(isSelected) if (tab.thumbnail != null) { thumbnailView.setImageBitmap(tab.thumbnail) @@ -79,16 +88,15 @@ class TabTrayViewHolder( // Media state playPauseButtonView.increaseTapArea(PLAY_PAUSE_BUTTON_EXTRA_DPS) - val session = itemView.context?.components?.core?.sessionManager?.findSessionById(tab.id) with(playPauseButtonView) { invalidate() - when (session?.toTab(itemView.context)?.mediaState) { + Do exhaustive when (store.state.getMediaStateForSession(tab.id)) { MediaState.State.PAUSED -> { showAndEnable() contentDescription = context.getString(R.string.mozac_feature_media_notification_action_play) setImageDrawable( - androidx.appcompat.content.res.AppCompatResources.getDrawable( + AppCompatResources.getDrawable( context, R.drawable.tab_tray_play_with_background ) @@ -100,7 +108,7 @@ class TabTrayViewHolder( contentDescription = context.getString(R.string.mozac_feature_media_notification_action_pause) setImageDrawable( - androidx.appcompat.content.res.AppCompatResources.getDrawable( + AppCompatResources.getDrawable( context, R.drawable.tab_tray_pause_with_background ) @@ -115,16 +123,15 @@ class TabTrayViewHolder( } playPauseButtonView.setOnClickListener { - val mState = session?.toTab(itemView.context)?.mediaState - when (mState) { + Do exhaustive when (store.state.getMediaStateForSession(tab.id)) { MediaState.State.PLAYING -> { - itemView.context.components.analytics.metrics.track(Event.TabMediaPause) - itemView.context.components.core.store.state.media.pauseIfPlaying() + metrics.track(Event.TabMediaPause) + store.state.media.pauseIfPlaying() } MediaState.State.PAUSED -> { - itemView.context.components.analytics.metrics.track(Event.TabMediaPlay) - itemView.context.components.core.store.state.media.playIfPaused() + metrics.track(Event.TabMediaPlay) + store.state.media.playIfPaused() } MediaState.State.NONE -> throw AssertionError( @@ -133,10 +140,6 @@ class TabTrayViewHolder( } } - itemView.setOnClickListener { - observable.notifyObservers { onTabSelected(tab) } - } - closeView.setOnClickListener { observable.notifyObservers { onTabClosed(tab) } } @@ -189,7 +192,7 @@ class TabTrayViewHolder( } internal fun updateAccessibilityRowIndex(item: View, newIndex: Int) { - item.setAccessibilityDelegate(object : View.AccessibilityDelegate() { + item.accessibilityDelegate = object : View.AccessibilityDelegate() { override fun onInitializeAccessibilityNodeInfo( host: View?, info: AccessibilityNodeInfo? @@ -208,7 +211,7 @@ class TabTrayViewHolder( } } } - }) + } } companion object { diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlay.kt b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlay.kt index 91916c768..8ed15bf03 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlay.kt +++ b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlay.kt @@ -13,7 +13,6 @@ import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.widget.ImageView -import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.marginTop import kotlinx.android.synthetic.main.tracking_protection_onboarding_popup.* @@ -22,6 +21,7 @@ import mozilla.components.browser.session.Session import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController +import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.ext.increaseTapArea import org.mozilla.fenix.utils.Settings @@ -63,10 +63,10 @@ class TrackingProtectionOverlay( val layout = LayoutInflater.from(context) .inflate(R.layout.tracking_protection_onboarding_popup, null) - val isBottomToolbar = settings.shouldUseBottomToolbar + val toolbarPosition = settings.toolbarPosition - layout.drop_down_triangle.isGone = isBottomToolbar - layout.pop_up_triangle.isVisible = isBottomToolbar + layout.drop_down_triangle.isVisible = toolbarPosition == ToolbarPosition.TOP + layout.pop_up_triangle.isVisible = toolbarPosition == ToolbarPosition.BOTTOM layout.onboarding_message.text = context.getString( @@ -91,11 +91,7 @@ class TrackingProtectionOverlay( val xOffset = triangleMarginStartPx + triangleWidthPx / 2 - val gravity = if (isBottomToolbar) { - Gravity.START or Gravity.BOTTOM - } else { - Gravity.START or Gravity.TOP - } + val gravity = Gravity.START or toolbarPosition.androidGravity trackingOnboardingDialog.apply { setContentView(layout) diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionStore.kt b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionStore.kt index a9d79fd18..f83a18532 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionStore.kt +++ b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionStore.kt @@ -1,6 +1,6 @@ /* 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/. */ + * 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 diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsFragmentStore.kt index 6d51f98a3..1c2f5e982 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsFragmentStore.kt @@ -1,6 +1,6 @@ /* 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/. */ + * 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.trackingprotectionexceptions diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsInteractor.kt b/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsInteractor.kt index e609cd003..79694ac1c 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/trackingprotectionexceptions/ExceptionsInteractor.kt @@ -1,6 +1,6 @@ /* 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/. */ + * 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.trackingprotectionexceptions diff --git a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt index cb88d7579..180b022a2 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -29,6 +29,7 @@ import org.mozilla.fenix.Config import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.components.metrics.MozillaProductDetector +import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.getPreferenceKey import org.mozilla.fenix.settings.PhoneFeature @@ -463,6 +464,9 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = !touchExplorationIsEnabled && !switchServiceIsEnabled ) + val toolbarPosition: ToolbarPosition + get() = if (shouldUseBottomToolbar) ToolbarPosition.BOTTOM else ToolbarPosition.TOP + /** * Check each active accessibility service to see if it can perform gestures, if any can, * then it is *likely* a switch service is enabled. We are assuming this to be the case based on #7486 @@ -827,12 +831,10 @@ class Settings(private val appContext: Context) : PreferencesHolder { get() { return when (savedLoginsSortingStrategyString) { SavedLoginsFragment.SORTING_STRATEGY_ALPHABETICALLY -> SortingStrategy.Alphabetically( - appContext + appContext.components.publicSuffixList ) - SavedLoginsFragment.SORTING_STRATEGY_LAST_USED -> SortingStrategy.LastUsed( - appContext - ) - else -> SortingStrategy.Alphabetically(appContext) + SavedLoginsFragment.SORTING_STRATEGY_LAST_USED -> SortingStrategy.LastUsed + else -> SortingStrategy.Alphabetically(appContext.components.publicSuffixList) } } set(value) { diff --git a/app/src/main/java/org/mozilla/fenix/utils/ToolbarPopupWindow.kt b/app/src/main/java/org/mozilla/fenix/utils/ToolbarPopupWindow.kt new file mode 100644 index 000000000..9e7b41204 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/utils/ToolbarPopupWindow.kt @@ -0,0 +1,108 @@ +/* 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.utils + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.widget.LinearLayout +import android.widget.PopupWindow +import androidx.annotation.VisibleForTesting +import androidx.core.view.isVisible +import com.google.android.material.snackbar.Snackbar +import kotlinx.android.synthetic.main.browser_toolbar_popup_window.view.* +import mozilla.components.browser.session.Session +import mozilla.components.browser.state.selector.selectedTab +import mozilla.components.browser.state.store.BrowserStore +import org.mozilla.fenix.R +import org.mozilla.fenix.components.FenixSnackbar +import org.mozilla.fenix.ext.components +import java.lang.ref.WeakReference + +object ToolbarPopupWindow { + fun show( + view: WeakReference, + customTabSession: Session? = null, + handlePasteAndGo: (String) -> Unit, + handlePaste: (String) -> Unit, + copyVisible: Boolean = true + ) { + val context = view.get()?.context ?: return + val isCustomTabSession = customTabSession != null + val clipboard = context.components.clipboardHandler + val customView = LayoutInflater.from(context) + .inflate(R.layout.browser_toolbar_popup_window, null) + val popupWindow = PopupWindow( + customView, + LinearLayout.LayoutParams.WRAP_CONTENT, + context.resources.getDimensionPixelSize(R.dimen.context_menu_height), + true + ) + popupWindow.elevation = + context.resources.getDimension(R.dimen.mozac_browser_menu_elevation) + + // This is a workaround for SDK<23 to allow popup dismissal on outside or back button press + // See: https://github.com/mozilla-mobile/fenix/issues/10027 + popupWindow.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + + customView.copy.isVisible = copyVisible + + customView.paste.isVisible = !clipboard.text.isNullOrEmpty() && !isCustomTabSession + customView.paste_and_go.isVisible = + !clipboard.text.isNullOrEmpty() && !isCustomTabSession + + customView.copy.setOnClickListener { + popupWindow.dismiss() + clipboard.text = getUrlForClipboard( + it.context.components.core.store, + customTabSession + ) + + view.get()?.let { + FenixSnackbar.make( + view = it, + duration = Snackbar.LENGTH_SHORT, + isDisplayedWithBrowserToolbar = true + ) + .setText(context.getString(R.string.browser_toolbar_url_copied_to_clipboard_snackbar)) + .show() + } + } + + customView.paste.setOnClickListener { + popupWindow.dismiss() + handlePaste(clipboard.text!!) + } + + customView.paste_and_go.setOnClickListener { + popupWindow.dismiss() + handlePasteAndGo(clipboard.text!!) + } + + view.get()?.let { + popupWindow.showAsDropDown( + it, + context.resources.getDimensionPixelSize(R.dimen.context_menu_x_offset), + 0, + Gravity.START + ) + } + } + + @VisibleForTesting + internal fun getUrlForClipboard( + store: BrowserStore, + customTabSession: Session? = null + ): String? { + return if (customTabSession != null) { + customTabSession.url + } else { + val selectedTab = store.state.selectedTab + selectedTab?.readerState?.activeUrl ?: selectedTab?.content?.url + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/utils/view/ViewHolder.kt b/app/src/main/java/org/mozilla/fenix/utils/view/ViewHolder.kt index 29f610441..21a1eb638 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/view/ViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/view/ViewHolder.kt @@ -1,3 +1,7 @@ +/* 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.utils.view import android.view.View diff --git a/app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNew.kt b/app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNew.kt index d022cc19d..a536f593a 100644 --- a/app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNew.kt +++ b/app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNew.kt @@ -1,9 +1,9 @@ -package org.mozilla.fenix.whatsnew - /* 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.whatsnew + import android.content.Context import android.os.StrictMode import org.mozilla.fenix.ext.resetPoliciesAfter diff --git a/app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNewStorage.kt b/app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNewStorage.kt index 2c8182230..6e47cf589 100644 --- a/app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNewStorage.kt +++ b/app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNewStorage.kt @@ -1,9 +1,9 @@ -package org.mozilla.fenix.whatsnew - /* 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.whatsnew + import android.content.Context import android.content.SharedPreferences import androidx.preference.PreferenceManager diff --git a/app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNewVersion.kt b/app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNewVersion.kt index 502a2f333..20180f2bb 100644 --- a/app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNewVersion.kt +++ b/app/src/main/java/org/mozilla/fenix/whatsnew/WhatsNewVersion.kt @@ -1,8 +1,9 @@ -package org.mozilla.fenix.whatsnew /* 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.whatsnew + import android.content.Context import mozilla.components.support.ktx.android.content.appVersionName diff --git a/app/src/main/res/layout/add_new_collection_dialog.xml b/app/src/main/res/layout/add_new_collection_dialog.xml new file mode 100644 index 000000000..99054c9f4 --- /dev/null +++ b/app/src/main/res/layout/add_new_collection_dialog.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/collection_dialog_list_item.xml b/app/src/main/res/layout/collection_dialog_list_item.xml new file mode 100644 index 000000000..7a2dc36e7 --- /dev/null +++ b/app/src/main/res/layout/collection_dialog_list_item.xml @@ -0,0 +1,20 @@ + + + diff --git a/app/src/main/res/layout/component_tabstray.xml b/app/src/main/res/layout/component_tabstray.xml index 51a1edc62..3dd4f2ae0 100644 --- a/app/src/main/res/layout/component_tabstray.xml +++ b/app/src/main/res/layout/component_tabstray.xml @@ -2,15 +2,14 @@ - - @@ -20,81 +19,141 @@ android:layout_height="3dp" android:layout_marginTop="8dp" android:background="@color/secondary_text_normal_theme" - app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintWidth_percent="0.1" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintWidth_percent="0.1" /> + app:layout_constraintTop_toBottomOf="@id/topBar" /> - + app:layout_constraintTop_toBottomOf="@+id/handle"> - + + + android:layout_height="wrap_content" + android:layout_marginStart="12dp" + android:textColor="@color/contrast_text_normal_theme" + android:textSize="18sp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/collect_multi_select" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintStart_toEndOf="@+id/exit_multi_select" + app:layout_constraintTop_toTopOf="parent" + tools:text="3 selected" /> - + + + android:layout_height="80dp" + android:background="@color/foundation_normal_theme" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintWidth_percent="0.5" + app:tabGravity="fill" + app:tabIconTint="@color/tab_icon" + app:tabIndicatorColor="@color/accent_normal_theme" + app:tabRippleColor="@android:color/transparent"> - + - + - + + + + + + + app:layout_constraintTop_toBottomOf="@+id/topBar" /> - + app:layout_constraintTop_toBottomOf="@+id/divider" /> diff --git a/app/src/main/res/layout/custom_search_engine_radio_button.xml b/app/src/main/res/layout/custom_search_engine_radio_button.xml index 8ece2dbfe..d8e23521d 100644 --- a/app/src/main/res/layout/custom_search_engine_radio_button.xml +++ b/app/src/main/res/layout/custom_search_engine_radio_button.xml @@ -7,6 +7,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_height="@dimen/search_engine_radio_button_height" + android:minHeight="@dimen/radio_button_preference_height" android:layout_width="match_parent" android:background="?android:selectableItemBackground" android:clickable="true" diff --git a/app/src/main/res/layout/name_collection_dialog.xml b/app/src/main/res/layout/name_collection_dialog.xml new file mode 100644 index 000000000..f587cfb23 --- /dev/null +++ b/app/src/main/res/layout/name_collection_dialog.xml @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/app/src/main/res/layout/search_engine_radio_button.xml b/app/src/main/res/layout/search_engine_radio_button.xml index 015807ba2..74cf2c2ba 100644 --- a/app/src/main/res/layout/search_engine_radio_button.xml +++ b/app/src/main/res/layout/search_engine_radio_button.xml @@ -2,12 +2,12 @@ - - @@ -18,18 +18,14 @@ android:importantForAccessibility="no" android:textAppearance="?android:attr/textAppearanceListItem" android:layout_marginStart="@dimen/search_bar_search_engine_icon_padding" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintBottom_toBottomOf="parent"/> + android:layout_gravity="center" /> + android:layout_gravity="center" /> + tools:text="Google" + android:layout_weight="1" + android:layout_gravity="center" /> - + app:srcCompat="@drawable/ic_menu" /> + diff --git a/app/src/main/res/layout/search_suggestions_onboarding.xml b/app/src/main/res/layout/search_suggestions_onboarding.xml index a4cf42fd1..d957c2c13 100644 --- a/app/src/main/res/layout/search_suggestions_onboarding.xml +++ b/app/src/main/res/layout/search_suggestions_onboarding.xml @@ -50,12 +50,13 @@ android:id="@+id/learn_more" android:layout_width="0dp" android:layout_height="wrap_content" + android:minHeight="48dp" android:text="@string/exceptions_empty_message_learn_more_link" android:textAppearance="?android:attr/textAppearanceSmall" android:textColor="?attr/accentHighContrast" android:textStyle="bold" app:layout_constraintBottom_toTopOf="@id/allow" - app:layout_constraintEnd_toEndOf="@id/title" + app:layout_constraintEnd_toStartOf="@id/dismiss" app:layout_constraintStart_toStartOf="@id/title" app:layout_constraintTop_toBottomOf="@id/text" tools:textColor="@color/accent_high_contrast_private_theme"/> @@ -67,9 +68,9 @@ android:layout_height="wrap_content" android:padding="12dp" android:text="@string/search_suggestions_onboarding_allow_button" - app:layout_constraintBottom_toBottomOf="parent" + android:layout_marginTop="20dp" app:layout_constraintEnd_toEndOf="@id/title" - app:layout_constraintTop_toBottomOf="@id/learn_more" /> + app:layout_constraintTop_toBottomOf="@id/text" /> + android:clickable="true" + android:focusable="true" + android:foreground="?android:selectableItemBackground"> + android:layout_height="match_parent" + android:importantForAccessibility="no" + android:padding="22dp" + app:srcCompat="@drawable/mozac_ic_globe" + app:tint="?tabTrayThumbnailIcon" /> + + + + + app:srcCompat="@drawable/mozac_ic_close" + app:tint="@color/tab_tray_item_text_normal_theme" /> diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index d543ac084..c33f8f0f4 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -109,7 +109,12 @@ + tools:layout="@layout/fragment_tab_tray_dialog"> + + + tools:layout="@layout/fragment_synced_tabs" /> + tools:layout="@layout/fragment_exceptions" /> Dewis ffolder Ydych chi’n siŵr eich bod am ddileu’r ffolder yma? + + Bydd %s yn dileu’r eitemau hyn. Wedi dileu %1$s @@ -631,8 +633,10 @@ The first parameter is the host part of the URL of the bookmark deleted, if any --> Wedi dileu %1$s - + Wedi dileu Nodau Tudalen + + Yn dileu’r nodau tudalen hyn DADWNEUD @@ -725,6 +729,8 @@ %d tab wedi’i ddewis Tabiau wedi’u cadw! + + Casgliad wedi’i gadw! Tab wedi’i gadw! @@ -830,6 +836,10 @@ GWRTHOD Ydych chi’n siŵr eich bod am ddileu %1$s? + + Bydd dileu’r tab hwn yn dileu’r casgliad cyfan. Gallwch greu casgliadau newydd ar unrhyw adeg. + + Dileu %1$s? Dileu @@ -1228,6 +1238,8 @@ Fodd bynnag, gall fod yn llai sefydlog. Llwythwch ein porwr beta i gael profiad Bydd mewngofnodion a chyfrineiriau sydd heb eu cadw i’w gweld yma. Ni fydd mewngofnodion a chyfrineiriau’n cael eu cadw ar gyfer y gwefannau hyn. + + Dileu pob eithriad Chwilio mewngofnodion @@ -1267,6 +1279,8 @@ Fodd bynnag, gall fod yn llai sefydlog. Llwythwch ein porwr beta i gael profiad Copïo’r enw defnyddiwr Copïo’r wefan + + Agor y wefan yn y porwr Dangos cyfrinair diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 673a112f6..c2c3ec46a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -589,6 +589,8 @@ Ordner auswählen Soll dieser Ordner wirklich gelöscht werden? + + %s wird die ausgewählten Elemente löschen. %1$s gelöscht @@ -646,8 +648,10 @@ %1$s gelöscht - + Lesezeichen gelöscht + + Ausgewählte Ordner werden gelöscht RÜCKGÄNGIG @@ -751,6 +755,8 @@ Tabs gespeichert + + Sammlung gespeichert! Tab gespeichert @@ -862,6 +868,10 @@ VERWEIGERN Soll %1$s wirklich gelöscht werden? + + Durch Löschen dieses Tabs wird die gesamte Sammlung gelöscht. Sie können jederzeit neue Sammlungen erstellen. + + %1$s löschen? Löschen @@ -1263,6 +1273,8 @@ Zugangsdaten und Passwörter, die nicht gespeichert werden, werden hier angezeigt. Zugangsdaten und Passwörter werden für diese Websites nicht gespeichert. + + Alle Ausnahmen löschen Zugangsdaten durchsuchen @@ -1301,6 +1313,8 @@ Benutzernamen kopieren Website kopieren + + Website im Browser öffnen Passwort anzeigen diff --git a/app/src/main/res/values-dsb/strings.xml b/app/src/main/res/values-dsb/strings.xml index 2b98ca1b3..b9013af97 100644 --- a/app/src/main/res/values-dsb/strings.xml +++ b/app/src/main/res/values-dsb/strings.xml @@ -149,13 +149,11 @@ Scannowaś - Skrotconki + Pytnica Nastajenja pytnicow - - Pytaś z - Něnto pytaś z: + Něnto pytaś z: Wótkaz z mjazywótkłada zasajźiś @@ -263,8 +261,8 @@ Wuwijarske rědy Daloke pytanje zmólkow pśez USB - - Pytańske skrotconki pokazaś + + Pytnice pokazaś Pytańske naraźenja pokazaś @@ -579,6 +577,8 @@ Zarědnik wubraś Cośo napšawdu toś ten zarědnik wulašowaś? + + %s wubrane zapiski wulašujo. %1$s jo se wulašował @@ -634,8 +634,10 @@ %1$s wulašowany - + Cytańske znamjenja su se wulašowali + + Wubrane zarědniki se lašuju ANULĚROWAŚ @@ -728,6 +730,8 @@ %d rejtarik wubrany Rejtariki su skłaźone! + + Zběrka skłaźona! Rejtarik jo skłaźony! @@ -832,6 +836,10 @@ WÓTPOKAZAŚ Cośo napšawdu %1$s lašowaś? + + Gaž toś ten rejtarik wulašujośo, se ceła zběrka wulašujo. Móžośo kuždy cas nowe zběrki napóraś. + + %1$s lašowaś? Lašowaś @@ -886,8 +894,6 @@ Wulašujo pśeglědowańske daty awtomatiski, gaž „Skóńcyś“ z głownego menija wuběraśo Wulašujo pśeglědowańske daty awtomatiski, gaž „Skóńcyś“ z głownego menija wuběraśo - - Pśeglědowańska historija Skóńcyś @@ -1235,6 +1241,8 @@ Pśizjawjenja a gronidła, kótarež se njeskładuju, se how pokažu. Pśizjawjenja a gronidła se za toś te sedła njeskładuju. + + Wšykne wuwześa wulašowaś Pśizjawjenja pytaś @@ -1274,6 +1282,8 @@ Wužywaŕske mě kopěrowaś Sedło kopěrowaś + + Sedło we wobglědowaku wócyniś Gronidło pokazaś @@ -1449,4 +1459,13 @@ W pórěźe, som zrozměł - + + + Skrotconki + + Pytaś z + + Něnto pytaś z: + + Pytańske skrotconki pokazaś + diff --git a/app/src/main/res/values-es-rCL/strings.xml b/app/src/main/res/values-es-rCL/strings.xml index 6dda574a4..6c6bc6ada 100644 --- a/app/src/main/res/values-es-rCL/strings.xml +++ b/app/src/main/res/values-es-rCL/strings.xml @@ -147,14 +147,11 @@ Escanear - Atajos + Motor de búsqueda Ajustes del motor de búsqueda - - Buscar con - Esta vez, buscar con: - + Esta vez, buscar con: Rellenar enlace desde el portapapeles @@ -262,8 +259,8 @@ Herramientas de desarrollador Depuración remota vía USB - - Mostrar atajos de búsqueda + + Mostrar motores de búsqueda Mostrar sugerencias de búsqueda @@ -577,6 +574,8 @@ Seleccionar carpeta ¿Estás seguro de querer borrar esta carpeta? + + %s eliminará los elementos seleccionados. %1$s eliminado @@ -631,8 +630,10 @@ %1$s eliminado - + Marcadores eliminados + + Eliminando carpetas seleccionadas DESHACER @@ -725,6 +726,8 @@ %d pestaña seleccionada ¡Pestañas guardadas! + + Colección guardada ¡Pestaña guardada! @@ -830,6 +833,10 @@ DENEGAR ¿Estás seguro de que deseas eliminar %1$s? + + Eliminar esta pestaña eliminará toda la colección. Puedes crear nuevas colecciones en cualquier momento. + + ¿Eliminar %1$s? Eliminar @@ -1230,6 +1237,8 @@ Las credenciales y contraseñas que no son guardadas serán mostradas aquí. Las credenciales y contraseñas no serán guardadas para estos sitios. + + Eliminar todas las excepciones Buscar credenciales @@ -1268,6 +1277,8 @@ Copiar nombre de usuario Copiar sitio + + Abrir sitio en el navegador Mostrar contraseña @@ -1442,4 +1453,14 @@ Ok, ¡ya caché! - + + + Atajos + + Buscar con + + Esta vez, buscar con: + + + Mostrar atajos de búsqueda + diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 597297acd..1b5521080 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -589,6 +589,8 @@ Seleccionar carpeta ¿Seguro que quieres eliminar esta carpeta? + + %s va a eliminar los elementos seleccionados. Se ha eliminado %1$s @@ -644,8 +646,10 @@ Se eliminó %1$s - + Marcadores eliminados + + Eliminar carpetas seleccionadas DESHACER @@ -749,6 +753,8 @@ ¡Pestañas guardadas! + + ¡Colección guardada! ¡Pestaña guardada! @@ -860,6 +866,10 @@ DENEGAR ¿Seguro que quieres eliminar %1$s? + + Eliminar esta pestaña va a eliminar toda la colección. Puedes crear nuevas colecciones en cualquier momento. + + ¿Eliminar %1$s? Eliminar @@ -1264,6 +1274,8 @@ Los inicios de sesión y contraseñas no guardados aparecerán aquí. No se guardarán los inicios de sesión y contraseñas para estos sitios. + + Eliminar todas las excepciones Buscar inicios de sesión @@ -1302,6 +1314,8 @@ Copiar nombre de usuario Copiar sitio + + Abrir sitio en el navegador Mostrar contraseña diff --git a/app/src/main/res/values-fy-rNL/strings.xml b/app/src/main/res/values-fy-rNL/strings.xml index 9971f3f13..42b5bfb19 100644 --- a/app/src/main/res/values-fy-rNL/strings.xml +++ b/app/src/main/res/values-fy-rNL/strings.xml @@ -577,6 +577,8 @@ Map selektearje Binne jo wis dat jo dizze map fuortsmite wolle? + + %s sil de selektearre items fuortsmite. %1$s fuortsmiten @@ -631,8 +633,10 @@ %1$s fuortsmiten - + Blêdwizers fuortsmiten + + Selektearre mappen fuortsmite UNGEDIEN MEITSJE @@ -725,6 +729,8 @@ %d ljepblêd selektearre Ljepblêden bewarre! + + Kolleksje bewarre! Ljepblêd bewarre! @@ -831,6 +837,10 @@ Binne jo wis dat jo %1$s fuortsmite wolle? + + As jo dit ljepblêd fuortsmite, wurdt de hiele kolleksje fuortsmiten. Jo kinne op elk momint nije kolleksjes meitsje. + + %1$s fuortsmite? Fuortsmite @@ -886,8 +896,6 @@ Smyt automatysk navigaasjegegevens fuort wannear\'t jo yn it haadmenu ‘Ofslute’ selektearje Smyt automatysk navigaasjegegevens fuort wannear\'t jo yn it haadmenu ‘Ofslute’ selektearje - - Navigaasjeskiednis Ofslute @@ -1233,6 +1241,8 @@ Net-bewarre oanmeldingen en wachtwurden wurde hjir werjûn. Oanmeldingen en wachtwurden wurde foar dizze websites net bewarre. + + Alle útsûnderingen fuortsmite Oanmeldingen sykje @@ -1271,6 +1281,8 @@ Brûkersnamme kopiearje Website kopiearje + + Website iepenje yn browser Wachtwurd toane diff --git a/app/src/main/res/values-hsb/strings.xml b/app/src/main/res/values-hsb/strings.xml index a2e5b9610..d2ed79c9f 100644 --- a/app/src/main/res/values-hsb/strings.xml +++ b/app/src/main/res/values-hsb/strings.xml @@ -149,13 +149,11 @@ Skenować - Skrótšenki + Pytawa Nastajenja pytawy - - Pytać z - Nětko pytać z: + Nětko pytać z: Wotkaz z mjezyskłada zasadźić @@ -263,8 +261,8 @@ Wuwiwarske nastroje Zdalene pytanje zmylkow přez USB - - Pytanske skrótšenki pokazać + + Pytawy pokazać Pytanske namjety pokazać @@ -578,6 +576,8 @@ Rjadowak wubrać Chceće woprawdźe tutón rjadowak zhašeć? + + %s wubrane zapiski zhaša. %1$s je so zhašał @@ -633,8 +633,10 @@ %1$s zhašany - + Zapołožki su so zhašeli + + Wubrane rjadowaki zhašeć COFNYĆ @@ -727,6 +729,8 @@ %d rajtark wubrany Rajtarki su składowane! + + Zběrka je składowana! Rajtark je składowany! @@ -832,6 +836,10 @@ WOTPOKAZAĆ Chceće woprawdźe %1$s zhašeć? + + Hdyž tutón rajtark zhašeće, so cyła zběrka zhaša. Móžeće kóždy čas nowe zběrki wutworić. + + %1$s zhašeć? Zhašeć @@ -887,8 +895,6 @@ Zhaša přehladowanske daty awtomatisce, hdyž „Skónčić“ z hłowneho menija wuběraće Zhaša přehladowanske daty awtomatisce, hdyž „Skónčić“ z hłowneho menija wuběraće - - Přehladowanska historija Skónčić @@ -1233,6 +1239,8 @@ Přizjewjenja a hesła, kotrež so njeskładuja, so tu pokazaja. Přizjewjenja a hesła so za tute sydła njeskładuja. + + Wšě wuwzaća zhašeć Přizjewjenja pytać @@ -1271,6 +1279,8 @@ Wužiwarske mjeno kopěrować Sydło kopěrować + + Sydło we wobhladowaku wočinić Hesło pokazać @@ -1447,4 +1457,13 @@ W porjadku, sym zrozumił - + + + Skrótšenki + + Pytać z + + Nětko pytać z: + + Pytanske skrótšenki pokazać + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 1cca008f9..5e5d2b00d 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -592,6 +592,8 @@ Seleziona cartella Eliminare questa cartella? + + %s eliminerà gli elementi selezionati. %1$s eliminata @@ -647,8 +649,10 @@ Eliminato %1$s - + Segnalibri eliminati + + Eliminazione cartelle selezionate ANNULLA @@ -752,6 +756,8 @@ Schede salvate. + + Raccolta salvata. Scheda salvata. @@ -864,6 +870,10 @@ NEGA Eliminare %1$s? + + L’eliminazione di questa scheda rimuoverà l’intera raccolta. Puoi creare nuove raccolte in qualsiasi momento. + + Eliminare %1$s? Elimina @@ -1265,6 +1275,8 @@ Le credenziali e le password non salvate verranno mostrate qui. Le credenziali e le password non verranno salvate per questi siti. + + Elimina tutte le eccezioni Cerca credenziali @@ -1303,6 +1315,8 @@ Copia nome utente Copia sito + + Apri sito nel browser Mostra password diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index a15a0ac57..dacb1a106 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -564,6 +564,8 @@ בחירת תיקייה האם ברצונך למחוק תיקייה זו? + + ‏%s ימחק את הפריטים שנבחרו. נמחק %1$s @@ -619,8 +621,10 @@ נמחק %1$s - + הסימניות נמחקו + + בתהליך מחיקת התיקיות שנבחרו ביטול @@ -711,6 +715,8 @@ לשונית אחת נבחרה הלשוניות נשמרו! + + האוסף נשמר! הלשונית נשמרה! @@ -817,6 +823,10 @@ לחסום האם ברצונך למחוק את %1$s? + + מחיקת לשונית זו תמחק את האוסף כולו. באפשרותך ליצור אוספים חדשים בכל עת. + + למחוק את %1$s? מחיקה @@ -1207,6 +1217,8 @@ כניסות וססמאות שאינן שמורות יופיעו כאן. כניסות וססמאות לא יישמרו עבור אתרים אלו. + + מחיקת כל החריגות חיפוש כניסות @@ -1243,6 +1255,8 @@ העתקת שם משתמש העתקת אתר + + פתיחת אתר בדפדפן הצגת ססמה diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index 0500d96e7..d49539e2b 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -580,6 +580,8 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Fren akaram Tebɣiḍ ad tekseḍ akaram-agi? + + %s ad ikkes iferdisen ittwafernen. %1$s4 yettwakkes @@ -635,8 +637,10 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Kkes %1$s - + Ticraḍ n isebtar ttwakksent + + Tukksa n ikaramen ittwafernen UƔAL @@ -731,6 +735,8 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara %d n yiccer yettwafren Accaren ttwaskelsen! + + Tagrumma tettwasekles! Iccer yettwaselkes! @@ -837,6 +843,10 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara GDEL Tebɣiḍ ad tekseḍ %1$s? + + S tukksa n iccer-a ad tettwakkes akk tegrumma. Tzemreḍ ad ternuḍ tagrumma tamaynut melmi i tebɣiḍ. + + Kkes %1$s? Kkes @@ -892,8 +902,6 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Ad yekkes s wudem awurman isefka n tunigin ticki tferneḍ "Ffeɣ" seg umuɣ agejdan Ad yekkes s wudem awurman isefka n tunigin ticki tferneḍ "Ffeɣ" seg umuɣ agejdan - - Amazray n tunigin Ffeɣ @@ -1240,6 +1248,8 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Inekcam akked wawalen uffiren ur yettwaskelsen ara ad ttwaseknen dagi. Inekcam akked wawalen uffiren ur ttwaseklasen ara i yismal-a. + + Kkes akk tisuraf Nadi inekcam @@ -1278,6 +1288,8 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Nɣel isem n useqdac Nɣel asmel + + Ldi asmel deg iminig Sken awal uffir diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 7fa5cc3d8..01954b6c4 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -151,13 +151,11 @@ Skann - Snarveier + Søkemotor Innstillinger for søkemotor - - Søk med - Denne gangen, søk med: + Denne gangen, søk med: Fyll inn lenke fra utklippstavlen @@ -265,8 +263,8 @@ Fjernfeilsøking via USB - - Vis søkesnarveier + + Vis søkemotorer Vis søkeforslag @@ -1232,7 +1230,7 @@ Spør om å lagre - Aldri lagre + Lagre aldri Autofyll @@ -1479,4 +1477,13 @@ OK, jeg skjønner - + + + Snarveier + + Søk med + + Denne gangen, søk med: + + Vis søkesnarveier + diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index b318dc1f1..1d5ee0a68 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -60,6 +60,7 @@ @color/tab_tray_heading_icon_inactive_dark_theme @color/tab_tray_item_thumbnail_background_dark_theme @color/tab_tray_item_thumbnail_icon_dark_theme + @color/tab_tray_selected_mask_dark_theme @color/top_site_background_dark_theme diff --git a/app/src/main/res/values-nn-rNO/strings.xml b/app/src/main/res/values-nn-rNO/strings.xml index e70e2e0b0..70b0566dd 100644 --- a/app/src/main/res/values-nn-rNO/strings.xml +++ b/app/src/main/res/values-nn-rNO/strings.xml @@ -148,14 +148,8 @@ Skann - - Snarvegar Innstillingar for søkjemotor - - Søk med - - Denne gong, søk med: Fyll inn lenke frå utklippstavla @@ -265,8 +259,6 @@ Fjernfeilsøking via USB - - Vis søkjesnarvegar Vis søkjeforslag @@ -581,6 +573,8 @@ Er du sikker på at du vil slette denne mappa? + + %s vil slette dei valde elementa. Sletta %1$s @@ -636,8 +630,10 @@ Sletta %1$s - + Bokmerke sletta + + Slettar valde mapper ANGRE @@ -732,6 +728,8 @@ %d fane vald Faner lagra! + + Samling lagra! Fane lagra! @@ -838,6 +836,10 @@ AVSLÅ Er du sikker på at du vil slette %1$s? + + Dersom du slettar denne fana, blir heile samlinga sletta. Du kan når som helst lage nye samlingar. + + Vil du slette %1$s? Slett @@ -1112,7 +1114,7 @@ Berre i tilpassa faner - Kryptominarar + Kryptoutvinnarar Fingerprinters Blokkert @@ -1127,7 +1129,7 @@ Blokkerer informasjonskapslar som annonsenettverk og analyseselskap brukar for å samanstille aktiviteten din på nettet på tvers av nettstadar. - Kryptominarar + Kryptoutvinnarar Hindrar vondsinna skript i å få tilgang til eininga di for å utvinne digitale valutaer. @@ -1215,7 +1217,7 @@ Spør om å lagre - Aldri lagre + Lagre aldri Autofyll @@ -1240,6 +1242,8 @@ Innloggingar og passord som ikkje er lagra vil visast her. Innloggingar og passord vil ikkje bli lagra for desse nettstadane. + + Slett alle unntak Søk innloggingar @@ -1279,6 +1283,8 @@ Kopier brukarnamn Kopier nettstad + + Opne nettstad i nettlesaren Vis passord @@ -1455,4 +1461,13 @@ OK, eg forstår det - + + + Snarvegar + + Søk med + + Denne gong, søk med: + + Vis søkjesnarvegar + diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index 19b8fd9b1..3d7507109 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -343,6 +343,8 @@ Telemetria + + Donadas tecnicas e d’utilizacion Donadas marketing @@ -362,6 +364,8 @@ Activar la sincronizacion Connexion + + S’identificar per se reconnectar Suprimir lo compte @@ -557,6 +561,8 @@ Seleccionar un dossièr Volètz vertadièrament suprimir aqueste dorsièr ? + + %s suprimirà los elements seleccionats. %1$s suprimit @@ -611,9 +617,11 @@ %1$s suprimit - + Marcapagina suprimit + + Supression dels dossièrs seleccionats ANULLAR @@ -622,6 +630,10 @@ Permissions Anar als paramètres + + Accès rapid als paramètres Recomandat @@ -804,6 +816,8 @@ REFUSAR Volètz vertadièrament suprimir %1$s ? + + Suprimir %1$s ? Suprimir @@ -1120,6 +1134,8 @@ Per ne saber mai sus Sync. Excepcions + + Suprimir totas las excepcions Recercar d’identificants diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index d7167c0c5..1d2a75161 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -150,13 +150,11 @@ Skanuj - Skróty + Wyszukiwarka Ustawienia wyszukiwarki - - Szukaj w - Tym razem szukaj w: + Tym razem szukaj w: Wklej odnośnik ze schowka @@ -264,8 +262,8 @@ Narzędzia dla programistów Zdalne debugowanie przez USB - - Skróty wyszukiwania + + Wyszukiwarki Podpowiedzi wyszukiwania @@ -581,6 +579,8 @@ Wybierz folder Czy na pewno usunąć ten folder? + + %s usunie zaznaczone elementy. Usunięto folder „%1$s” @@ -635,8 +635,10 @@ Usunięto zakładkę „%1$s” - + Usunięto zakładki + + Usuwanie zaznaczonych folderów Cofnij @@ -730,6 +732,8 @@ Wybrane karty: %d Zachowano karty + + Zachowano kolekcję Zachowano kartę @@ -788,7 +792,7 @@ - Sesja przeglądania prywatnego + Sesja trybu prywatnego Usuń prywatne karty @@ -836,6 +840,10 @@ Odmów Czy na pewno usunąć „%1$s”? + + Usunięcie tej karty spowoduje usunięcie całej kolekcji. W każdej chwili można utworzyć nowe kolekcje. + + Czy usunąć „%1$s”? Usuń @@ -1237,6 +1245,8 @@ Tutaj będą wyświetlane dane logowania i hasła, które nie będą zachowywane. Dane logowania i hasła dla tych witryn nie będą zachowywane. + + Usuń wszystkie wyjątki Szukaj danych logowania @@ -1275,6 +1285,8 @@ Kopiuj nazwę użytkownika Kopiuj witrynę + + Otwórz witrynę w przeglądarce Wyświetl hasło @@ -1451,4 +1463,13 @@ OK - + + + Skróty + + Szukaj w + + Tym razem szukaj w: + + Skróty wyszukiwania + diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index ac25486d8..1a640b8cf 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -580,6 +580,8 @@ Selecionar pasta Tem a certeza que deseja eliminar esta pasta? + + %s irá excluir os itens selecionados. Eliminada %1$s @@ -634,8 +636,10 @@ %1$s eliminado - + Marcadores eliminados + + A eliminar as pastas selecionadas ANULAR @@ -729,6 +733,8 @@ %d separador selecionado Separadores guardados! + + Coleção guardada! Separador guardado! @@ -834,6 +840,10 @@ NEGAR Tem a certeza que pretende eliminar %1$s? + + A eliminação deste separador irá eliminar toda a coleção. Pode criar coleções novas a qualquer momento. + + Eliminar %1$s? Eliminar @@ -889,8 +899,6 @@ Exclui automaticamente os dados de navegação quando seleciona "Sair" no menu principal Exclui automaticamente os dados de navegação quando seleciona \"Sair\" no menu principal - - Histórico de navegação Sair @@ -1239,6 +1247,8 @@ As credenciais e palavras-passe que não estão guardadas serão mostradas aqui. As credenciais e palavras-passe não serão guardadas para estes sites. + + Eliminar todas as exceções Pesquisar credenciais @@ -1278,6 +1288,8 @@ Copiar nome de utilizador Copiar site + + Abrir site no navegador Mostrar palavra-passe diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 48e1a9269..1707c0e7e 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -148,13 +148,11 @@ Skeniraj - Bližnjice + Iskalnik Nastavitve iskalnika - - Išči z - Tokrat išči z iskalnikom: + Tokrat išči z iskalnikom: Izpolni povezavo iz odložišča @@ -262,8 +260,8 @@ Razvojna orodja Oddaljeno razhroščevanje preko USB - - Prikaži bližnjice za iskanje + + Prikaži iskalnike Prikaži predloge iskanja @@ -583,6 +581,8 @@ Izberi mapo Ali ste prepričani, da želite zbrisati to mapo? + + %s bo izbrisal izbrane predmete. Mapa %1$s izbrisana @@ -638,8 +638,10 @@ Izbrisan %1$s - + Zaznamki izbrisani + + Brisanje izbranih map RAZVELJAVI @@ -734,6 +736,8 @@ %d izbran zavihek Zavihki shranjeni! + + Zbirka shranjena! Zavihek shranjen! @@ -839,6 +843,10 @@ ZAVRNI Ali ste prepričani, da želite izbrisati %1$s? + + Če izbrišete ta zavihek, boste izbrisali celotno zbirko. Nove zbirke lahko ustvarite kadarkoli. + + Izbrišem %1$s? Izbriši @@ -1241,6 +1249,8 @@ Tu bodo prikazane prijave in gesla, ki niso shranjena. Prijave in gesla za te strani ne bodo shranjene. + + Izbriši vse izjeme Iskanje prijav @@ -1279,6 +1289,8 @@ Kopiraj uporabniško ime Kopiraj spletno mesto + + Odpri stran v brskalniku Prikaži geslo @@ -1459,4 +1471,13 @@ V redu, razumem - + + + Bližnjice + + Išči z + + Tokrat išči z iskalnikom: + + Prikaži bližnjice za iskanje + diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 322f90a73..3a0e30d4c 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -147,13 +147,11 @@ Скенирај - Пречице + Претраживач Подешавања претраживача - - Претражи са - Овај пут потражи користећи: + Овај пут потражи користећи: Убаци везу из оставе @@ -262,8 +260,8 @@ Алати за програмере Удаљено отклањање грешака преко USB-а - - Пречице за претрагу + + Прикажи претраживаче Предлози претраге @@ -576,6 +574,8 @@ Изабери фасциклу Да ли сте сигурни да желите да избришете ову фасциклу? + + %s ће обрисати одабране ставке. %1$s избрисано @@ -630,8 +630,10 @@ Обрисано %1$s - + Обележивачи су избрисани + + Брисање одабраних фасцикли ОПОЗОВИ @@ -725,6 +727,8 @@ %d језичак изабран Језичци сачувани! + + Збирка је сачувана! Језичак сачуван! @@ -830,6 +834,10 @@ ОДБИЈ Да ли сте сигурни да желите обрисати збирку %1$s? + + Брисањем овог језичка обрисаћете целу једну збирку. Можете да направите нову збирку било кад. + + Обрисати %1$s? Обриши @@ -884,8 +892,6 @@ Самостално брише податке прегледања када изаберете „Изађи“ у главном менију Самостално брише податке прегледања када изаберете \"Изађи\" у главном менију - - Историјат прегледања Изађи @@ -1236,6 +1242,8 @@ Овде ће бити приказане пријаве и лозинке које нису сачуване. Пријаве и лозинке неће бити сачуване за ове странице. + + Обрисати све изузетке Претражи пријаве @@ -1274,6 +1282,8 @@ Копирај корисничко Копирај страницу + + Отвори страницу у прегледачу Прикажи лозинку @@ -1450,4 +1460,13 @@ Важи, разумем - + + + Пречице + + Претражи са + + Овај пут потражи користећи: + + Пречице за претрагу + diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 9304c25b2..60dc9e5a9 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -151,14 +151,11 @@ Skanna - Genvägar + Sökmotor Inställningar för sökmotor - - Sök med - - Denna gång, sök med: + Denna gång, sök med: Fyll i länk från urklipp @@ -266,8 +263,8 @@ Utvecklarverktyg Fjärrfelsökning via USB - - Visa sökgenvägar + + Visa sökmotorer Visa sökförslag @@ -1473,4 +1470,14 @@ Ok, jag förstår - + + + Genvägar + + Sök med + + + Denna gång, sök med: + + Visa sökgenvägar + diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index da34254b5..bfef1a9e8 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -582,6 +582,8 @@ సంచయపు ఎంపిక మీరు నిజంగానే ఈ సంచయాన్ని తొలగించాలనుకుంటున్నారా? + + ఎంచుకున్న అంశాలను %s తొలగిస్తుంది. %1$s తొలగించబడింది @@ -638,9 +640,11 @@ %1$s తొలగించబడింది - + ఇష్టాంశాలు తొలగించబడ్డాయి + + ఎంచుకున్న సంచయాలను తొలగిస్తూంది చర్య రద్దు @@ -736,6 +740,8 @@ %d ట్యాబు ఎంచుకున్నారు ట్యాబులు భద్రమయ్యాయి! + + సేకరణ భద్రమయ్యింది! ట్యాబు భద్రమయింది! @@ -840,6 +846,10 @@ తిరస్కరించు మీరు నిజంగానే %1$s‌ను తొలగించాలనుకుంటున్నారా? + + ఈ ట్యాబును తొలగిస్తే మొత్తం సేకరణ పోతుంది. కొత్త సేకరణలను మీరు ఎప్పుడైనా సృష్టించుకోవచ్చు. + + %1$s‌ని తొలగించాలా? తొలగించు @@ -1250,6 +1260,8 @@ భద్రపరచని ప్రవేశాలు, సంకేతపదాలు ఇక్కడ కనిపిస్తాయి. ఈ సైట్ల ప్రవేశాలు, సంకేతపదాలు భద్రపరచబడవు. + + మినహాయింపులన్నీ తొలగించు ప్రవేశాలను వెతకండి @@ -1289,6 +1301,8 @@ వాడుకరి పేరుని కాపీచెయ్యి సైటును కాపీచెయ్యి + + సైటును విహారిణిలో తెరువు సంకేతపదాన్ని చూపించు diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index e00860230..2752cf6ed 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -150,14 +150,11 @@ Сканувати - Ярлики - + Засіб пошуку Налаштування пошукових засобів - - Шукати з - Цього разу шукати з: + Цього разу шукати з: Заповнити посилання з буфера обміну @@ -268,8 +265,8 @@ Віддалене зневадження через USB - - Посилання на пошукові засоби + + Показати засоби пошуку Показувати пошукові пропозиції @@ -586,6 +583,8 @@ Обрати теку Ви впевнені, що хочете видалити цю теку? + + %s видалить вибрані елементи. %1$s видалено @@ -641,8 +640,10 @@ %1$s видалено - + Закладки видалено + + Видалення вибраних тек ВІДНОВИТИ @@ -736,6 +737,8 @@ Вибрано %d вкладку Вкладки збережено! + + Збірку збережено! Вкладка збережена! @@ -842,6 +845,10 @@ ЗАБОРОНИТИ Ви дійсно хочете видалити %1$s? + + Видалення цієї вкладки видалить всю збірку. Ви можете створити нові збірки будь-коли. + + Видалити %1$s? Видалити @@ -1246,6 +1253,8 @@ Не збережені паролі з’являтимуться тут. Паролі для цих сайтів не зберігатимуться. + + Видалити всі винятки Шукати паролі @@ -1285,6 +1294,8 @@ Копіювати ім’я користувача Копіювати сайт + + Відкрити сайт у браузері Показати пароль @@ -1460,4 +1471,14 @@ Гаразд, зрозуміло - + + + Ярлики + + + Шукати з + + Цього разу шукати з: + + Посилання на пошукові засоби + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 21854871f..619a14af1 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -85,6 +85,7 @@ @color/ink_20_48a @color/light_grey_10 @color/light_grey_60 + @color/violet_70_12a #FBFBFE @@ -144,6 +145,7 @@ @color/violet_50_48a @color/dark_grey_50 @color/dark_grey_05 + @color/violet_50_32a #FBFBFE @@ -249,6 +251,7 @@ @color/tab_tray_heading_icon_inactive_light_theme @color/tab_tray_item_thumbnail_background_light_theme @color/tab_tray_item_thumbnail_icon_light_theme + @color/tab_tray_selected_mask_light_theme #DFDFE3 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 51601631d..04601fc28 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,6 +23,28 @@ 1 open tab. Tap to switch tabs. %1$s open tabs. Tap to switch tabs. + + %1$d selected + + Add new collection + + Name + + Select collection + + Exit multiselect mode + + Save selected tabs to collection + + Selected %1$s + + Unselected %1$s + + Exited multiselect mode + + Entered multiselect mode, select tabs to save to a collection + + Selected %1$s is produced by Mozilla. @@ -498,6 +520,8 @@ Remove %1$s (Private Mode) + + Save @@ -703,9 +727,9 @@ Add new collection - Select All + Select all - Deselect All + Deselect all Select tabs to save diff --git a/mozilla-detekt-rules/src/test/java/org/mozilla/fenix/detektrules/MozillaBannedPropertyAccessTest.kt b/mozilla-detekt-rules/src/test/java/org/mozilla/fenix/detektrules/MozillaBannedPropertyAccessTest.kt index e9b6b7271..b31c46ac7 100644 --- a/mozilla-detekt-rules/src/test/java/org/mozilla/fenix/detektrules/MozillaBannedPropertyAccessTest.kt +++ b/mozilla-detekt-rules/src/test/java/org/mozilla/fenix/detektrules/MozillaBannedPropertyAccessTest.kt @@ -2,12 +2,16 @@ * 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/. */ +@file:Suppress("Deprecation") + package org.mozilla.fenix.detektrules -import io.gitlab.arturbosch.detekt.test.lint +import io.gitlab.arturbosch.detekt.api.Config import io.gitlab.arturbosch.detekt.api.YamlConfig +import io.gitlab.arturbosch.detekt.test.lint import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest @@ -17,10 +21,18 @@ import org.junit.jupiter.params.provider.MethodSource import java.util.stream.Stream internal class MozillaBannedPropertyAccessTest { + + private lateinit var config: Config + + @BeforeEach + fun setup() { + config = YamlConfig.loadResource(this.javaClass.getResource("/config.yml")) + } + @Test internal fun `non compliant property access should warn`() { val findings = - MozillaBannedPropertyAccess(YamlConfig.loadResource(this.javaClass.getResource("/config.yml"))).lint( + MozillaBannedPropertyAccess(config).lint( NONCOMPLIANT_ACCESS.trimIndent() ) assertEquals(1, findings.size) @@ -32,7 +44,7 @@ internal class MozillaBannedPropertyAccessTest { @ParameterizedTest(name = "{1} should not warn") internal fun testCompliantWhen(source: String) { val findings = - MozillaBannedPropertyAccess(YamlConfig.loadResource(this.javaClass.getResource("/config.yml"))).lint( + MozillaBannedPropertyAccess(config).lint( source ) assertTrue(findings.isEmpty()) diff --git a/taskcluster/fenix_taskgraph/transforms/build.py b/taskcluster/fenix_taskgraph/transforms/build.py index 3136b1b91..399a4c006 100644 --- a/taskcluster/fenix_taskgraph/transforms/build.py +++ b/taskcluster/fenix_taskgraph/transforms/build.py @@ -45,7 +45,6 @@ def add_shippable_secrets(config, tasks): } for key, target_file in ( ('adjust', '.adjust_token'), ('firebase', 'app/src/{}/res/values/firebase.xml'.format(gradle_build_type)), - ('digital_asset_links', '.digital_asset_links_token'), ('leanplum', '.leanplum_token'), ('sentry_dsn', '.sentry_token'), ('mls', '.mls_token'), @@ -56,7 +55,6 @@ def add_shippable_secrets(config, tasks): "path": target_file, } for fake_value, target_file in ( ("faketoken", ".adjust_token"), - ("faketoken", ".digital_asset_links_token"), ("fake:token", ".leanplum_token"), # : is used by leanplum ("faketoken", ".mls_token"), ("https://fake@sentry.prod.mozaws.net/368", ".sentry_token"),