Copione merged onto master
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
commit
9a4ae5a0b1
|
@ -83,7 +83,6 @@ gen-external-apklibs
|
|||
.leanplum_token
|
||||
.adjust_token
|
||||
.sentry_token
|
||||
.digital_asset_links_token
|
||||
.mls_token
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<HomeActivity>) {
|
||||
IdlingRegistry.getInstance().register(
|
||||
AddonsInstallingIdlingResource(
|
||||
activityTestRule.activity.supportFragmentManager
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun unregisterAddonInstallingIdlingResource(activityTestRule: ActivityTestRule<HomeActivity>) {
|
||||
IdlingRegistry.getInstance().unregister(
|
||||
AddonsInstallingIdlingResource(
|
||||
activityTestRule.activity.supportFragmentManager
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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<View>(R.id.add_ons_progress_bar)
|
||||
} ?: return true
|
||||
|
||||
if (progressbar.visibility == VISIBLE)
|
||||
return false
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -96,6 +96,7 @@ class ContextMenusTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Ignore("Test failures: https://github.com/mozilla-mobile/fenix/issues/12473")
|
||||
@Test
|
||||
fun verifyContextCopyLink() {
|
||||
val pageLinks =
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -66,6 +66,7 @@ class SearchTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/12968")
|
||||
@Test
|
||||
fun shortcutSearchEngineSettingsTest() {
|
||||
homeScreen {
|
||||
|
|
|
@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -366,7 +366,7 @@ class BrowserRobot {
|
|||
}
|
||||
|
||||
fun openNavigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition {
|
||||
|
||||
mDevice.waitForIdle(waitingTime)
|
||||
navURLBar().click()
|
||||
|
||||
NavigationToolbarRobot().interact()
|
||||
|
|
|
@ -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<RecyclerView.ViewHolder>(
|
||||
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<RecyclerView.ViewHolder>(
|
||||
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")))
|
||||
|
|
|
@ -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))
|
|
@ -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)))
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String>?,
|
||||
store: BrowserStore,
|
||||
publicSuffixList: PublicSuffixList
|
||||
): List<Tab> {
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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<Tab>,
|
||||
private val new: List<Tab>,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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<AppBarLayout.LayoutParams> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<TabCounter> = WeakReference<TabCounter>(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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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?,
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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').
|
||||
|
|
|
@ -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<TabCollection>.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
|
||||
}
|
||||
|
|
|
@ -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<Session>) {
|
||||
|
@ -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<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = HEADER_MARGIN.dpToPx(resources.displayMetrics)
|
||||
}
|
||||
}
|
||||
|
||||
view.bottom_bar.background = resources.getDrawable(
|
||||
ThemeManager.resolveAttribute(R.attr.bottomBarBackgroundTop, requireContext()),
|
||||
null
|
||||
)
|
||||
|
||||
view.homeAppBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
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<Session>.toTabs(): List<Tab> {
|
||||
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
|
||||
|
|
|
@ -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<String>? = null,
|
||||
|
@ -322,7 +356,7 @@ class DefaultSessionControlController(
|
|||
}
|
||||
|
||||
override fun handleCreateCollection() {
|
||||
showCollectionCreationFragment(step = SaveCollectionStep.SelectTabs)
|
||||
showTabTrayCollectionCreation()
|
||||
}
|
||||
|
||||
private fun showShareFragment(data: List<ShareData>) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<TopSite>,
|
||||
collections: List<TabCollection>,
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<BookmarkNode>(), UserInteractionHan
|
|||
|
||||
private fun showTabTray() {
|
||||
invokePendingDeletion()
|
||||
TabTrayDialogFragment.show(parentFragmentManager)
|
||||
navigate(BookmarkFragmentDirections.actionGlobalTabTrayDialogFragment())
|
||||
}
|
||||
|
||||
private fun navigate(directions: NavDirections) {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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<HistoryItem>(), UserInteractionHandl
|
|||
|
||||
private fun showTabTray() {
|
||||
invokePendingDeletion()
|
||||
TabTrayDialogFragment.show(parentFragmentManager)
|
||||
findNavController().nav(
|
||||
R.id.historyFragment,
|
||||
HistoryFragmentDirections.actionGlobalTabTrayDialogFragment()
|
||||
)
|
||||
}
|
||||
|
||||
private fun getMultiSelectSnackBarMessage(historyItems: Set<HistoryItem>): String {
|
||||
|
@ -259,7 +261,10 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
/**
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -86,6 +86,7 @@ class SearchFragment : Fragment(), UserInteractionHandler {
|
|||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val activity = activity as HomeActivity
|
||||
val settings = activity.settings()
|
||||
val args by navArgs<SearchFragmentArgs>()
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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<Boolean> { 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<Preference>(R.string.pref_key_sign_in)
|
||||
val preferenceFirefoxAccount =
|
||||
requirePreference<AccountPreference>(R.string.pref_key_account)
|
||||
val preferenceFirefoxAccountAuthError =
|
||||
requirePreference<AccountAuthErrorPreference>(R.string.pref_key_account_auth_error)
|
||||
val accountPreferenceCategory =
|
||||
requirePreference<PreferenceCategory>(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<Preference>(getPreferenceKey(R.string.pref_key_override_fxa_server))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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/. */
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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<Preference>(R.string.pref_key_sign_in)
|
||||
private val preferenceFirefoxAccount =
|
||||
fragment.requirePreference<AccountPreference>(R.string.pref_key_account)
|
||||
private val preferenceFirefoxAccountAuthError =
|
||||
fragment.requirePreference<AccountAuthErrorPreference>(R.string.pref_key_account_auth_error)
|
||||
private val accountPreferenceCategory =
|
||||
fragment.requirePreference<PreferenceCategory>(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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<SavedLogin>): List<SavedLogin>
|
||||
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<SavedLogin>): List<SavedLogin> {
|
||||
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<SavedLogin>): List<SavedLogin> {
|
||||
return logins.sortedByDescending { it.timeLastUsed }
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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**.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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<SyncedTabsAdapter.AdapterItem, SyncedTabsViewHolder>(
|
||||
DiffCallback
|
||||
) {
|
||||
) : ListAdapter<SyncedTabsAdapter.AdapterItem, SyncedTabsViewHolder>(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<SyncedDeviceTabs>) {
|
||||
val allDeviceTabs = mutableListOf<AdapterItem>()
|
||||
|
||||
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<AdapterItem>() {
|
||||
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) =
|
||||
|
|
|
@ -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<SyncedTabsAdapter.AdapterItem>().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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String>,
|
||||
private val onNewCollectionClicked: () -> Unit
|
||||
) : RecyclerView.Adapter<CollectionsAdapter.CollectionItemViewHolder>() {
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
|
@ -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<Any>
|
||||
) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Tab>)
|
||||
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<Session>) -> Unit,
|
||||
private val showAddNewCollectionDialog: (List<Session>) -> 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<Tab>) {
|
||||
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<Session> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<TabTrayDialogFragmentArgs>()
|
||||
|
||||
private val tabsFeature = ViewBoundFeatureWrapper<TabsFeature>()
|
||||
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<Session>) {
|
||||
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<RecyclerView>(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<Session>) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<TabTrayDialogFragmentState, TabTrayDialogFragmentAction>(
|
||||
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<Tab>()
|
||||
|
||||
object Normal : Mode()
|
||||
data class MultiSelect(override val selectedItems: Set<Tab>) : 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()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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<Tab>)
|
||||
|
||||
/**
|
||||
* 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<Tab>) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<View>(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<ViewGroup.MarginLayoutParams> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<TabsTray.Observer>) {
|
||||
// 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<TabsTray.Observer>
|
||||
) {
|
||||
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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue