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
|
.leanplum_token
|
||||||
.adjust_token
|
.adjust_token
|
||||||
.sentry_token
|
.sentry_token
|
||||||
.digital_asset_links_token
|
|
||||||
.mls_token
|
.mls_token
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -354,21 +354,6 @@ android.applicationVariants.all { variant ->
|
||||||
println("X_X")
|
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
|
// MLS: Read token from local file if it exists
|
||||||
// -------------------------------------------------------------------------------------------------
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
@ -413,6 +398,7 @@ dependencies {
|
||||||
implementation Deps.leanplum_fcm
|
implementation Deps.leanplum_fcm
|
||||||
|
|
||||||
implementation Deps.mozilla_concept_engine
|
implementation Deps.mozilla_concept_engine
|
||||||
|
implementation Deps.mozilla_concept_menu
|
||||||
implementation Deps.mozilla_concept_push
|
implementation Deps.mozilla_concept_push
|
||||||
implementation Deps.mozilla_concept_storage
|
implementation Deps.mozilla_concept_storage
|
||||||
implementation Deps.mozilla_concept_sync
|
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
|
@Test
|
||||||
fun verifyContextCopyLink() {
|
fun verifyContextCopyLink() {
|
||||||
val pageLinks =
|
val pageLinks =
|
||||||
|
|
|
@ -9,6 +9,7 @@ import androidx.test.uiautomator.UiDevice
|
||||||
import okhttp3.mockwebserver.MockWebServer
|
import okhttp3.mockwebserver.MockWebServer
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
|
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
|
||||||
|
@ -124,6 +125,7 @@ class DeepLinkTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Ignore("Crashing, see: https://github.com/mozilla-mobile/fenix/issues/11239")
|
||||||
@Test
|
@Test
|
||||||
fun openSettingsSearchEngine() {
|
fun openSettingsSearchEngine() {
|
||||||
robot.openSettingsSearchEngine {
|
robot.openSettingsSearchEngine {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import mozilla.components.browser.storage.sync.PlacesHistoryStorage
|
||||||
import okhttp3.mockwebserver.MockWebServer
|
import okhttp3.mockwebserver.MockWebServer
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
|
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
|
||||||
|
@ -164,6 +165,7 @@ class HistoryTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Ignore("Failing test: https://github.com/mozilla-mobile/fenix/issues/12893")
|
||||||
@Test
|
@Test
|
||||||
fun deleteAllHistoryTest() {
|
fun deleteAllHistoryTest() {
|
||||||
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
|
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
|
||||||
|
|
|
@ -66,6 +66,7 @@ class NavigationToolbarTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Ignore("Flaky test: https://github.com/mozilla-mobile/fenix/issues/12894")
|
||||||
@Test
|
@Test
|
||||||
fun goForwardTest() {
|
fun goForwardTest() {
|
||||||
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
|
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
|
||||||
|
|
|
@ -66,6 +66,7 @@ class SearchTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/12968")
|
||||||
@Test
|
@Test
|
||||||
fun shortcutSearchEngineSettingsTest() {
|
fun shortcutSearchEngineSettingsTest() {
|
||||||
homeScreen {
|
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
|
@Test
|
||||||
// Walks through settings menu and sub-menus to ensure all items are present
|
// Walks through settings menu and sub-menus to ensure all items are present
|
||||||
fun settingsMenuBasicsItemsTests() {
|
fun settingsMenuBasicsItemsTests() {
|
||||||
|
@ -90,6 +91,7 @@ class SettingsBasicsTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/12968")
|
||||||
@Test
|
@Test
|
||||||
fun selectNewDefaultSearchEngine() {
|
fun selectNewDefaultSearchEngine() {
|
||||||
// Goes through the settings and changes the default search engine, then verifies it has changed.
|
// 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 okhttp3.mockwebserver.MockWebServer
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
|
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
|
||||||
|
@ -216,6 +217,7 @@ class SmokeTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Ignore("Flaky test: https://github.com/mozilla-mobile/fenix/issues/12899")
|
||||||
@Test
|
@Test
|
||||||
fun verifyETPToolbarShieldIconIsNotDisplayedIfETPIsOFFGloballyTest() {
|
fun verifyETPToolbarShieldIconIsNotDisplayedIfETPIsOFFGloballyTest() {
|
||||||
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
|
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
|
||||||
|
|
|
@ -366,7 +366,7 @@ class BrowserRobot {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openNavigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition {
|
fun openNavigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition {
|
||||||
|
mDevice.waitForIdle(waitingTime)
|
||||||
navURLBar().click()
|
navURLBar().click()
|
||||||
|
|
||||||
NavigationToolbarRobot().interact()
|
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
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.Visibility
|
import androidx.test.espresso.matcher.ViewMatchers.Visibility
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
|
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.withEffectiveVisibility
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
|
@ -74,7 +75,9 @@ class SettingsRobot {
|
||||||
|
|
||||||
// ADVANCED SECTION
|
// ADVANCED SECTION
|
||||||
fun verifyAdvancedHeading() = assertAdvancedHeading()
|
fun verifyAdvancedHeading() = assertAdvancedHeading()
|
||||||
fun verifyAddons() = assertAddons()
|
fun verifyAddons() = assertAddonsButton()
|
||||||
|
|
||||||
|
// DEVELOPER TOOLS SECTION
|
||||||
fun verifyRemoteDebug() = assertRemoteDebug()
|
fun verifyRemoteDebug() = assertRemoteDebug()
|
||||||
fun verifyLeakCanaryButton() = assertLeakCanaryButton()
|
fun verifyLeakCanaryButton() = assertLeakCanaryButton()
|
||||||
|
|
||||||
|
@ -211,6 +214,13 @@ class SettingsRobot {
|
||||||
SettingsSubMenuDataCollectionRobot().interact()
|
SettingsSubMenuDataCollectionRobot().interact()
|
||||||
return SettingsSubMenuDataCollectionRobot.Transition()
|
return SettingsSubMenuDataCollectionRobot.Transition()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun openAddonsManagerMenu(interact: SettingsSubMenuAddonsManagerRobot.() -> Unit): SettingsSubMenuAddonsManagerRobot.Transition {
|
||||||
|
addonsManagerButton().click()
|
||||||
|
|
||||||
|
SettingsSubMenuAddonsManagerRobot().interact()
|
||||||
|
return SettingsSubMenuAddonsManagerRobot.Transition()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -349,15 +359,25 @@ private fun assertDeveloperToolsHeading() {
|
||||||
|
|
||||||
// ADVANCED SECTION
|
// ADVANCED SECTION
|
||||||
private fun assertAdvancedHeading() {
|
private fun assertAdvancedHeading() {
|
||||||
scrollToElementByText("Advanced")
|
onView(withId(R.id.recycler_view)).perform(
|
||||||
onView(withText("Advanced"))
|
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
||||||
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
hasDescendant(withText("Add-ons"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
onView(withText("Add-ons"))
|
||||||
|
.check(matches(isCompletelyDisplayed()))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assertAddons() {
|
private fun assertAddonsButton() {
|
||||||
scrollToElementByText("Add-ons")
|
onView(withId(R.id.recycler_view)).perform(
|
||||||
onView(withText("Add-ons"))
|
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
|
||||||
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
hasDescendant(withText("Add-ons"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
addonsManagerButton()
|
||||||
|
.check(matches(isCompletelyDisplayed()))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assertRemoteDebug() {
|
private fun assertRemoteDebug() {
|
||||||
|
@ -414,5 +434,7 @@ fun isPackageInstalled(packageName: String): Boolean {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun addonsManagerButton() = onView(withText(R.string.preferences_addons))
|
||||||
|
|
||||||
private fun goBackButton() =
|
private fun goBackButton() =
|
||||||
onView(CoreMatchers.allOf(ViewMatchers.withContentDescription("Navigate up")))
|
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
|
||||||
import androidx.test.uiautomator.Until.findObject
|
import androidx.test.uiautomator.Until.findObject
|
||||||
import org.hamcrest.CoreMatchers.allOf
|
import org.hamcrest.CoreMatchers.allOf
|
||||||
|
import org.hamcrest.CoreMatchers.anyOf
|
||||||
|
import org.hamcrest.CoreMatchers.containsString
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.helpers.TestAssetHelper
|
import org.mozilla.fenix.helpers.TestAssetHelper
|
||||||
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
|
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 closeTabButton() = onView(withId(R.id.mozac_browser_tabstray_close))
|
||||||
private fun assertCloseTabsButton(title: String) =
|
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)))
|
.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 privateBrowsingButton() = onView(withContentDescription("Private tabs"))
|
||||||
private fun newTabButton() = onView(withId(R.id.new_tab_button))
|
private fun newTabButton() = onView(withId(R.id.new_tab_button))
|
||||||
private fun threeDotMenu() = onView(withId(R.id.tab_tray_overflow))
|
private fun threeDotMenu() = onView(withId(R.id.tab_tray_overflow))
|
||||||
|
|
|
@ -347,6 +347,17 @@ class ThreeDotMenuMainRobot {
|
||||||
ThreeDotMenuMainRobot().interact()
|
ThreeDotMenuMainRobot().interact()
|
||||||
return ThreeDotMenuMainRobot.Transition()
|
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()
|
private fun assertaddNewCollectionButton() = addNewCollectionButton()
|
||||||
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
.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()
|
private fun assertCollectionNameTextField() = collectionNameTextField()
|
||||||
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||||
|
|
||||||
|
@ -473,6 +486,7 @@ private fun assertReaderViewAppearanceButton(visible: Boolean) = readerViewAppea
|
||||||
|
|
||||||
private fun addToFirefoxHomeButton() =
|
private fun addToFirefoxHomeButton() =
|
||||||
onView(allOf(withText(R.string.browser_menu_add_to_top_sites)))
|
onView(allOf(withText(R.string.browser_menu_add_to_top_sites)))
|
||||||
|
|
||||||
private fun assertAddToFirefoxHome() {
|
private fun assertAddToFirefoxHome() {
|
||||||
onView(withId(R.id.mozac_browser_menu_recyclerView))
|
onView(withId(R.id.mozac_browser_menu_recyclerView))
|
||||||
.perform(
|
.perform(
|
||||||
|
@ -514,3 +528,10 @@ private fun assertOpenInAppButton() {
|
||||||
)
|
)
|
||||||
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
).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:allowBackup="false"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/NormalTheme"
|
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
|
package org.mozilla.fenix
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -28,7 +28,6 @@ import androidx.navigation.NavDirections
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import androidx.navigation.ui.AppBarConfiguration
|
import androidx.navigation.ui.AppBarConfiguration
|
||||||
import androidx.navigation.ui.NavigationUI
|
import androidx.navigation.ui.NavigationUI
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import kotlinx.android.synthetic.main.activity_home.*
|
import kotlinx.android.synthetic.main.activity_home.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
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.SessionState
|
||||||
import mozilla.components.browser.state.state.WebExtensionState
|
import mozilla.components.browser.state.state.WebExtensionState
|
||||||
import mozilla.components.browser.state.store.BrowserStore
|
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.engine.EngineView
|
||||||
import mozilla.components.concept.tabstray.TabsTray
|
|
||||||
import mozilla.components.feature.contextmenu.DefaultSelectionActionDelegate
|
import mozilla.components.feature.contextmenu.DefaultSelectionActionDelegate
|
||||||
import mozilla.components.feature.search.BrowserStoreSearchAdapter
|
import mozilla.components.feature.search.BrowserStoreSearchAdapter
|
||||||
import mozilla.components.feature.search.SearchAdapter
|
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.settings.search.EditCustomSearchEngineFragmentDirections
|
||||||
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections
|
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections
|
||||||
import org.mozilla.fenix.sync.SyncedTabsFragmentDirections
|
import org.mozilla.fenix.sync.SyncedTabsFragmentDirections
|
||||||
import org.mozilla.fenix.tabtray.FenixTabsAdapter
|
|
||||||
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
|
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
|
||||||
import org.mozilla.fenix.theme.DefaultThemeManager
|
import org.mozilla.fenix.theme.DefaultThemeManager
|
||||||
import org.mozilla.fenix.theme.ThemeManager
|
import org.mozilla.fenix.theme.ThemeManager
|
||||||
|
@ -315,17 +310,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
|
||||||
actionSorter = ::actionSorter
|
actionSorter = ::actionSorter
|
||||||
)
|
)
|
||||||
}.asView()
|
}.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)
|
else -> super.onCreateView(parent, name, context, attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import android.view.Gravity
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.accessibility.AccessibilityManager
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.net.toUri
|
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.DefaultBrowserToolbarController
|
||||||
import org.mozilla.fenix.components.toolbar.SwipeRefreshScrollingViewBehavior
|
import org.mozilla.fenix.components.toolbar.SwipeRefreshScrollingViewBehavior
|
||||||
import org.mozilla.fenix.components.toolbar.ToolbarIntegration
|
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.DownloadService
|
||||||
import org.mozilla.fenix.downloads.DynamicDownloadDialog
|
import org.mozilla.fenix.downloads.DynamicDownloadDialog
|
||||||
|
import org.mozilla.fenix.ext.accessibilityManager
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.enterToImmersiveMode
|
import org.mozilla.fenix.ext.enterToImmersiveMode
|
||||||
import org.mozilla.fenix.ext.hideToolbar
|
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.sessionsOfType
|
||||||
import org.mozilla.fenix.ext.settings
|
import org.mozilla.fenix.ext.settings
|
||||||
import org.mozilla.fenix.home.SharedViewModel
|
import org.mozilla.fenix.home.SharedViewModel
|
||||||
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
|
|
||||||
import org.mozilla.fenix.theme.ThemeManager
|
import org.mozilla.fenix.theme.ThemeManager
|
||||||
import org.mozilla.fenix.utils.allowUndo
|
import org.mozilla.fenix.utils.allowUndo
|
||||||
import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration
|
import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration
|
||||||
|
@ -118,7 +120,7 @@ import java.lang.ref.WeakReference
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
@Suppress("TooManyFunctions", "LargeClass")
|
@Suppress("TooManyFunctions", "LargeClass")
|
||||||
abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, SessionManager.Observer,
|
abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, SessionManager.Observer,
|
||||||
OnBackLongPressedListener {
|
OnBackLongPressedListener, AccessibilityManager.AccessibilityStateChangeListener {
|
||||||
private lateinit var browserFragmentStore: BrowserFragmentStore
|
private lateinit var browserFragmentStore: BrowserFragmentStore
|
||||||
private lateinit var browserAnimator: BrowserAnimator
|
private lateinit var browserAnimator: BrowserAnimator
|
||||||
|
|
||||||
|
@ -228,7 +230,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
|
||||||
tabCollectionStorage = requireComponents.core.tabCollectionStorage,
|
tabCollectionStorage = requireComponents.core.tabCollectionStorage,
|
||||||
topSiteStorage = requireComponents.core.topSiteStorage,
|
topSiteStorage = requireComponents.core.topSiteStorage,
|
||||||
onTabCounterClicked = {
|
onTabCounterClicked = {
|
||||||
TabTrayDialogFragment.show(parentFragmentManager)
|
findNavController().nav(
|
||||||
|
R.id.browserFragment,
|
||||||
|
BrowserFragmentDirections.actionGlobalTabTrayDialogFragment()
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onCloseTab = {
|
onCloseTab = {
|
||||||
val snapshot = sessionManager.createSessionSnapshot(it)
|
val snapshot = sessionManager.createSessionSnapshot(it)
|
||||||
|
@ -243,7 +248,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
|
||||||
}
|
}
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.allowUndo(
|
viewLifecycleOwner.lifecycleScope.allowUndo(
|
||||||
requireView(),
|
requireView().browserLayout,
|
||||||
snackbarMessage,
|
snackbarMessage,
|
||||||
requireContext().getString(R.string.snackbar_deleted_undo),
|
requireContext().getString(R.string.snackbar_deleted_undo),
|
||||||
{
|
{
|
||||||
|
@ -264,7 +269,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
|
||||||
|
|
||||||
_browserToolbarView = BrowserToolbarView(
|
_browserToolbarView = BrowserToolbarView(
|
||||||
container = view.browserLayout,
|
container = view.browserLayout,
|
||||||
shouldUseBottomToolbar = context.settings().shouldUseBottomToolbar,
|
toolbarPosition = context.settings().toolbarPosition,
|
||||||
interactor = browserInteractor,
|
interactor = browserInteractor,
|
||||||
customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) },
|
customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) },
|
||||||
lifecycleOwner = viewLifecycleOwner
|
lifecycleOwner = viewLifecycleOwner
|
||||||
|
@ -307,7 +312,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
|
||||||
feature = ContextMenuFeature(
|
feature = ContextMenuFeature(
|
||||||
fragmentManager = parentFragmentManager,
|
fragmentManager = parentFragmentManager,
|
||||||
store = store,
|
store = store,
|
||||||
candidates = getContextMenuCandidates(context, view),
|
candidates = getContextMenuCandidates(context, view.browserLayout),
|
||||||
engineView = view.engineView,
|
engineView = view.engineView,
|
||||||
useCases = context.components.useCases.contextMenuUseCases,
|
useCases = context.components.useCases.contextMenuUseCases,
|
||||||
tabId = customTabSessionId
|
tabId = customTabSessionId
|
||||||
|
@ -677,10 +682,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
|
||||||
private fun initializeEngineView(toolbarHeight: Int) {
|
private fun initializeEngineView(toolbarHeight: Int) {
|
||||||
engineView.setDynamicToolbarMaxHeight(toolbarHeight)
|
engineView.setDynamicToolbarMaxHeight(toolbarHeight)
|
||||||
|
|
||||||
val behavior = if (requireContext().settings().shouldUseBottomToolbar) {
|
val context = requireContext()
|
||||||
EngineViewBottomBehavior(context, null)
|
val behavior = when (context.settings().toolbarPosition) {
|
||||||
} else {
|
ToolbarPosition.BOTTOM -> EngineViewBottomBehavior(context, null)
|
||||||
SwipeRefreshScrollingViewBehavior(requireContext(), null, engineView, browserToolbarView)
|
ToolbarPosition.TOP -> SwipeRefreshScrollingViewBehavior(context, null, engineView, browserToolbarView)
|
||||||
}
|
}
|
||||||
|
|
||||||
(swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams).behavior = behavior
|
(swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams).behavior = behavior
|
||||||
|
@ -713,6 +718,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
|
||||||
super.onStart()
|
super.onStart()
|
||||||
requireComponents.core.sessionManager.register(this, this, autoPause = true)
|
requireComponents.core.sessionManager.register(this, this, autoPause = true)
|
||||||
sitePermissionWifiIntegration.get()?.maybeAddWifiConnectedListener()
|
sitePermissionWifiIntegration.get()?.maybeAddWifiConnectedListener()
|
||||||
|
requireContext().accessibilityManager.addAccessibilityStateChangeListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
|
@ -825,7 +831,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
|
||||||
if (session.hasParentSession) {
|
if (session.hasParentSession) {
|
||||||
sessionManager.remove(session, true)
|
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
|
!goToOverview
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -843,7 +851,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
|
||||||
* Returns the layout [android.view.Gravity] for the quick settings and ETP dialog.
|
* Returns the layout [android.view.Gravity] for the quick settings and ETP dialog.
|
||||||
*/
|
*/
|
||||||
protected fun getAppropriateLayoutGravity(): Int =
|
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.
|
* Updates the site permissions rules based on user settings.
|
||||||
|
@ -1011,6 +1019,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
_browserToolbarView = null
|
_browserToolbarView = null
|
||||||
_browserInteractor = null
|
_browserInteractor = null
|
||||||
|
requireContext().accessibilityManager.removeAccessibilityStateChangeListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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_PROMPT_PERMISSIONS = 2
|
||||||
private const val REQUEST_CODE_APP_PERMISSIONS = 3
|
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 kotlinx.coroutines.launch
|
||||||
import mozilla.components.concept.engine.EngineView
|
import mozilla.components.concept.engine.EngineView
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.components.toolbar.ToolbarPosition
|
||||||
import org.mozilla.fenix.ext.settings
|
import org.mozilla.fenix.ext.settings
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
@ -155,12 +156,15 @@ class BrowserAnimator(
|
||||||
fun getToolbarNavOptions(context: Context): NavOptions {
|
fun getToolbarNavOptions(context: Context): NavOptions {
|
||||||
val navOptions = NavOptions.Builder()
|
val navOptions = NavOptions.Builder()
|
||||||
|
|
||||||
if (!context.settings().shouldUseBottomToolbar) {
|
when (context.settings().toolbarPosition) {
|
||||||
navOptions.setEnterAnim(R.anim.fade_in)
|
ToolbarPosition.TOP -> {
|
||||||
navOptions.setExitAnim(R.anim.fade_out)
|
navOptions.setEnterAnim(R.anim.fade_in)
|
||||||
} else {
|
navOptions.setExitAnim(R.anim.fade_out)
|
||||||
navOptions.setEnterAnim(R.anim.fade_in_up)
|
}
|
||||||
navOptions.setExitAnim(R.anim.fade_out_down)
|
ToolbarPosition.BOTTOM -> {
|
||||||
|
navOptions.setEnterAnim(R.anim.fade_in_up)
|
||||||
|
navOptions.setExitAnim(R.anim.fade_out_down)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return navOptions.build()
|
return navOptions.build()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* 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
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
package org.mozilla.fenix.browser
|
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.components.metrics.MetricController
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.metrics
|
import org.mozilla.fenix.ext.metrics
|
||||||
|
import org.mozilla.fenix.ext.sessionsOfType
|
||||||
import org.mozilla.fenix.ext.settings
|
import org.mozilla.fenix.ext.settings
|
||||||
import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry
|
import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry
|
||||||
import org.mozilla.fenix.utils.Settings
|
import org.mozilla.fenix.utils.Settings
|
||||||
|
@ -44,25 +45,29 @@ class UriOpenedObserver(
|
||||||
session.register(singleSessionObserver, owner)
|
session.register(singleSessionObserver, owner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun saveOpenTabsCount() {
|
||||||
|
settings.setOpenTabsCount(sessionManager.sessionsOfType(private = false).count())
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAllSessionsRemoved() {
|
override fun onAllSessionsRemoved() {
|
||||||
settings.setOpenTabsCount(sessionManager.sessions.filter { !it.private }.size)
|
saveOpenTabsCount()
|
||||||
sessionManager.sessions.forEach {
|
sessionManager.sessions.forEach {
|
||||||
it.unregister(singleSessionObserver)
|
it.unregister(singleSessionObserver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSessionAdded(session: Session) {
|
override fun onSessionAdded(session: Session) {
|
||||||
settings.setOpenTabsCount(sessionManager.sessions.filter { !it.private }.size)
|
saveOpenTabsCount()
|
||||||
session.register(singleSessionObserver, owner)
|
session.register(singleSessionObserver, owner)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSessionRemoved(session: Session) {
|
override fun onSessionRemoved(session: Session) {
|
||||||
settings.setOpenTabsCount(sessionManager.sessions.filter { !it.private }.size)
|
saveOpenTabsCount()
|
||||||
session.unregister(singleSessionObserver)
|
session.unregister(singleSessionObserver)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSessionsRestored() {
|
override fun onSessionsRestored() {
|
||||||
settings.setOpenTabsCount(sessionManager.sessions.filter { !it.private }.size)
|
saveOpenTabsCount()
|
||||||
sessionManager.sessions.forEach {
|
sessionManager.sessions.forEach {
|
||||||
it.register(singleSessionObserver, owner)
|
it.register(singleSessionObserver, owner)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ import android.graphics.drawable.ColorDrawable
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.view.isGone
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.marginTop
|
import androidx.core.view.marginTop
|
||||||
import kotlinx.android.synthetic.main.search_widget_cfr.view.*
|
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.SearchWidgetCreator
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
import org.mozilla.fenix.components.metrics.MetricController
|
import org.mozilla.fenix.components.metrics.MetricController
|
||||||
|
import org.mozilla.fenix.components.toolbar.ToolbarPosition
|
||||||
import org.mozilla.fenix.utils.Settings
|
import org.mozilla.fenix.utils.Settings
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,18 +50,14 @@ class SearchWidgetCFR(
|
||||||
val searchWidgetCFRDialog = Dialog(context)
|
val searchWidgetCFRDialog = Dialog(context)
|
||||||
val layout = LayoutInflater.from(context)
|
val layout = LayoutInflater.from(context)
|
||||||
.inflate(R.layout.search_widget_cfr, null)
|
.inflate(R.layout.search_widget_cfr, null)
|
||||||
val isBottomToolbar = settings.shouldUseBottomToolbar
|
val toolbarPosition = settings.toolbarPosition
|
||||||
|
|
||||||
layout.drop_down_triangle.isGone = isBottomToolbar
|
layout.drop_down_triangle.isVisible = toolbarPosition == ToolbarPosition.TOP
|
||||||
layout.pop_up_triangle.isVisible = isBottomToolbar
|
layout.pop_up_triangle.isVisible = toolbarPosition == ToolbarPosition.BOTTOM
|
||||||
|
|
||||||
val toolbar = getToolbar()
|
val toolbar = getToolbar()
|
||||||
|
|
||||||
val gravity = if (isBottomToolbar) {
|
val gravity = Gravity.CENTER_HORIZONTAL or toolbarPosition.androidGravity
|
||||||
Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM
|
|
||||||
} else {
|
|
||||||
Gravity.CENTER_HORIZONTAL or Gravity.TOP
|
|
||||||
}
|
|
||||||
|
|
||||||
layout.cfr_neg_button.setOnClickListener {
|
layout.cfr_neg_button.setOnClickListener {
|
||||||
metrics.track(Event.SearchWidgetCFRNotNowPressed)
|
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.TabCollectionStorage
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
import org.mozilla.fenix.components.metrics.MetricController
|
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
|
import org.mozilla.fenix.home.Tab
|
||||||
|
|
||||||
interface CollectionCreationController {
|
interface CollectionCreationController {
|
||||||
|
@ -92,7 +94,7 @@ class DefaultCollectionCreationController(
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.track(
|
metrics.track(
|
||||||
Event.CollectionSaved(normalSessionSize(sessionManager), sessionBundle.size)
|
Event.CollectionSaved(sessionManager.normalSessionSize(), sessionBundle.size)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +136,7 @@ class DefaultCollectionCreationController(
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.track(
|
metrics.track(
|
||||||
Event.CollectionTabsAdded(normalSessionSize(sessionManager), sessionBundle.size)
|
Event.CollectionTabsAdded(sessionManager.normalSessionSize(), sessionBundle.size)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,7 +148,7 @@ class DefaultCollectionCreationController(
|
||||||
} else {
|
} else {
|
||||||
SaveCollectionStep.SelectCollection
|
SaveCollectionStep.SelectCollection
|
||||||
},
|
},
|
||||||
defaultCollectionNumber = getDefaultCollectionNumber()
|
defaultCollectionNumber = store.state.tabCollections.getDefaultCollectionNumber()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -155,26 +157,11 @@ class DefaultCollectionCreationController(
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
CollectionCreationAction.StepChanged(
|
CollectionCreationAction.StepChanged(
|
||||||
SaveCollectionStep.NameCollection,
|
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) {
|
override fun addTabToSelection(tab: Tab) {
|
||||||
store.dispatch(CollectionCreationAction.TabAdded(tab))
|
store.dispatch(CollectionCreationAction.TabAdded(tab))
|
||||||
}
|
}
|
||||||
|
@ -209,14 +196,4 @@ class DefaultCollectionCreationController(
|
||||||
SaveCollectionStep.SelectTabs, SaveCollectionStep.RenameCollection -> null
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import mozilla.components.browser.session.SessionManager
|
import mozilla.components.browser.state.selector.findTab
|
||||||
import mozilla.components.browser.state.store.BrowserStore
|
import mozilla.components.browser.state.state.BrowserState
|
||||||
|
import mozilla.components.browser.state.state.TabSessionState
|
||||||
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
||||||
import mozilla.components.lib.state.ext.consumeFrom
|
import mozilla.components.lib.state.ext.consumeFrom
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.components.StoreProvider
|
import org.mozilla.fenix.components.StoreProvider
|
||||||
|
import org.mozilla.fenix.ext.getMediaStateForSession
|
||||||
import org.mozilla.fenix.ext.requireComponents
|
import org.mozilla.fenix.ext.requireComponents
|
||||||
import org.mozilla.fenix.ext.toTab
|
import org.mozilla.fenix.ext.toShortUrl
|
||||||
import org.mozilla.fenix.home.Tab
|
import org.mozilla.fenix.home.Tab
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
|
@ -47,12 +49,11 @@ class CollectionCreationFragment : DialogFragment() {
|
||||||
val view = inflater.inflate(R.layout.fragment_create_collection, container, false)
|
val view = inflater.inflate(R.layout.fragment_create_collection, container, false)
|
||||||
val args: CollectionCreationFragmentArgs by navArgs()
|
val args: CollectionCreationFragmentArgs by navArgs()
|
||||||
|
|
||||||
val sessionManager = requireComponents.core.sessionManager
|
|
||||||
val store = requireComponents.core.store
|
val store = requireComponents.core.store
|
||||||
val publicSuffixList = requireComponents.publicSuffixList
|
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) {
|
val selectedTabs = if (args.selectedTabIds != null) {
|
||||||
sessionManager.getTabs(args.selectedTabIds, store, publicSuffixList).toSet()
|
store.state.getTabs(args.selectedTabIds, publicSuffixList).toSet()
|
||||||
} else {
|
} else {
|
||||||
if (tabs.size == 1) setOf(tabs.first()) else emptySet()
|
if (tabs.size == 1) setOf(tabs.first()) else emptySet()
|
||||||
}
|
}
|
||||||
|
@ -112,14 +113,30 @@ class CollectionCreationFragment : DialogFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
@VisibleForTesting
|
||||||
fun SessionManager.getTabs(
|
internal fun BrowserState.getTabs(
|
||||||
tabIds: Array<String>?,
|
tabIds: Array<String>?,
|
||||||
store: BrowserStore,
|
|
||||||
publicSuffixList: PublicSuffixList
|
publicSuffixList: PublicSuffixList
|
||||||
): List<Tab> {
|
): List<Tab> {
|
||||||
return tabIds
|
return tabIds
|
||||||
?.mapNotNull { this.findSessionById(it) }
|
?.mapNotNull { id -> findTab(id) }
|
||||||
?.map { it.toTab(store, publicSuffixList) }
|
?.map { it.toTab(this, publicSuffixList) }
|
||||||
?: emptyList()
|
.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.
|
* Diff callback for comparing tab lists with selected state.
|
||||||
*/
|
*/
|
||||||
@Suppress("LongParameterList")
|
|
||||||
internal class TabDiffUtil(
|
internal class TabDiffUtil(
|
||||||
private val old: List<Tab>,
|
private val old: List<Tab>,
|
||||||
private val new: List<Tab>,
|
private val new: List<Tab>,
|
||||||
|
|
|
@ -47,6 +47,7 @@ import org.mozilla.fenix.utils.Settings
|
||||||
* background worker.
|
* background worker.
|
||||||
*/
|
*/
|
||||||
@Mockable
|
@Mockable
|
||||||
|
@Suppress("LongParameterList")
|
||||||
class BackgroundServices(
|
class BackgroundServices(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val push: Push,
|
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.SecureAbove22Preferences
|
||||||
import mozilla.components.lib.dataprotect.generateEncryptionKey
|
import mozilla.components.lib.dataprotect.generateEncryptionKey
|
||||||
import mozilla.components.service.digitalassetlinks.RelationChecker
|
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 mozilla.components.service.sync.logins.SyncableLoginsStorage
|
||||||
import org.mozilla.fenix.AppRequestInterceptor
|
import org.mozilla.fenix.AppRequestInterceptor
|
||||||
import org.mozilla.fenix.BuildConfig.DIGITAL_ASSET_LINKS_TOKEN
|
|
||||||
import org.mozilla.fenix.Config
|
import org.mozilla.fenix.Config
|
||||||
import org.mozilla.fenix.HomeActivity
|
import org.mozilla.fenix.HomeActivity
|
||||||
import org.mozilla.fenix.R
|
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.
|
* The [RelationChecker] checks Digital Asset Links relationships for Trusted Web Activities.
|
||||||
*/
|
*/
|
||||||
val relationChecker: RelationChecker by lazy {
|
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
|
package org.mozilla.fenix.components
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
|
|
@ -25,6 +25,7 @@ import org.mozilla.fenix.utils.Mockable
|
||||||
* Component group for miscellaneous components.
|
* Component group for miscellaneous components.
|
||||||
*/
|
*/
|
||||||
@Mockable
|
@Mockable
|
||||||
|
@Suppress("LongParameterList")
|
||||||
class IntentProcessors(
|
class IntentProcessors(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val sessionManager: SessionManager,
|
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
|
package org.mozilla.fenix.components
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* 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
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
package org.mozilla.fenix.components
|
package org.mozilla.fenix.components
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ import org.mozilla.fenix.utils.Mockable
|
||||||
* modules and can be triggered by UI interactions.
|
* modules and can be triggered by UI interactions.
|
||||||
*/
|
*/
|
||||||
@Mockable
|
@Mockable
|
||||||
|
@Suppress("LongParameterList")
|
||||||
class UseCases(
|
class UseCases(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val engine: Engine,
|
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.TrackingProtection
|
||||||
import org.mozilla.fenix.GleanMetrics.UserSpecifiedSearchEngines
|
import org.mozilla.fenix.GleanMetrics.UserSpecifiedSearchEngines
|
||||||
import org.mozilla.fenix.GleanMetrics.VoiceSearch
|
import org.mozilla.fenix.GleanMetrics.VoiceSearch
|
||||||
|
import org.mozilla.fenix.components.toolbar.ToolbarPosition
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.settings
|
import org.mozilla.fenix.ext.settings
|
||||||
import org.mozilla.fenix.utils.BrowsersCache
|
import org.mozilla.fenix.utils.BrowsersCache
|
||||||
|
@ -724,10 +725,9 @@ class GleanMetricsService(private val context: Context) : MetricsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
toolbarPosition.set(
|
toolbarPosition.set(
|
||||||
if (context.settings().shouldUseBottomToolbar) {
|
when (context.settings().toolbarPosition) {
|
||||||
Event.ToolbarPositionChanged.Position.BOTTOM.name
|
ToolbarPosition.BOTTOM -> Event.ToolbarPositionChanged.Position.BOTTOM.name
|
||||||
} else {
|
ToolbarPosition.TOP -> Event.ToolbarPositionChanged.Position.TOP.name
|
||||||
Event.ToolbarPositionChanged.Position.TOP.name
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,47 +4,38 @@
|
||||||
|
|
||||||
package org.mozilla.fenix.components.toolbar
|
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.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.PopupWindow
|
|
||||||
import androidx.annotation.LayoutRes
|
import androidx.annotation.LayoutRes
|
||||||
import androidx.annotation.VisibleForTesting
|
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
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_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_EXIT_UNTIL_COLLAPSED
|
||||||
import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL
|
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.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import kotlinx.android.extensions.LayoutContainer
|
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.*
|
||||||
import kotlinx.android.synthetic.main.component_browser_top_toolbar.view.*
|
import kotlinx.android.synthetic.main.component_browser_top_toolbar.view.*
|
||||||
import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
|
import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
|
||||||
import mozilla.components.browser.session.Session
|
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.BrowserToolbar
|
||||||
import mozilla.components.browser.toolbar.behavior.BrowserToolbarBottomBehavior
|
import mozilla.components.browser.toolbar.behavior.BrowserToolbarBottomBehavior
|
||||||
import mozilla.components.browser.toolbar.display.DisplayToolbar
|
import mozilla.components.browser.toolbar.display.DisplayToolbar
|
||||||
import mozilla.components.support.ktx.android.util.dpToFloat
|
import mozilla.components.support.ktx.android.util.dpToFloat
|
||||||
import mozilla.components.support.utils.URLStringUtils
|
import mozilla.components.support.utils.URLStringUtils
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.components.FenixSnackbar
|
|
||||||
import org.mozilla.fenix.customtabs.CustomTabToolbarIntegration
|
import org.mozilla.fenix.customtabs.CustomTabToolbarIntegration
|
||||||
import org.mozilla.fenix.customtabs.CustomTabToolbarMenu
|
import org.mozilla.fenix.customtabs.CustomTabToolbarMenu
|
||||||
import org.mozilla.fenix.ext.bookmarkStorage
|
import org.mozilla.fenix.ext.bookmarkStorage
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.settings
|
import org.mozilla.fenix.ext.settings
|
||||||
import org.mozilla.fenix.theme.ThemeManager
|
import org.mozilla.fenix.theme.ThemeManager
|
||||||
|
import org.mozilla.fenix.utils.ToolbarPopupWindow
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
interface BrowserToolbarViewInteractor {
|
interface BrowserToolbarViewInteractor {
|
||||||
fun onBrowserToolbarPaste(text: String)
|
fun onBrowserToolbarPaste(text: String)
|
||||||
|
@ -56,10 +47,11 @@ interface BrowserToolbarViewInteractor {
|
||||||
fun onScrolled(offset: Int)
|
fun onScrolled(offset: Int)
|
||||||
fun onReaderModePressed(enabled: Boolean)
|
fun onReaderModePressed(enabled: Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("LargeClass")
|
@SuppressWarnings("LargeClass")
|
||||||
class BrowserToolbarView(
|
class BrowserToolbarView(
|
||||||
private val container: ViewGroup,
|
private val container: ViewGroup,
|
||||||
private val shouldUseBottomToolbar: Boolean,
|
private val toolbarPosition: ToolbarPosition,
|
||||||
private val interactor: BrowserToolbarViewInteractor,
|
private val interactor: BrowserToolbarViewInteractor,
|
||||||
private val customTabSession: Session?,
|
private val customTabSession: Session?,
|
||||||
private val lifecycleOwner: LifecycleOwner
|
private val lifecycleOwner: LifecycleOwner
|
||||||
|
@ -71,9 +63,9 @@ class BrowserToolbarView(
|
||||||
private val settings = container.context.settings()
|
private val settings = container.context.settings()
|
||||||
|
|
||||||
@LayoutRes
|
@LayoutRes
|
||||||
private val toolbarLayout = when {
|
private val toolbarLayout = when (settings.toolbarPosition) {
|
||||||
settings.shouldUseBottomToolbar -> R.layout.component_bottom_browser_toolbar
|
ToolbarPosition.BOTTOM -> R.layout.component_bottom_browser_toolbar
|
||||||
else -> R.layout.component_browser_top_toolbar
|
ToolbarPosition.TOP -> R.layout.component_browser_top_toolbar
|
||||||
}
|
}
|
||||||
|
|
||||||
private val layout = LayoutInflater.from(container.context)
|
private val layout = LayoutInflater.from(container.context)
|
||||||
|
@ -88,63 +80,19 @@ class BrowserToolbarView(
|
||||||
val isCustomTabSession = customTabSession != null
|
val isCustomTabSession = customTabSession != null
|
||||||
|
|
||||||
view.display.setOnUrlLongClickListener {
|
view.display.setOnUrlLongClickListener {
|
||||||
val clipboard = view.context.components.clipboardHandler
|
ToolbarPopupWindow.show(
|
||||||
val customView = LayoutInflater.from(view.context)
|
WeakReference(view),
|
||||||
.inflate(R.layout.browser_toolbar_popup_window, null)
|
customTabSession,
|
||||||
val popupWindow = PopupWindow(
|
interactor::onBrowserToolbarPasteAndGo,
|
||||||
customView,
|
interactor::onBrowserToolbarPaste
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
|
||||||
view.context.resources.getDimensionPixelSize(R.dimen.context_menu_height),
|
|
||||||
true
|
|
||||||
)
|
)
|
||||||
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
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
with(container.context) {
|
with(container.context) {
|
||||||
val sessionManager = components.core.sessionManager
|
val sessionManager = components.core.sessionManager
|
||||||
|
|
||||||
if (!shouldUseBottomToolbar) {
|
if (toolbarPosition == ToolbarPosition.TOP) {
|
||||||
val offsetChangedListener =
|
val offsetChangedListener =
|
||||||
AppBarLayout.OnOffsetChangedListener { _: AppBarLayout?, verticalOffset: Int ->
|
AppBarLayout.OnOffsetChangedListener { _: AppBarLayout?, verticalOffset: Int ->
|
||||||
interactor.onScrolled(verticalOffset)
|
interactor.onScrolled(verticalOffset)
|
||||||
|
@ -167,10 +115,9 @@ class BrowserToolbarView(
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
display.progressGravity = if (shouldUseBottomToolbar) {
|
display.progressGravity = when (toolbarPosition) {
|
||||||
DisplayToolbar.Gravity.TOP
|
ToolbarPosition.BOTTOM -> DisplayToolbar.Gravity.TOP
|
||||||
} else {
|
ToolbarPosition.TOP -> DisplayToolbar.Gravity.BOTTOM
|
||||||
DisplayToolbar.Gravity.BOTTOM
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val primaryTextColor = ContextCompat.getColor(
|
val primaryTextColor = ContextCompat.getColor(
|
||||||
|
@ -207,7 +154,7 @@ class BrowserToolbarView(
|
||||||
this,
|
this,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
customTabSession?.id,
|
customTabSession?.id,
|
||||||
shouldReverseItems = !shouldUseBottomToolbar,
|
shouldReverseItems = toolbarPosition == ToolbarPosition.TOP,
|
||||||
onItemTapped = {
|
onItemTapped = {
|
||||||
interactor.onBrowserToolbarMenuItemTapped(it)
|
interactor.onBrowserToolbarMenuItemTapped(it)
|
||||||
}
|
}
|
||||||
|
@ -216,7 +163,7 @@ class BrowserToolbarView(
|
||||||
menuToolbar = DefaultToolbarMenu(
|
menuToolbar = DefaultToolbarMenu(
|
||||||
context = this,
|
context = this,
|
||||||
hasAccountProblem = components.backgroundServices.accountManager.accountNeedsReauth(),
|
hasAccountProblem = components.backgroundServices.accountManager.accountNeedsReauth(),
|
||||||
shouldReverseItems = !shouldUseBottomToolbar,
|
shouldReverseItems = toolbarPosition == ToolbarPosition.TOP,
|
||||||
onItemTapped = { interactor.onBrowserToolbarMenuItemTapped(it) },
|
onItemTapped = { interactor.onBrowserToolbarMenuItemTapped(it) },
|
||||||
lifecycleOwner = lifecycleOwner,
|
lifecycleOwner = lifecycleOwner,
|
||||||
sessionManager = sessionManager,
|
sessionManager = sessionManager,
|
||||||
|
@ -243,7 +190,7 @@ class BrowserToolbarView(
|
||||||
menuToolbar,
|
menuToolbar,
|
||||||
ShippedDomainsProvider().also { it.initialize(this) },
|
ShippedDomainsProvider().also { it.initialize(this) },
|
||||||
components.core.historyStorage,
|
components.core.historyStorage,
|
||||||
components.core.sessionManager,
|
lifecycleOwner,
|
||||||
sessionId = null,
|
sessionId = null,
|
||||||
isPrivate = sessionManager.selectedSession?.private ?: false,
|
isPrivate = sessionManager.selectedSession?.private ?: false,
|
||||||
interactor = interactor,
|
interactor = interactor,
|
||||||
|
@ -254,12 +201,15 @@ class BrowserToolbarView(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun expand() {
|
fun expand() {
|
||||||
if (settings.shouldUseBottomToolbar) {
|
when (settings.toolbarPosition) {
|
||||||
(view.layoutParams as CoordinatorLayout.LayoutParams).apply {
|
ToolbarPosition.BOTTOM -> {
|
||||||
(behavior as BrowserToolbarBottomBehavior).forceExpand(view)
|
(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.
|
* Note that the bottom toolbar has a feature flag for being dynamic, so it may not get flags set.
|
||||||
*/
|
*/
|
||||||
fun setScrollFlags(shouldDisableScroll: Boolean = false) {
|
fun setScrollFlags(shouldDisableScroll: Boolean = false) {
|
||||||
if (view.context.settings().shouldUseBottomToolbar) {
|
when (settings.toolbarPosition) {
|
||||||
if (view.layoutParams is CoordinatorLayout.LayoutParams) {
|
ToolbarPosition.BOTTOM -> {
|
||||||
(view.layoutParams as CoordinatorLayout.LayoutParams).apply {
|
(view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply {
|
||||||
behavior = BrowserToolbarBottomBehavior(view.context, null)
|
behavior = BrowserToolbarBottomBehavior(view.context, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ToolbarPosition.TOP -> {
|
||||||
return
|
view.updateLayoutParams<AppBarLayout.LayoutParams> {
|
||||||
}
|
scrollFlags = if (settings.shouldUseFixedTopToolbar || shouldDisableScroll) {
|
||||||
|
// Force expand the toolbar so the user is not stuck with a hidden toolbar
|
||||||
val params = view.layoutParams as AppBarLayout.LayoutParams
|
expand()
|
||||||
|
0
|
||||||
params.scrollFlags = when (view.context.settings().shouldUseFixedTopToolbar || shouldDisableScroll) {
|
} else {
|
||||||
true -> {
|
SCROLL_FLAG_SCROLL or
|
||||||
// Force expand the toolbar so the user is not stuck with a hidden toolbar
|
SCROLL_FLAG_ENTER_ALWAYS or
|
||||||
expand()
|
SCROLL_FLAG_SNAP or
|
||||||
0
|
SCROLL_FLAG_EXIT_UNTIL_COLLAPSED
|
||||||
}
|
}
|
||||||
false -> {
|
}
|
||||||
SCROLL_FLAG_SCROLL or SCROLL_FLAG_ENTER_ALWAYS or SCROLL_FLAG_SNAP or SCROLL_FLAG_EXIT_UNTIL_COLLAPSED
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
view.layoutParams = params
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TOOLBAR_ELEVATION = 16
|
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 lifecycleOwner View lifecycle owner used to determine when to cancel UI jobs.
|
||||||
* @param bookmarksStorage Used to check if a page is bookmarked.
|
* @param bookmarksStorage Used to check if a page is bookmarked.
|
||||||
*/
|
*/
|
||||||
@Suppress("LargeClass")
|
@Suppress("LargeClass", "LongParameterList")
|
||||||
class DefaultToolbarMenu(
|
class DefaultToolbarMenu(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val sessionManager: SessionManager,
|
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
|
package org.mozilla.fenix.components.toolbar
|
||||||
|
|
||||||
sealed class TabCounterMenuItem {
|
sealed class TabCounterMenuItem {
|
||||||
|
|
|
@ -8,17 +8,21 @@ import android.content.Context
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
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.BrowserMenu
|
||||||
import mozilla.components.browser.menu.BrowserMenuBuilder
|
import mozilla.components.browser.menu.BrowserMenuBuilder
|
||||||
import mozilla.components.browser.menu.item.BrowserMenuDivider
|
import mozilla.components.browser.menu.item.BrowserMenuDivider
|
||||||
import mozilla.components.browser.menu.item.BrowserMenuImageText
|
import mozilla.components.browser.menu.item.BrowserMenuImageText
|
||||||
import mozilla.components.browser.session.Session
|
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
|
||||||
import mozilla.components.browser.session.SessionManager
|
|
||||||
import mozilla.components.concept.toolbar.Toolbar
|
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.R
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.sessionsOfType
|
|
||||||
import org.mozilla.fenix.ext.settings
|
import org.mozilla.fenix.ext.settings
|
||||||
import org.mozilla.fenix.theme.ThemeManager
|
import org.mozilla.fenix.theme.ThemeManager
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
@ -26,8 +30,9 @@ import java.lang.ref.WeakReference
|
||||||
/**
|
/**
|
||||||
* A [Toolbar.Action] implementation that shows a [TabCounter].
|
* A [Toolbar.Action] implementation that shows a [TabCounter].
|
||||||
*/
|
*/
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class TabCounterToolbarButton(
|
class TabCounterToolbarButton(
|
||||||
private val sessionManager: SessionManager,
|
private val lifecycleOwner: LifecycleOwner,
|
||||||
private val isPrivate: Boolean,
|
private val isPrivate: Boolean,
|
||||||
private val onItemTapped: (TabCounterMenuItem) -> Unit = {},
|
private val onItemTapped: (TabCounterMenuItem) -> Unit = {},
|
||||||
private val showTabs: () -> Unit
|
private val showTabs: () -> Unit
|
||||||
|
@ -35,7 +40,11 @@ class TabCounterToolbarButton(
|
||||||
private var reference: WeakReference<TabCounter> = WeakReference<TabCounter>(null)
|
private var reference: WeakReference<TabCounter> = WeakReference<TabCounter>(null)
|
||||||
|
|
||||||
override fun createView(parent: ViewGroup): View {
|
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 {
|
val view = TabCounter(parent.context).apply {
|
||||||
reference = WeakReference(this)
|
reference = WeakReference(this)
|
||||||
|
@ -50,10 +59,11 @@ class TabCounterToolbarButton(
|
||||||
|
|
||||||
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
|
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
|
||||||
override fun onViewAttachedToWindow(v: View?) {
|
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
|
override fun bind(view: View) = Unit
|
||||||
|
|
||||||
private fun updateCount() {
|
private fun updateCount(count: Int) {
|
||||||
val count = sessionManager.sessionsOfType(private = isPrivate).count()
|
reference.get()?.setCountWithAnimation(count)
|
||||||
|
|
||||||
reference.get()?.let {
|
|
||||||
it.setCountWithAnimation(count)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getTabContextMenu(context: Context): BrowserMenu {
|
private fun getTabContextMenu(context: Context): BrowserMenu {
|
||||||
|
@ -113,29 +119,10 @@ class TabCounterToolbarButton(
|
||||||
)
|
)
|
||||||
|
|
||||||
return BrowserMenuBuilder(
|
return BrowserMenuBuilder(
|
||||||
if (context.settings().shouldUseBottomToolbar) {
|
when (context.settings().toolbarPosition) {
|
||||||
menuItems.reversed()
|
ToolbarPosition.BOTTOM -> menuItems.reversed()
|
||||||
} else {
|
ToolbarPosition.TOP -> menuItems
|
||||||
menuItems
|
|
||||||
}
|
}
|
||||||
).build(context)
|
).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 android.content.Context
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import com.airbnb.lottie.LottieCompositionFactory
|
import com.airbnb.lottie.LottieCompositionFactory
|
||||||
import com.airbnb.lottie.LottieDrawable
|
import com.airbnb.lottie.LottieDrawable
|
||||||
import mozilla.components.browser.domains.autocomplete.DomainAutocompleteProvider
|
import mozilla.components.browser.domains.autocomplete.DomainAutocompleteProvider
|
||||||
import mozilla.components.browser.session.SessionManager
|
|
||||||
import mozilla.components.browser.toolbar.BrowserToolbar
|
import mozilla.components.browser.toolbar.BrowserToolbar
|
||||||
import mozilla.components.browser.toolbar.display.DisplayToolbar
|
import mozilla.components.browser.toolbar.display.DisplayToolbar
|
||||||
import mozilla.components.concept.engine.Engine
|
import mozilla.components.concept.engine.Engine
|
||||||
|
@ -74,7 +74,7 @@ class DefaultToolbarIntegration(
|
||||||
toolbarMenu: ToolbarMenu,
|
toolbarMenu: ToolbarMenu,
|
||||||
domainAutocompleteProvider: DomainAutocompleteProvider,
|
domainAutocompleteProvider: DomainAutocompleteProvider,
|
||||||
historyStorage: HistoryStorage,
|
historyStorage: HistoryStorage,
|
||||||
sessionManager: SessionManager,
|
lifecycleOwner: LifecycleOwner,
|
||||||
sessionId: String? = null,
|
sessionId: String? = null,
|
||||||
isPrivate: Boolean,
|
isPrivate: Boolean,
|
||||||
interactor: BrowserToolbarViewInteractor,
|
interactor: BrowserToolbarViewInteractor,
|
||||||
|
@ -135,10 +135,11 @@ class DefaultToolbarIntegration(
|
||||||
val onTabCounterMenuItemTapped = { item: TabCounterMenuItem ->
|
val onTabCounterMenuItemTapped = { item: TabCounterMenuItem ->
|
||||||
interactor.onTabCounterMenuItemTapped(item)
|
interactor.onTabCounterMenuItemTapped(item)
|
||||||
}
|
}
|
||||||
val tabsAction = TabCounterToolbarButton(sessionManager, isPrivate, onTabCounterMenuItemTapped) {
|
val tabsAction =
|
||||||
toolbar.hideKeyboard()
|
TabCounterToolbarButton(lifecycleOwner, isPrivate, onTabCounterMenuItemTapped) {
|
||||||
interactor.onTabCounterClicked()
|
toolbar.hideKeyboard()
|
||||||
}
|
interactor.onTabCounterClicked()
|
||||||
|
}
|
||||||
toolbar.addBrowserAction(tabsAction)
|
toolbar.addBrowserAction(tabsAction)
|
||||||
|
|
||||||
val engineForSpeculativeConnects = if (!isPrivate) engine else null
|
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
|
* [DynamicDownloadDialog] is used to show a view in the current tab to the user, triggered when
|
||||||
* downloadFeature.onDownloadStopped gets invoked. It uses [DynamicDownloadDialogBehavior] to
|
* downloadFeature.onDownloadStopped gets invoked. It uses [DynamicDownloadDialogBehavior] to
|
||||||
* hide when the users scrolls through a website as to not impede his activities.
|
* hide when the users scrolls through a website as to not impede his activities.
|
||||||
* */
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
class DynamicDownloadDialog(
|
class DynamicDownloadDialog(
|
||||||
private val container: ViewGroup,
|
private val container: ViewGroup,
|
||||||
private val downloadState: DownloadState?,
|
private val downloadState: DownloadState?,
|
||||||
|
|
|
@ -7,8 +7,6 @@ package org.mozilla.fenix.ext
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowManager
|
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.
|
* 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_FULLSCREEN
|
||||||
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
|
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.ContextThemeWrapper
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.accessibility.AccessibilityManager
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import mozilla.components.browser.search.SearchEngineManager
|
import mozilla.components.browser.search.SearchEngineManager
|
||||||
import mozilla.components.support.locale.LocaleManager
|
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)
|
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) =
|
fun SessionManager.sessionsOfType(private: Boolean) =
|
||||||
sessions.asSequence().filter { it.private == private }
|
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
|
package org.mozilla.fenix.ext
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Patterns
|
import android.util.Patterns
|
||||||
import android.webkit.URLUtil
|
import android.webkit.URLUtil
|
||||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
@ -94,8 +92,8 @@ private fun Uri.isIpv6(): Boolean {
|
||||||
/**
|
/**
|
||||||
* Trim a host's prefix and suffix
|
* Trim a host's prefix and suffix
|
||||||
*/
|
*/
|
||||||
fun String.urlToTrimmedHost(context: Context): String = runBlocking {
|
fun String.urlToTrimmedHost(publicSuffixList: PublicSuffixList): String = runBlocking {
|
||||||
urlToTrimmedHost(context.components.publicSuffixList).await()
|
urlToTrimmedHost(publicSuffixList).await()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -115,16 +113,6 @@ fun String.simplifiedUrl(): String {
|
||||||
return afterScheme
|
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) {
|
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,
|
// 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').
|
// 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 androidx.core.content.ContextCompat
|
||||||
import mozilla.components.feature.tab.collections.TabCollection
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.collections.DefaultCollectionCreationController
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,3 +23,17 @@ fun TabCollection.getIconColor(context: Context): Int {
|
||||||
iconColors.recycle()
|
iconColors.recycle()
|
||||||
return color
|
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.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.android.synthetic.main.fragment_home.*
|
import kotlinx.android.synthetic.main.fragment_home.*
|
||||||
import kotlinx.android.synthetic.main.fragment_home.view.*
|
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.menu.view.MenuButton
|
||||||
import mozilla.components.browser.session.Session
|
import mozilla.components.browser.session.Session
|
||||||
import mozilla.components.browser.session.SessionManager
|
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.normalTabs
|
||||||
import mozilla.components.browser.state.selector.privateTabs
|
import mozilla.components.browser.state.selector.privateTabs
|
||||||
import mozilla.components.browser.state.state.BrowserState
|
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.AccountObserver
|
||||||
import mozilla.components.concept.sync.AuthType
|
import mozilla.components.concept.sync.AuthType
|
||||||
import mozilla.components.concept.sync.OAuthAccount
|
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.metrics.Event
|
||||||
import org.mozilla.fenix.components.tips.FenixTipManager
|
import org.mozilla.fenix.components.tips.FenixTipManager
|
||||||
import org.mozilla.fenix.components.tips.providers.MigrationTipProvider
|
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.components
|
||||||
import org.mozilla.fenix.ext.hideToolbar
|
import org.mozilla.fenix.ext.hideToolbar
|
||||||
import org.mozilla.fenix.ext.metrics
|
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.resetPoliciesAfter
|
||||||
import org.mozilla.fenix.ext.sessionsOfType
|
import org.mozilla.fenix.ext.sessionsOfType
|
||||||
import org.mozilla.fenix.ext.settings
|
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.DefaultSessionControlController
|
||||||
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
|
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
|
||||||
import org.mozilla.fenix.home.sessioncontrol.SessionControlView
|
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
|
||||||
import org.mozilla.fenix.settings.SupportUtils.SumoTopic.HELP
|
import org.mozilla.fenix.settings.SupportUtils.SumoTopic.HELP
|
||||||
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
|
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
|
||||||
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
|
|
||||||
import org.mozilla.fenix.theme.ThemeManager
|
import org.mozilla.fenix.theme.ThemeManager
|
||||||
import org.mozilla.fenix.utils.FragmentPreDrawManager
|
import org.mozilla.fenix.utils.FragmentPreDrawManager
|
||||||
|
import org.mozilla.fenix.utils.ToolbarPopupWindow
|
||||||
import org.mozilla.fenix.utils.allowUndo
|
import org.mozilla.fenix.utils.allowUndo
|
||||||
import org.mozilla.fenix.whatsnew.WhatsNew
|
import org.mozilla.fenix.whatsnew.WhatsNew
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
|
@ -120,16 +120,12 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private val snackbarAnchorView: View?
|
private val snackbarAnchorView: View?
|
||||||
get() {
|
get() = when (requireContext().settings().toolbarPosition) {
|
||||||
return if (requireContext().settings().shouldUseBottomToolbar) {
|
ToolbarPosition.BOTTOM -> toolbarLayout
|
||||||
toolbarLayout
|
ToolbarPosition.TOP -> null
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val browsingModeManager get() = (activity as HomeActivity).browsingModeManager
|
private val browsingModeManager get() = (activity as HomeActivity).browsingModeManager
|
||||||
private var homeAppBarOffset = 0
|
|
||||||
|
|
||||||
private val collectionStorageObserver = object : TabCollectionStorage.Observer {
|
private val collectionStorageObserver = object : TabCollectionStorage.Observer {
|
||||||
override fun onCollectionCreated(title: String, sessions: List<Session>) {
|
override fun onCollectionCreated(title: String, sessions: List<Session>) {
|
||||||
|
@ -147,8 +143,9 @@ class HomeFragment : Fragment() {
|
||||||
|
|
||||||
private val sessionManager: SessionManager
|
private val sessionManager: SessionManager
|
||||||
get() = requireComponents.core.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 val onboarding by lazy { FenixOnboarding(requireContext()) }
|
||||||
private lateinit var homeFragmentStore: HomeFragmentStore
|
private lateinit var homeFragmentStore: HomeFragmentStore
|
||||||
private var _sessionControlInteractor: SessionControlInteractor? = null
|
private var _sessionControlInteractor: SessionControlInteractor? = null
|
||||||
|
@ -218,7 +215,6 @@ class HomeFragment : Fragment() {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
updateLayout(view)
|
updateLayout(view)
|
||||||
setOffset(view)
|
|
||||||
sessionControlView = SessionControlView(
|
sessionControlView = SessionControlView(
|
||||||
view.sessionControlRecyclerView,
|
view.sessionControlRecyclerView,
|
||||||
sessionControlInteractor,
|
sessionControlInteractor,
|
||||||
|
@ -253,45 +249,36 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateLayout(view: View) {
|
private fun updateLayout(view: View) {
|
||||||
val shouldUseBottomToolbar = view.context.settings().shouldUseBottomToolbar
|
when (view.context.settings().toolbarPosition) {
|
||||||
|
ToolbarPosition.TOP -> {
|
||||||
if (!shouldUseBottomToolbar) {
|
view.toolbarLayout.layoutParams = CoordinatorLayout.LayoutParams(
|
||||||
view.toolbarLayout.layoutParams = CoordinatorLayout.LayoutParams(
|
ConstraintLayout.LayoutParams.MATCH_PARENT,
|
||||||
ConstraintLayout.LayoutParams.MATCH_PARENT,
|
ConstraintLayout.LayoutParams.WRAP_CONTENT
|
||||||
ConstraintLayout.LayoutParams.WRAP_CONTENT
|
).apply {
|
||||||
)
|
|
||||||
.apply {
|
|
||||||
gravity = Gravity.TOP
|
gravity = Gravity.TOP
|
||||||
}
|
}
|
||||||
|
|
||||||
ConstraintSet().apply {
|
ConstraintSet().apply {
|
||||||
clone(view.toolbarLayout)
|
clone(view.toolbarLayout)
|
||||||
clear(view.bottom_bar.id, BOTTOM)
|
clear(view.bottom_bar.id, BOTTOM)
|
||||||
clear(view.bottomBarShadow.id, BOTTOM)
|
clear(view.bottomBarShadow.id, BOTTOM)
|
||||||
connect(view.bottom_bar.id, TOP, PARENT_ID, TOP)
|
connect(view.bottom_bar.id, TOP, PARENT_ID, TOP)
|
||||||
connect(view.bottomBarShadow.id, TOP, view.bottom_bar.id, BOTTOM)
|
connect(view.bottomBarShadow.id, TOP, view.bottom_bar.id, BOTTOM)
|
||||||
connect(view.bottomBarShadow.id, BOTTOM, PARENT_ID, BOTTOM)
|
connect(view.bottomBarShadow.id, BOTTOM, PARENT_ID, BOTTOM)
|
||||||
applyTo(view.toolbarLayout)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
ToolbarPosition.BOTTOM -> {
|
||||||
view.bottom_bar.background = resources.getDrawable(
|
|
||||||
ThemeManager.resolveAttribute(R.attr.bottomBarBackgroundTop, requireContext()),
|
|
||||||
null
|
|
||||||
)
|
|
||||||
|
|
||||||
view.homeAppBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
topMargin = HEADER_MARGIN.dpToPx(resources.displayMetrics)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
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 {
|
view.tab_button.setOnClickListener {
|
||||||
openTabTray()
|
openTabTray()
|
||||||
}
|
}
|
||||||
|
@ -407,46 +404,95 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
bundleArgs.getString(SESSION_TO_DELETE)?.also {
|
bundleArgs.getString(SESSION_TO_DELETE)?.also {
|
||||||
sessionManager.findSessionById(it)?.let { session ->
|
if (it == ALL_NORMAL_TABS || it == ALL_PRIVATE_TABS) {
|
||||||
val snapshot = sessionManager.createSessionSnapshot(session)
|
removeAllTabsAndShowSnackbar(it)
|
||||||
val state = snapshot.engineSession?.saveState()
|
} else {
|
||||||
val isSelected =
|
removeTabAndShowSnackbar(it)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTabCounter(requireComponents.core.store.state)
|
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() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
_sessionControlInteractor = null
|
_sessionControlInteractor = null
|
||||||
sessionControlView = null
|
sessionControlView = null
|
||||||
bundleArgs.clear()
|
bundleArgs.clear()
|
||||||
requireView().homeAppBar.removeOnOffsetChangedListener(homeAppBarOffSetListener)
|
|
||||||
requireActivity().window.clearFlags(FLAG_SECURE)
|
requireActivity().window.clearFlags(FLAG_SECURE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -561,7 +607,6 @@ class HomeFragment : Fragment() {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
calculateNewOffset()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun recommendPrivateBrowsingShortcut() {
|
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() {
|
private fun registerCollectionStorageObserver() {
|
||||||
requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this)
|
requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this)
|
||||||
}
|
}
|
||||||
|
@ -791,7 +832,9 @@ class HomeFragment : Fragment() {
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
val recyclerView = sessionControlView!!.view
|
val recyclerView = sessionControlView!!.view
|
||||||
delay(ANIM_SCROLL_DELAY)
|
delay(ANIM_SCROLL_DELAY)
|
||||||
val tabsSize = getNumberOfSessions()
|
val tabsSize = store.state
|
||||||
|
.getNormalOrPrivateTabs(browsingModeManager.mode.isPrivate)
|
||||||
|
.size
|
||||||
|
|
||||||
var indexOfCollection = tabsSize + NON_TAB_ITEM_NUM
|
var indexOfCollection = tabsSize + NON_TAB_ITEM_NUM
|
||||||
changedCollection?.let { changedCollection ->
|
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() {
|
private fun openTabTray() {
|
||||||
TabTrayDialogFragment.show(parentFragmentManager)
|
findNavController().nav(
|
||||||
|
R.id.homeFragment,
|
||||||
|
HomeFragmentDirections.actionGlobalTabTrayDialogFragment()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateTabCounter(browserState: BrowserState) {
|
private fun updateTabCounter(browserState: BrowserState) {
|
||||||
|
@ -938,6 +952,9 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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 FOCUS_ON_ADDRESS_BAR = "focusOnAddressBar"
|
||||||
private const val SESSION_TO_DELETE = "session_to_delete"
|
private const val SESSION_TO_DELETE = "session_to_delete"
|
||||||
private const val ANIMATION_DELAY = 100L
|
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.tab.collections.ext.restore
|
||||||
import mozilla.components.feature.tabs.TabsUseCases
|
import mozilla.components.feature.tabs.TabsUseCases
|
||||||
import mozilla.components.feature.top.sites.TopSite
|
import mozilla.components.feature.top.sites.TopSite
|
||||||
|
import mozilla.components.support.ktx.kotlin.isUrl
|
||||||
import org.mozilla.fenix.BrowserDirection
|
import org.mozilla.fenix.BrowserDirection
|
||||||
import org.mozilla.fenix.HomeActivity
|
import org.mozilla.fenix.HomeActivity
|
||||||
import org.mozilla.fenix.R
|
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.TopSiteStorage
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
import org.mozilla.fenix.components.metrics.MetricController
|
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.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.nav
|
||||||
import org.mozilla.fenix.ext.sessionsOfType
|
import org.mozilla.fenix.ext.sessionsOfType
|
||||||
|
import org.mozilla.fenix.ext.settings
|
||||||
import org.mozilla.fenix.home.HomeFragment
|
import org.mozilla.fenix.home.HomeFragment
|
||||||
import org.mozilla.fenix.home.HomeFragmentAction
|
import org.mozilla.fenix.home.HomeFragmentAction
|
||||||
import org.mozilla.fenix.home.HomeFragmentDirections
|
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
|
* [HomeFragment] controller. An interface that handles the view manipulation of the Tabs triggered
|
||||||
* by the Interactor.
|
* by the Interactor.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
interface SessionControlController {
|
interface SessionControlController {
|
||||||
/**
|
/**
|
||||||
* @see [CollectionInteractor.onCollectionAddTabTapped]
|
* @see [CollectionInteractor.onCollectionAddTabTapped]
|
||||||
|
@ -120,15 +125,28 @@ interface SessionControlController {
|
||||||
*/
|
*/
|
||||||
fun handleToggleCollectionExpanded(collection: TabCollection, expand: Boolean)
|
fun handleToggleCollectionExpanded(collection: TabCollection, expand: Boolean)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [TipInteractor.onCloseTip]
|
||||||
|
*/
|
||||||
fun handleCloseTip(tip: Tip)
|
fun handleCloseTip(tip: Tip)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [ToolbarInteractor.onPasteAndGo]
|
||||||
|
*/
|
||||||
|
fun handlePasteAndGo(clipboardText: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [ToolbarInteractor.onPaste]
|
||||||
|
*/
|
||||||
|
fun handlePaste(clipboardText: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see [CollectionInteractor.onAddTabsToCollectionTapped]
|
* @see [CollectionInteractor.onAddTabsToCollectionTapped]
|
||||||
*/
|
*/
|
||||||
fun handleCreateCollection()
|
fun handleCreateCollection()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("TooManyFunctions", "LargeClass", "LongParameterList")
|
@Suppress("TooManyFunctions", "LargeClass")
|
||||||
class DefaultSessionControlController(
|
class DefaultSessionControlController(
|
||||||
private val activity: HomeActivity,
|
private val activity: HomeActivity,
|
||||||
private val engine: Engine,
|
private val engine: Engine,
|
||||||
|
@ -192,8 +210,12 @@ class DefaultSessionControlController(
|
||||||
metrics.track(Event.CollectionTabRemoved)
|
metrics.track(Event.CollectionTabRemoved)
|
||||||
|
|
||||||
if (collection.tabs.size == 1) {
|
if (collection.tabs.size == 1) {
|
||||||
val title = activity.resources.getString(R.string.delete_tab_and_collection_dialog_title, collection.title)
|
val title = activity.resources.getString(
|
||||||
val message = activity.resources.getString(R.string.delete_tab_and_collection_dialog_message)
|
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)
|
showDeleteCollectionPrompt(collection, title, message)
|
||||||
} else {
|
} else {
|
||||||
viewLifecycleScope.launch(Dispatchers.IO) {
|
viewLifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
@ -208,7 +230,8 @@ class DefaultSessionControlController(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleDeleteCollectionTapped(collection: TabCollection) {
|
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)
|
showDeleteCollectionPrompt(collection, null, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,8 +277,12 @@ class DefaultSessionControlController(
|
||||||
|
|
||||||
override fun handleSelectTopSite(url: String, isDefault: Boolean) {
|
override fun handleSelectTopSite(url: String, isDefault: Boolean) {
|
||||||
metrics.track(Event.TopSiteOpenInNewTab)
|
metrics.track(Event.TopSiteOpenInNewTab)
|
||||||
if (isDefault) { metrics.track(Event.TopSiteOpenDefault) }
|
if (isDefault) {
|
||||||
if (url == SupportUtils.POCKET_TRENDING_URL) { metrics.track(Event.PocketTopSiteClicked) }
|
metrics.track(Event.TopSiteOpenDefault)
|
||||||
|
}
|
||||||
|
if (url == SupportUtils.POCKET_TRENDING_URL) {
|
||||||
|
metrics.track(Event.PocketTopSiteClicked)
|
||||||
|
}
|
||||||
addTabUseCase.invoke(
|
addTabUseCase.invoke(
|
||||||
url = url,
|
url = url,
|
||||||
selectTab = true,
|
selectTab = true,
|
||||||
|
@ -297,6 +324,13 @@ class DefaultSessionControlController(
|
||||||
fragmentStore.dispatch(HomeFragmentAction.RemoveTip(tip))
|
fragmentStore.dispatch(HomeFragmentAction.RemoveTip(tip))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showTabTrayCollectionCreation() {
|
||||||
|
val directions = HomeFragmentDirections.actionGlobalTabTrayDialogFragment(
|
||||||
|
enterMultiselect = true
|
||||||
|
)
|
||||||
|
navController.nav(R.id.homeFragment, directions)
|
||||||
|
}
|
||||||
|
|
||||||
private fun showCollectionCreationFragment(
|
private fun showCollectionCreationFragment(
|
||||||
step: SaveCollectionStep,
|
step: SaveCollectionStep,
|
||||||
selectedTabIds: Array<String>? = null,
|
selectedTabIds: Array<String>? = null,
|
||||||
|
@ -322,7 +356,7 @@ class DefaultSessionControlController(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleCreateCollection() {
|
override fun handleCreateCollection() {
|
||||||
showCollectionCreationFragment(step = SaveCollectionStep.SelectTabs)
|
showTabTrayCollectionCreation()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showShareFragment(data: List<ShareData>) {
|
private fun showShareFragment(data: List<ShareData>) {
|
||||||
|
@ -331,4 +365,37 @@ class DefaultSessionControlController(
|
||||||
)
|
)
|
||||||
navController.nav(R.id.homeFragment, directions)
|
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()
|
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].
|
* Interface for onboarding related actions in the [SessionControlInteractor].
|
||||||
*/
|
*/
|
||||||
|
@ -163,7 +175,8 @@ interface TopSiteInteractor {
|
||||||
@SuppressWarnings("TooManyFunctions")
|
@SuppressWarnings("TooManyFunctions")
|
||||||
class SessionControlInteractor(
|
class SessionControlInteractor(
|
||||||
private val controller: SessionControlController
|
private val controller: SessionControlController
|
||||||
) : CollectionInteractor, OnboardingInteractor, TopSiteInteractor, TipInteractor, TabSessionInteractor {
|
) : CollectionInteractor, OnboardingInteractor, TopSiteInteractor, TipInteractor,
|
||||||
|
TabSessionInteractor, ToolbarInteractor {
|
||||||
override fun onCollectionAddTabTapped(collection: TabCollection) {
|
override fun onCollectionAddTabTapped(collection: TabCollection) {
|
||||||
controller.handleCollectionAddTabTapped(collection)
|
controller.handleCollectionAddTabTapped(collection)
|
||||||
}
|
}
|
||||||
|
@ -235,4 +248,12 @@ class SessionControlInteractor(
|
||||||
override fun onPrivateBrowsingLearnMoreClicked() {
|
override fun onPrivateBrowsingLearnMoreClicked() {
|
||||||
controller.handlePrivateBrowsingLearnMoreClicked()
|
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
|
// 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.
|
// When we remove the tabs from the home screen this will get much simpler again.
|
||||||
@SuppressWarnings("LongParameterList", "ComplexMethod")
|
@Suppress("ComplexMethod")
|
||||||
private fun normalModeAdapterItems(
|
private fun normalModeAdapterItems(
|
||||||
topSites: List<TopSite>,
|
topSites: List<TopSite>,
|
||||||
collections: List<TabCollection>,
|
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.R
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
import org.mozilla.fenix.components.metrics.Event.OnboardingToolbarPosition.Position
|
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.asActivity
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.onboarding.OnboardingRadioButton
|
import org.mozilla.fenix.onboarding.OnboardingRadioButton
|
||||||
|
@ -29,10 +30,9 @@ class OnboardingToolbarPositionPickerViewHolder(view: View) : RecyclerView.ViewH
|
||||||
radioBottomToolbar.addIllustration(view.toolbar_bottom_image)
|
radioBottomToolbar.addIllustration(view.toolbar_bottom_image)
|
||||||
|
|
||||||
val settings = view.context.components.settings
|
val settings = view.context.components.settings
|
||||||
radio = if (settings.shouldUseBottomToolbar) {
|
radio = when (settings.toolbarPosition) {
|
||||||
radioBottomToolbar
|
ToolbarPosition.BOTTOM -> radioBottomToolbar
|
||||||
} else {
|
ToolbarPosition.TOP -> radioTopToolbar
|
||||||
radioTopToolbar
|
|
||||||
}
|
}
|
||||||
radio.updateRadioValue(true)
|
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
|
package org.mozilla.fenix.home.tips
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.core.view.isVisible
|
||||||
import kotlinx.android.synthetic.main.button_tip_item.view.*
|
import kotlinx.android.synthetic.main.button_tip_item.*
|
||||||
import org.mozilla.fenix.BrowserDirection
|
import org.mozilla.fenix.BrowserDirection
|
||||||
import org.mozilla.fenix.HomeActivity
|
import org.mozilla.fenix.HomeActivity
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
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.Tip
|
||||||
import org.mozilla.fenix.components.tips.TipType
|
import org.mozilla.fenix.components.tips.TipType
|
||||||
import org.mozilla.fenix.ext.addUnderline
|
import org.mozilla.fenix.ext.addUnderline
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.settings
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
|
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
|
||||||
|
import org.mozilla.fenix.utils.Settings
|
||||||
|
import org.mozilla.fenix.utils.view.ViewHolder
|
||||||
|
|
||||||
class ButtonTipViewHolder(
|
class ButtonTipViewHolder(
|
||||||
val view: View,
|
view: View,
|
||||||
val interactor: SessionControlInteractor
|
private val interactor: SessionControlInteractor,
|
||||||
) : RecyclerView.ViewHolder(view) {
|
private val metrics: MetricController = view.context.components.analytics.metrics,
|
||||||
|
private val settings: Settings = view.context.components.settings
|
||||||
|
) : ViewHolder(view) {
|
||||||
|
|
||||||
var tip: Tip? = null
|
var tip: Tip? = null
|
||||||
|
|
||||||
fun bind(tip: Tip) {
|
fun bind(tip: Tip) {
|
||||||
|
@ -25,44 +34,39 @@ class ButtonTipViewHolder(
|
||||||
|
|
||||||
this.tip = tip
|
this.tip = tip
|
||||||
|
|
||||||
view.apply {
|
metrics.track(Event.TipDisplayed(tip.identifier))
|
||||||
context.components.analytics.metrics.track(Event.TipDisplayed(tip.identifier))
|
|
||||||
|
|
||||||
tip_header_text.text = tip.title
|
tip_header_text.text = tip.title
|
||||||
tip_description_text.text = tip.description
|
tip_description_text.text = tip.description
|
||||||
tip_button.text = tip.type.text
|
tip_button.text = tip.type.text
|
||||||
|
|
||||||
if (tip.learnMoreURL == null) {
|
tip_learn_more.isVisible = tip.learnMoreURL != null
|
||||||
tip_learn_more.visibility = View.GONE
|
if (tip.learnMoreURL != null) {
|
||||||
} else {
|
tip_learn_more.addUnderline()
|
||||||
tip_learn_more.addUnderline()
|
|
||||||
|
|
||||||
tip_learn_more.setOnClickListener {
|
tip_learn_more.setOnClickListener {
|
||||||
(context as HomeActivity).openToBrowserAndLoad(
|
(itemView.context as HomeActivity).openToBrowserAndLoad(
|
||||||
searchTermOrURL = tip.learnMoreURL,
|
searchTermOrURL = tip.learnMoreURL,
|
||||||
newTab = true,
|
newTab = true,
|
||||||
from = BrowserDirection.FromHome
|
from = BrowserDirection.FromHome
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tip_button.setOnClickListener {
|
|
||||||
tip.type.action.invoke()
|
|
||||||
context.components.analytics.metrics.track(
|
|
||||||
Event.TipPressed(tip.identifier)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tip_close.setOnClickListener {
|
tip_button.setOnClickListener {
|
||||||
context.components.analytics.metrics.track(Event.TipClosed(tip.identifier))
|
tip.type.action.invoke()
|
||||||
|
metrics.track(Event.TipPressed(tip.identifier))
|
||||||
|
}
|
||||||
|
|
||||||
context.settings().preferences
|
tip_close.setOnClickListener {
|
||||||
.edit()
|
metrics.track(Event.TipClosed(tip.identifier))
|
||||||
.putBoolean(tip.identifier, false)
|
|
||||||
.apply()
|
|
||||||
|
|
||||||
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.
|
* [BookmarkFragment] controller.
|
||||||
* Delegated by View Interactors, handles container business logic and operates changes on it.
|
* Delegated by View Interactors, handles container business logic and operates changes on it.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
interface BookmarkController {
|
interface BookmarkController {
|
||||||
fun handleBookmarkChanged(item: BookmarkNode)
|
fun handleBookmarkChanged(item: BookmarkNode)
|
||||||
fun handleBookmarkTapped(item: BookmarkNode)
|
fun handleBookmarkTapped(item: BookmarkNode)
|
||||||
|
@ -47,7 +47,7 @@ interface BookmarkController {
|
||||||
fun handleBackPressed()
|
fun handleBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("TooManyFunctions", "LongParameterList")
|
@Suppress("TooManyFunctions")
|
||||||
class DefaultBookmarkController(
|
class DefaultBookmarkController(
|
||||||
private val activity: HomeActivity,
|
private val activity: HomeActivity,
|
||||||
private val navController: NavController,
|
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.requireComponents
|
||||||
import org.mozilla.fenix.ext.toShortUrl
|
import org.mozilla.fenix.ext.toShortUrl
|
||||||
import org.mozilla.fenix.library.LibraryPageFragment
|
import org.mozilla.fenix.library.LibraryPageFragment
|
||||||
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
|
|
||||||
import org.mozilla.fenix.utils.allowUndo
|
import org.mozilla.fenix.utils.allowUndo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -240,7 +239,7 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
|
||||||
|
|
||||||
private fun showTabTray() {
|
private fun showTabTray() {
|
||||||
invokePendingDeletion()
|
invokePendingDeletion()
|
||||||
TabTrayDialogFragment.show(parentFragmentManager)
|
navigate(BookmarkFragmentDirections.actionGlobalTabTrayDialogFragment())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun navigate(directions: NavDirections) {
|
private fun navigate(directions: NavDirections) {
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
/*
|
/* 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
|
||||||
* * License, v. 2.0. If a copy of the MPL was not distributed with this
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
* * file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.mozilla.fenix.library.history
|
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.showToolbar
|
||||||
import org.mozilla.fenix.ext.toShortUrl
|
import org.mozilla.fenix.ext.toShortUrl
|
||||||
import org.mozilla.fenix.library.LibraryPageFragment
|
import org.mozilla.fenix.library.LibraryPageFragment
|
||||||
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
|
|
||||||
import org.mozilla.fenix.utils.allowUndo
|
import org.mozilla.fenix.utils.allowUndo
|
||||||
|
|
||||||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||||
|
@ -207,7 +206,10 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
||||||
|
|
||||||
private fun showTabTray() {
|
private fun showTabTray() {
|
||||||
invokePendingDeletion()
|
invokePendingDeletion()
|
||||||
TabTrayDialogFragment.show(parentFragmentManager)
|
findNavController().nav(
|
||||||
|
R.id.historyFragment,
|
||||||
|
HistoryFragmentDirections.actionGlobalTabTrayDialogFragment()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMultiSelectSnackBarMessage(historyItems: Set<HistoryItem>): String {
|
private fun getMultiSelectSnackBarMessage(historyItems: Set<HistoryItem>): String {
|
||||||
|
@ -259,7 +261,10 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
||||||
launch(Main) {
|
launch(Main) {
|
||||||
viewModel.invalidate()
|
viewModel.invalidate()
|
||||||
historyStore.dispatch(HistoryFragmentAction.ExitDeletionMode)
|
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
|
/* 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
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
package org.mozilla.fenix.library.history
|
package org.mozilla.fenix.library.history
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* 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
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
package org.mozilla.fenix.library.history
|
package org.mozilla.fenix.library.history
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* 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
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
package org.mozilla.fenix.loginexceptions
|
package org.mozilla.fenix.loginexceptions
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* 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
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
package org.mozilla.fenix.loginexceptions
|
package org.mozilla.fenix.loginexceptions
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.os.BatteryManager
|
import android.os.BatteryManager
|
||||||
|
import mozilla.components.support.base.log.logger.Logger
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.onboarding.FenixOnboarding
|
import org.mozilla.fenix.onboarding.FenixOnboarding
|
||||||
import android.provider.Settings as AndroidSettings
|
import android.provider.Settings as AndroidSettings
|
||||||
|
@ -17,6 +18,8 @@ import android.provider.Settings as AndroidSettings
|
||||||
*/
|
*/
|
||||||
object Performance {
|
object Performance {
|
||||||
const val TAG = "FenixPerf"
|
const val TAG = "FenixPerf"
|
||||||
|
val logger = Logger(TAG)
|
||||||
|
|
||||||
private const val EXTRA_IS_PERFORMANCE_TEST = "performancetest"
|
private const val EXTRA_IS_PERFORMANCE_TEST = "performancetest"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -7,9 +7,9 @@ package org.mozilla.fenix.perf
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.view.doOnPreDraw
|
import androidx.core.view.doOnPreDraw
|
||||||
|
import mozilla.components.support.ktx.android.view.reportFullyDrawnSafe
|
||||||
import org.mozilla.fenix.HomeActivity
|
import org.mozilla.fenix.HomeActivity
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.ext.reportFullyDrawnSafe
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.TopSiteItemViewHolder
|
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.APP_LINK
|
||||||
import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupDestination.HOMESCREEN
|
import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupDestination.HOMESCREEN
|
||||||
|
@ -65,6 +65,6 @@ class StartupReportFullyDrawn {
|
||||||
// - the difference in timing is minimal (< 7ms on Pixel 2)
|
// - 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
|
// - if we compare against another app using a preDrawListener, as we are with Fennec, it
|
||||||
// should be comparable
|
// 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
|
||||||
* 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
|
||||||
* * License, v. 2.0. If a copy of the MPL was not distributed with this
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
* * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package org.mozilla.fenix.search
|
package org.mozilla.fenix.search
|
||||||
|
|
||||||
|
@ -9,6 +8,7 @@ import android.content.Intent
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import mozilla.components.browser.search.SearchEngine
|
import mozilla.components.browser.search.SearchEngine
|
||||||
import mozilla.components.browser.session.Session
|
import mozilla.components.browser.session.Session
|
||||||
|
import mozilla.components.browser.session.SessionManager
|
||||||
import mozilla.components.support.ktx.kotlin.isUrl
|
import mozilla.components.support.ktx.kotlin.isUrl
|
||||||
import org.mozilla.fenix.BrowserDirection
|
import org.mozilla.fenix.BrowserDirection
|
||||||
import org.mozilla.fenix.HomeActivity
|
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.ACTION
|
||||||
import org.mozilla.fenix.components.metrics.Event.PerformedSearch.SearchAccessPoint.NONE
|
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.Event.PerformedSearch.SearchAccessPoint.SUGGESTION
|
||||||
|
import org.mozilla.fenix.components.metrics.MetricController
|
||||||
import org.mozilla.fenix.components.metrics.MetricsUtils
|
import org.mozilla.fenix.components.metrics.MetricsUtils
|
||||||
import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore
|
import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore
|
||||||
import org.mozilla.fenix.crashes.CrashListActivity
|
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.navigateSafe
|
||||||
import org.mozilla.fenix.ext.settings
|
|
||||||
import org.mozilla.fenix.settings.SupportUtils
|
import org.mozilla.fenix.settings.SupportUtils
|
||||||
import org.mozilla.fenix.settings.SupportUtils.MozillaPage.MANIFESTO
|
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
|
* An interface that handles the view manipulation of the Search, triggered by the Interactor
|
||||||
|
@ -43,11 +42,14 @@ interface SearchController {
|
||||||
fun handleSearchShortcutsButtonClicked()
|
fun handleSearchShortcutsButtonClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions", "LongParameterList")
|
||||||
class DefaultSearchController(
|
class DefaultSearchController(
|
||||||
private val activity: HomeActivity,
|
private val activity: HomeActivity,
|
||||||
|
private val sessionManager: SessionManager,
|
||||||
private val store: SearchFragmentStore,
|
private val store: SearchFragmentStore,
|
||||||
private val navController: NavController,
|
private val navController: NavController,
|
||||||
|
private val settings: Settings,
|
||||||
|
private val metrics: MetricController,
|
||||||
private val clearToolbarFocus: () -> Unit
|
private val clearToolbarFocus: () -> Unit
|
||||||
) : SearchController {
|
) : SearchController {
|
||||||
|
|
||||||
|
@ -77,7 +79,7 @@ class DefaultSearchController(
|
||||||
val event = if (url.isUrl()) {
|
val event = if (url.isUrl()) {
|
||||||
Event.EnteredUrl(false)
|
Event.EnteredUrl(false)
|
||||||
} else {
|
} else {
|
||||||
activity.settings().incrementActiveSearchCount()
|
settings.incrementActiveSearchCount()
|
||||||
|
|
||||||
val searchAccessPoint = when (store.state.searchAccessPoint) {
|
val searchAccessPoint = when (store.state.searchAccessPoint) {
|
||||||
NONE -> ACTION
|
NONE -> ACTION
|
||||||
|
@ -93,7 +95,7 @@ class DefaultSearchController(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
event?.let { activity.metrics.track(it) }
|
event?.let { metrics.track(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleEditingCancelled() {
|
override fun handleEditingCancelled() {
|
||||||
|
@ -101,7 +103,6 @@ class DefaultSearchController(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleTextChanged(text: String) {
|
override fun handleTextChanged(text: String) {
|
||||||
val settings = activity.settings()
|
|
||||||
// Display the search shortcuts on each entry of the search fragment (see #5308)
|
// Display the search shortcuts on each entry of the search fragment (see #5308)
|
||||||
val textMatchesCurrentUrl = store.state.url == text
|
val textMatchesCurrentUrl = store.state.url == text
|
||||||
val textMatchesCurrentSearch = store.state.searchTerms == text
|
val textMatchesCurrentSearch = store.state.searchTerms == text
|
||||||
|
@ -130,11 +131,11 @@ class DefaultSearchController(
|
||||||
from = BrowserDirection.FromSearch
|
from = BrowserDirection.FromSearch
|
||||||
)
|
)
|
||||||
|
|
||||||
activity.metrics.track(Event.EnteredUrl(false))
|
metrics.track(Event.EnteredUrl(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleSearchTermsTapped(searchTerms: String) {
|
override fun handleSearchTermsTapped(searchTerms: String) {
|
||||||
activity.settings().incrementActiveSearchCount()
|
settings.incrementActiveSearchCount()
|
||||||
|
|
||||||
activity.openToBrowserAndLoad(
|
activity.openToBrowserAndLoad(
|
||||||
searchTermOrURL = searchTerms,
|
searchTermOrURL = searchTerms,
|
||||||
|
@ -156,14 +157,14 @@ class DefaultSearchController(
|
||||||
sap
|
sap
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
event?.let { activity.metrics.track(it) }
|
event?.let { metrics.track(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleSearchShortcutEngineSelected(searchEngine: SearchEngine) {
|
override fun handleSearchShortcutEngineSelected(searchEngine: SearchEngine) {
|
||||||
store.dispatch(SearchFragmentAction.SearchShortcutEngineSelected(searchEngine))
|
store.dispatch(SearchFragmentAction.SearchShortcutEngineSelected(searchEngine))
|
||||||
val isCustom =
|
val isCustom =
|
||||||
CustomSearchEngineStore.isCustomSearchEngine(activity, searchEngine.identifier)
|
CustomSearchEngineStore.isCustomSearchEngine(activity, searchEngine.identifier)
|
||||||
activity.metrics.track(Event.SearchShortcutSelected(searchEngine, isCustom))
|
metrics.track(Event.SearchShortcutSelected(searchEngine, isCustom))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleSearchShortcutsButtonClicked() {
|
override fun handleSearchShortcutsButtonClicked() {
|
||||||
|
@ -177,14 +178,14 @@ class DefaultSearchController(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleExistingSessionSelected(session: Session) {
|
override fun handleExistingSessionSelected(session: Session) {
|
||||||
activity.components.core.sessionManager.select(session)
|
sessionManager.select(session)
|
||||||
activity.openToBrowser(
|
activity.openToBrowser(
|
||||||
from = BrowserDirection.FromSearch
|
from = BrowserDirection.FromSearch
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleExistingSessionSelected(tabId: String) {
|
override fun handleExistingSessionSelected(tabId: String) {
|
||||||
val session = activity.components.core.sessionManager.findSessionById(tabId)
|
val session = sessionManager.findSessionById(tabId)
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
handleExistingSessionSelected(session)
|
handleExistingSessionSelected(session)
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,6 +86,7 @@ class SearchFragment : Fragment(), UserInteractionHandler {
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
val activity = activity as HomeActivity
|
val activity = activity as HomeActivity
|
||||||
|
val settings = activity.settings()
|
||||||
val args by navArgs<SearchFragmentArgs>()
|
val args by navArgs<SearchFragmentArgs>()
|
||||||
|
|
||||||
val tabId = args.sessionId
|
val tabId = args.sessionId
|
||||||
|
@ -112,13 +113,13 @@ class SearchFragment : Fragment(), UserInteractionHandler {
|
||||||
defaultEngineSource = currentSearchEngine,
|
defaultEngineSource = currentSearchEngine,
|
||||||
showSearchSuggestions = shouldShowSearchSuggestions(isPrivate),
|
showSearchSuggestions = shouldShowSearchSuggestions(isPrivate),
|
||||||
showSearchSuggestionsHint = false,
|
showSearchSuggestionsHint = false,
|
||||||
showSearchShortcuts = requireContext().settings().shouldShowSearchShortcuts &&
|
showSearchShortcuts = settings.shouldShowSearchShortcuts &&
|
||||||
url.isEmpty() &&
|
url.isEmpty() &&
|
||||||
areShortcutsAvailable,
|
areShortcutsAvailable,
|
||||||
areShortcutsAvailable = areShortcutsAvailable,
|
areShortcutsAvailable = areShortcutsAvailable,
|
||||||
showClipboardSuggestions = requireContext().settings().shouldShowClipboardSuggestions,
|
showClipboardSuggestions = settings.shouldShowClipboardSuggestions,
|
||||||
showHistorySuggestions = requireContext().settings().shouldShowHistorySuggestions,
|
showHistorySuggestions = settings.shouldShowHistorySuggestions,
|
||||||
showBookmarkSuggestions = requireContext().settings().shouldShowBookmarkSuggestions,
|
showBookmarkSuggestions = settings.shouldShowBookmarkSuggestions,
|
||||||
tabId = tabId,
|
tabId = tabId,
|
||||||
pastedText = args.pastedText,
|
pastedText = args.pastedText,
|
||||||
searchAccessPoint = args.searchAccessPoint
|
searchAccessPoint = args.searchAccessPoint
|
||||||
|
@ -128,8 +129,11 @@ class SearchFragment : Fragment(), UserInteractionHandler {
|
||||||
|
|
||||||
val searchController = DefaultSearchController(
|
val searchController = DefaultSearchController(
|
||||||
activity = activity,
|
activity = activity,
|
||||||
|
sessionManager = requireComponents.core.sessionManager,
|
||||||
store = searchStore,
|
store = searchStore,
|
||||||
navController = findNavController(),
|
navController = findNavController(),
|
||||||
|
settings = settings,
|
||||||
|
metrics = requireComponents.analytics.metrics,
|
||||||
clearToolbarFocus = ::clearToolbarFocus
|
clearToolbarFocus = ::clearToolbarFocus
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -160,7 +164,11 @@ class SearchFragment : Fragment(), UserInteractionHandler {
|
||||||
BrowserToolbar.Button(
|
BrowserToolbar.Button(
|
||||||
ContextCompat.getDrawable(requireContext(), R.drawable.ic_microphone)!!,
|
ContextCompat.getDrawable(requireContext(), R.drawable.ic_microphone)!!,
|
||||||
requireContext().getString(R.string.voice_search_content_description),
|
requireContext().getString(R.string.voice_search_content_description),
|
||||||
visible = { requireContext().settings().shouldShowVoiceSearch && speechIsAvailable() },
|
visible = {
|
||||||
|
currentSearchEngine.searchEngine.identifier.contains("google") &&
|
||||||
|
speechIsAvailable() &&
|
||||||
|
settings.shouldShowVoiceSearch
|
||||||
|
},
|
||||||
listener = ::launchVoiceSearch
|
listener = ::launchVoiceSearch
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* 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
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
package org.mozilla.fenix.search
|
package org.mozilla.fenix.search
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* 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
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
package org.mozilla.fenix.search.toolbar
|
package org.mozilla.fenix.search.toolbar
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
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.components
|
||||||
import org.mozilla.fenix.ext.requireComponents
|
import org.mozilla.fenix.ext.requireComponents
|
||||||
import org.mozilla.fenix.ext.settings
|
import org.mozilla.fenix.ext.settings
|
||||||
|
@ -122,8 +123,9 @@ class CustomizationFragment : PreferenceFragmentCompat() {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
topPreference.setCheckedWithoutClickListener(!requireContext().settings().shouldUseBottomToolbar)
|
val toolbarPosition = requireContext().settings().toolbarPosition
|
||||||
bottomPreference.setCheckedWithoutClickListener(requireContext().settings().shouldUseBottomToolbar)
|
topPreference.setCheckedWithoutClickListener(toolbarPosition == ToolbarPosition.TOP)
|
||||||
|
bottomPreference.setCheckedWithoutClickListener(toolbarPosition == ToolbarPosition.BOTTOM)
|
||||||
|
|
||||||
addToRadioGroup(topPreference, bottomPreference)
|
addToRadioGroup(topPreference, bottomPreference)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
package org.mozilla.fenix.settings
|
package org.mozilla.fenix.settings
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
@ -13,16 +12,13 @@ import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import androidx.navigation.findNavController
|
import androidx.navigation.findNavController
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceCategory
|
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import mozilla.components.concept.sync.AccountObserver
|
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.requireComponents
|
||||||
import org.mozilla.fenix.ext.settings
|
import org.mozilla.fenix.ext.settings
|
||||||
import org.mozilla.fenix.ext.showToolbar
|
import org.mozilla.fenix.ext.showToolbar
|
||||||
import org.mozilla.fenix.ext.toRoundedDrawable
|
import org.mozilla.fenix.settings.account.AccountUiView
|
||||||
import org.mozilla.fenix.settings.account.AccountAuthErrorPreference
|
|
||||||
import org.mozilla.fenix.settings.account.AccountPreference
|
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
@Suppress("LargeClass", "TooManyFunctions")
|
@Suppress("LargeClass", "TooManyFunctions")
|
||||||
class SettingsFragment : PreferenceFragmentCompat() {
|
class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
|
private lateinit var accountUiView: AccountUiView
|
||||||
|
|
||||||
private val accountObserver = object : AccountObserver {
|
private val accountObserver = object : AccountObserver {
|
||||||
private fun updateAccountUi(profile: Profile? = null) {
|
private fun updateAccountUi(profile: Profile? = null) {
|
||||||
val context = context ?: return
|
val context = context ?: return
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
updateAccountUIState(
|
accountUiView.updateAccountUIState(
|
||||||
context = context,
|
context = context,
|
||||||
profile = profile
|
profile = profile
|
||||||
?: context.components.backgroundServices.accountManager.accountProfile()
|
?: context.components.backgroundServices.accountManager.accountProfile()
|
||||||
|
@ -75,6 +71,13 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
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.
|
// Observe account changes to keep the UI up-to-date.
|
||||||
requireComponents.backgroundServices.accountManager.register(
|
requireComponents.backgroundServices.accountManager.register(
|
||||||
accountObserver,
|
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
|
// 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
|
// display a "Sign In" preference, which will then get replaced by the correct account information
|
||||||
// once this call is ran in onResume shortly after.
|
// once this call is ran in onResume shortly after.
|
||||||
updateAccountUIState(
|
accountUiView.updateAccountUIState(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
requireComponents.backgroundServices.accountManager.accountProfile()
|
requireComponents.backgroundServices.accountManager.accountProfile()
|
||||||
)
|
)
|
||||||
|
@ -162,7 +165,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
setupPreferences()
|
setupPreferences()
|
||||||
|
|
||||||
if (shouldUpdateAccountUIState) {
|
if (shouldUpdateAccountUIState) {
|
||||||
updateAccountUIState(
|
accountUiView.updateAccountUIState(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
requireComponents.backgroundServices.accountManager.accountProfile()
|
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()
|
preference.context.settings().preferences.edit()
|
||||||
.putBoolean(preference.key, newValue as Boolean).apply()
|
.putBoolean(preference.key, newValue).apply()
|
||||||
requireComponents.core.engine.settings.remoteDebuggingEnabled = newValue
|
requireComponents.core.engine.settings.remoteDebuggingEnabled = newValue
|
||||||
true
|
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() {
|
private fun updateFxASyncOverrideMenu() {
|
||||||
val preferenceFxAOverride =
|
val preferenceFxAOverride =
|
||||||
findPreference<Preference>(getPreferenceKey(R.string.pref_key_override_fxa_server))
|
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
|
package org.mozilla.fenix.settings
|
||||||
|
|
||||||
import androidx.core.content.edit
|
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
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, 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
|
/* 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
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
package org.mozilla.fenix.settings.account
|
package org.mozilla.fenix.settings.account
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* 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
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
package org.mozilla.fenix.settings.account
|
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
|
package org.mozilla.fenix.settings.logins
|
||||||
|
|
||||||
import android.content.Context
|
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
||||||
import org.mozilla.fenix.ext.urlToTrimmedHost
|
import org.mozilla.fenix.ext.urlToTrimmedHost
|
||||||
|
|
||||||
sealed class SortingStrategy {
|
sealed class SortingStrategy {
|
||||||
abstract operator fun invoke(logins: List<SavedLogin>): List<SavedLogin>
|
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> {
|
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> {
|
override fun invoke(logins: List<SavedLogin>): List<SavedLogin> {
|
||||||
return logins.sortedByDescending { it.timeLastUsed }
|
return logins.sortedByDescending { it.timeLastUsed }
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,12 @@ package org.mozilla.fenix.settings.logins.fragment
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuInflater
|
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
|
@ -31,17 +31,18 @@ import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.components.StoreProvider
|
import org.mozilla.fenix.components.StoreProvider
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.redirectToReAuth
|
import org.mozilla.fenix.ext.redirectToReAuth
|
||||||
|
import org.mozilla.fenix.ext.requireComponents
|
||||||
import org.mozilla.fenix.ext.settings
|
import org.mozilla.fenix.ext.settings
|
||||||
import org.mozilla.fenix.ext.showToolbar
|
import org.mozilla.fenix.ext.showToolbar
|
||||||
import org.mozilla.fenix.settings.logins.LoginsAction
|
import org.mozilla.fenix.settings.logins.LoginsAction
|
||||||
import org.mozilla.fenix.settings.logins.LoginsFragmentStore
|
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.LoginsListState
|
||||||
import org.mozilla.fenix.settings.logins.interactor.SavedLoginsInteractor
|
|
||||||
import org.mozilla.fenix.settings.logins.SavedLoginsSortingStrategyMenu
|
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.SortingStrategy
|
||||||
|
import org.mozilla.fenix.settings.logins.controller.LoginsListController
|
||||||
import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController
|
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")
|
@SuppressWarnings("TooManyFunctions")
|
||||||
class SavedLoginsFragment : Fragment() {
|
class SavedLoginsFragment : Fragment() {
|
||||||
|
@ -228,16 +229,14 @@ class SavedLoginsFragment : Fragment() {
|
||||||
SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort -> {
|
SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort -> {
|
||||||
savedLoginsInteractor.onSortingStrategyChanged(
|
savedLoginsInteractor.onSortingStrategyChanged(
|
||||||
SortingStrategy.Alphabetically(
|
SortingStrategy.Alphabetically(
|
||||||
requireContext().applicationContext
|
requireComponents.publicSuffixList
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SavedLoginsSortingStrategyMenu.Item.LastUsedSort -> {
|
SavedLoginsSortingStrategyMenu.Item.LastUsedSort -> {
|
||||||
savedLoginsInteractor.onSortingStrategyChanged(
|
savedLoginsInteractor.onSortingStrategyChanged(
|
||||||
SortingStrategy.LastUsed(
|
SortingStrategy.LastUsed
|
||||||
requireContext().applicationContext
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,15 @@ import org.mozilla.fenix.utils.Settings
|
||||||
fun PhoneFeature.shouldBeVisible(
|
fun PhoneFeature.shouldBeVisible(
|
||||||
sitePermissions: SitePermissions?,
|
sitePermissions: SitePermissions?,
|
||||||
settings: Settings
|
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**.
|
* Common [PhoneFeature] extensions used for **quicksettings**.
|
||||||
|
|
|
@ -14,6 +14,7 @@ import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.LinearLayout
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
@ -93,7 +94,7 @@ class AddSearchEngineFragment : Fragment(), CompoundButton.OnCheckedChangeListen
|
||||||
|
|
||||||
availableEngines.forEachIndexed(setupSearchEngineItem)
|
availableEngines.forEachIndexed(setupSearchEngineItem)
|
||||||
|
|
||||||
val engineItem = makeCustomButton(layoutInflater, res = resources)
|
val engineItem = makeCustomButton(layoutInflater)
|
||||||
engineItem.id = CUSTOM_INDEX
|
engineItem.id = CUSTOM_INDEX
|
||||||
engineItem.radio_button.isChecked = selectedIndex == CUSTOM_INDEX
|
engineItem.radio_button.isChecked = selectedIndex == CUSTOM_INDEX
|
||||||
engineViews.add(engineItem)
|
engineViews.add(engineItem)
|
||||||
|
@ -249,12 +250,11 @@ class AddSearchEngineFragment : Fragment(), CompoundButton.OnCheckedChangeListen
|
||||||
toggleCustomForm(selectedIndex == -1)
|
toggleCustomForm(selectedIndex == -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun makeCustomButton(layoutInflater: LayoutInflater, res: Resources): View {
|
private fun makeCustomButton(layoutInflater: LayoutInflater): View {
|
||||||
val wrapper = layoutInflater
|
val wrapper = layoutInflater
|
||||||
.inflate(R.layout.custom_search_engine_radio_button, null) as ConstraintLayout
|
.inflate(R.layout.custom_search_engine_radio_button, null) as ConstraintLayout
|
||||||
wrapper.setOnClickListener { wrapper.radio_button.isChecked = true }
|
wrapper.setOnClickListener { wrapper.radio_button.isChecked = true }
|
||||||
wrapper.radio_button.setOnCheckedChangeListener(this)
|
wrapper.radio_button.setOnCheckedChangeListener(this)
|
||||||
wrapper.minHeight = res.getDimensionPixelSize(R.dimen.radio_button_preference_height)
|
|
||||||
return wrapper
|
return wrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,7 +271,7 @@ class AddSearchEngineFragment : Fragment(), CompoundButton.OnCheckedChangeListen
|
||||||
res: Resources
|
res: Resources
|
||||||
): View {
|
): View {
|
||||||
val wrapper = layoutInflater
|
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.setOnClickListener { wrapper.radio_button.isChecked = true }
|
||||||
wrapper.radio_button.setOnCheckedChangeListener(this)
|
wrapper.radio_button.setOnCheckedChangeListener(this)
|
||||||
wrapper.engine_text.text = engine.name
|
wrapper.engine_text.text = engine.name
|
||||||
|
@ -280,7 +280,6 @@ class AddSearchEngineFragment : Fragment(), CompoundButton.OnCheckedChangeListen
|
||||||
engineIcon.setBounds(0, 0, iconSize, iconSize)
|
engineIcon.setBounds(0, 0, iconSize, iconSize)
|
||||||
wrapper.engine_icon.setImageDrawable(engineIcon)
|
wrapper.engine_icon.setImageDrawable(engineIcon)
|
||||||
wrapper.overflow_menu.visibility = View.GONE
|
wrapper.overflow_menu.visibility = View.GONE
|
||||||
wrapper.minHeight = res.getDimensionPixelSize(R.dimen.radio_button_preference_height)
|
|
||||||
return wrapper
|
return wrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,8 +12,8 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.LinearLayout
|
||||||
import android.widget.RadioGroup
|
import android.widget.RadioGroup
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.navigation.Navigation
|
import androidx.navigation.Navigation
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
|
@ -117,9 +117,10 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
|
||||||
res: Resources,
|
res: Resources,
|
||||||
allowDeletion: Boolean
|
allowDeletion: Boolean
|
||||||
): View {
|
): 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.setOnClickListener { wrapper.radio_button.isChecked = true }
|
||||||
wrapper.radio_button.setOnCheckedChangeListener(this)
|
wrapper.radio_button.setOnCheckedChangeListener(this)
|
||||||
wrapper.engine_text.text = engine.name
|
wrapper.engine_text.text = engine.name
|
||||||
|
@ -132,7 +133,11 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
|
||||||
onItemTapped = {
|
onItemTapped = {
|
||||||
when (it) {
|
when (it) {
|
||||||
is SearchEngineMenu.Item.Edit -> editCustomSearchEngine(engine)
|
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)
|
).menuBuilder.build(context).show(wrapper.overflow_menu)
|
||||||
|
@ -146,7 +151,8 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
|
||||||
|
|
||||||
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
|
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
|
||||||
searchEngineList.list.forEach { engine ->
|
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) {
|
when (wrapper.radio_button == buttonView) {
|
||||||
true -> onSearchEngineSelected(engine)
|
true -> onSearchEngineSelected(engine)
|
||||||
|
@ -165,12 +171,20 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
|
||||||
Navigation.findNavController(searchEngineGroup!!).navigate(directions)
|
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 isDefaultEngine = engine == context.components.search.provider.getDefaultEngine(context)
|
||||||
val initialEngineList = searchEngineList.copy()
|
val initialEngineList = searchEngineList.copy()
|
||||||
val initialDefaultEngine = searchEngineList.default
|
val initialDefaultEngine = searchEngineList.default
|
||||||
|
|
||||||
context.components.search.provider.uninstallSearchEngine(context, engine, isCustomSearchEngine)
|
context.components.search.provider.uninstallSearchEngine(
|
||||||
|
context,
|
||||||
|
engine,
|
||||||
|
isCustomSearchEngine
|
||||||
|
)
|
||||||
|
|
||||||
MainScope().allowUndo(
|
MainScope().allowUndo(
|
||||||
view = context.getRootView()!!,
|
view = context.getRootView()!!,
|
||||||
|
@ -178,7 +192,11 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
|
||||||
.getString(R.string.search_delete_search_engine_success_message, engine.name),
|
.getString(R.string.search_delete_search_engine_success_message, engine.name),
|
||||||
undoActionTitle = context.getString(R.string.snackbar_deleted_undo),
|
undoActionTitle = context.getString(R.string.snackbar_deleted_undo),
|
||||||
onCancel = {
|
onCancel = {
|
||||||
context.components.search.provider.installSearchEngine(context, engine, isCustomSearchEngine)
|
context.components.search.provider.installSearchEngine(
|
||||||
|
context,
|
||||||
|
engine,
|
||||||
|
isCustomSearchEngine
|
||||||
|
)
|
||||||
|
|
||||||
searchEngineList = initialEngineList.copy(
|
searchEngineList = initialEngineList.copy(
|
||||||
default = initialDefaultEngine
|
default = initialDefaultEngine
|
||||||
|
|
|
@ -6,7 +6,6 @@ package org.mozilla.fenix.shortcut
|
||||||
|
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import mozilla.components.browser.session.Session
|
import mozilla.components.browser.session.Session
|
||||||
import mozilla.components.concept.engine.manifest.WebAppManifest
|
|
||||||
import mozilla.components.feature.pwa.WebAppUseCases
|
import mozilla.components.feature.pwa.WebAppUseCases
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.browser.BrowserFragmentDirections
|
import org.mozilla.fenix.browser.BrowserFragmentDirections
|
||||||
|
@ -22,8 +21,8 @@ class PwaOnboardingObserver(
|
||||||
private val webAppUseCases: WebAppUseCases
|
private val webAppUseCases: WebAppUseCases
|
||||||
) : Session.Observer {
|
) : Session.Observer {
|
||||||
|
|
||||||
override fun onWebAppManifestChanged(session: Session, manifest: WebAppManifest?) {
|
override fun onLoadingStateChanged(session: Session, loading: Boolean) {
|
||||||
if (webAppUseCases.isInstallable() && !settings.userKnowsAboutPwas) {
|
if (!loading && webAppUseCases.isInstallable() && !settings.userKnowsAboutPwas) {
|
||||||
settings.incrementVisitedInstallableCount()
|
settings.incrementVisitedInstallableCount()
|
||||||
if (settings.shouldShowPwaOnboarding) {
|
if (settings.shouldShowPwaOnboarding) {
|
||||||
val directions =
|
val directions =
|
||||||
|
|
|
@ -8,16 +8,15 @@ import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import mozilla.components.concept.sync.Device as SyncDevice
|
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
|
||||||
import mozilla.components.browser.storage.sync.Tab as SyncTab
|
|
||||||
import org.mozilla.fenix.sync.SyncedTabsViewHolder.DeviceViewHolder
|
import org.mozilla.fenix.sync.SyncedTabsViewHolder.DeviceViewHolder
|
||||||
import org.mozilla.fenix.sync.SyncedTabsViewHolder.TabViewHolder
|
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(
|
class SyncedTabsAdapter(
|
||||||
private val listener: (SyncTab) -> Unit
|
private val listener: (SyncTab) -> Unit
|
||||||
) : ListAdapter<SyncedTabsAdapter.AdapterItem, SyncedTabsViewHolder>(
|
) : ListAdapter<SyncedTabsAdapter.AdapterItem, SyncedTabsViewHolder>(DiffCallback) {
|
||||||
DiffCallback
|
|
||||||
) {
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SyncedTabsViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SyncedTabsViewHolder {
|
||||||
val itemView = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
val itemView = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
||||||
|
@ -30,23 +29,35 @@ class SyncedTabsAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: SyncedTabsViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: SyncedTabsViewHolder, position: Int) {
|
||||||
val item = when (holder) {
|
holder.bind(getItem(position), listener)
|
||||||
is DeviceViewHolder -> getItem(position) as AdapterItem.Device
|
|
||||||
is TabViewHolder -> getItem(position) as AdapterItem.Tab
|
|
||||||
}
|
|
||||||
holder.bind(item, listener)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getItemViewType(position: Int) = when (getItem(position)) {
|
||||||
return when (getItem(position)) {
|
is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID
|
||||||
is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID
|
is AdapterItem.Tab -> TabViewHolder.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>() {
|
private object DiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
|
||||||
override fun areItemsTheSame(oldItem: AdapterItem, newItem: 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")
|
@Suppress("DiffUtilEquals")
|
||||||
override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
|
override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
|
||||||
|
|
|
@ -10,10 +10,10 @@ import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import kotlinx.android.synthetic.main.component_sync_tabs.view.*
|
import kotlinx.android.synthetic.main.component_sync_tabs.view.*
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
|
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
|
||||||
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
|
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
|
@ -43,15 +43,7 @@ class SyncedTabsLayout @JvmOverloads constructor(
|
||||||
// We may still be displaying a "loading" spinner, hide it.
|
// We may still be displaying a "loading" spinner, hide it.
|
||||||
stopLoading()
|
stopLoading()
|
||||||
|
|
||||||
val stringResId = when (error) {
|
sync_tabs_status.text = context.getText(stringResourceForError(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)
|
|
||||||
|
|
||||||
synced_tabs_list.visibility = View.GONE
|
synced_tabs_list.visibility = View.GONE
|
||||||
sync_tabs_status.visibility = View.VISIBLE
|
sync_tabs_status.visibility = View.VISIBLE
|
||||||
|
@ -65,19 +57,7 @@ class SyncedTabsLayout @JvmOverloads constructor(
|
||||||
synced_tabs_list.visibility = View.VISIBLE
|
synced_tabs_list.visibility = View.VISIBLE
|
||||||
sync_tabs_status.visibility = View.GONE
|
sync_tabs_status.visibility = View.GONE
|
||||||
|
|
||||||
val allDeviceTabs = emptyList<SyncedTabsAdapter.AdapterItem>().toMutableList()
|
adapter.updateData(syncedTabs)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,5 +90,13 @@ class SyncedTabsLayout @JvmOverloads constructor(
|
||||||
SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE,
|
SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE,
|
||||||
SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> true
|
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.content.Context
|
||||||
import android.view.LayoutInflater
|
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.TabViewHolder
|
||||||
import mozilla.components.browser.tabstray.TabsAdapter
|
import mozilla.components.browser.tabstray.TabsAdapter
|
||||||
|
import mozilla.components.concept.tabstray.Tab
|
||||||
import mozilla.components.concept.tabstray.Tabs
|
import mozilla.components.concept.tabstray.Tabs
|
||||||
import mozilla.components.support.images.loader.ImageLoader
|
import mozilla.components.support.images.loader.ImageLoader
|
||||||
import org.mozilla.fenix.R
|
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(
|
class FenixTabsAdapter(
|
||||||
context: Context,
|
private val context: Context,
|
||||||
imageLoader: ImageLoader
|
imageLoader: ImageLoader
|
||||||
) : TabsAdapter(
|
) : TabsAdapter(
|
||||||
viewHolderProvider = { parentView, _ ->
|
viewHolderProvider = { parentView ->
|
||||||
TabTrayViewHolder(
|
TabTrayViewHolder(
|
||||||
LayoutInflater.from(context).inflate(
|
LayoutInflater.from(context).inflate(
|
||||||
R.layout.tab_tray_item,
|
R.layout.tab_tray_item,
|
||||||
parentView,
|
parentView,
|
||||||
false),
|
false
|
||||||
|
),
|
||||||
imageLoader
|
imageLoader
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
var tabTrayInteractor: TabTrayInteractor? = null
|
||||||
|
|
||||||
|
private val mode: TabTrayDialogFragmentState.Mode?
|
||||||
|
get() = tabTrayInteractor?.onModeRequested()
|
||||||
|
|
||||||
|
val selectedItems get() = mode?.selectedItems ?: setOf()
|
||||||
|
|
||||||
var onTabsUpdated: (() -> Unit)? = null
|
var onTabsUpdated: (() -> Unit)? = null
|
||||||
var tabCount = 0
|
var tabCount = 0
|
||||||
|
|
||||||
|
@ -35,9 +50,59 @@ class FenixTabsAdapter(
|
||||||
tabCount = tabs.list.size
|
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) {
|
override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
|
||||||
super.onBindViewHolder(holder, position)
|
super.onBindViewHolder(holder, position)
|
||||||
val newIndex = tabCount - position - 1
|
val newIndex = tabCount - position - 1
|
||||||
(holder as TabTrayViewHolder).updateAccessibilityRowIndex(holder.itemView, newIndex)
|
(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.annotation.VisibleForTesting
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import mozilla.components.browser.session.Session
|
import mozilla.components.browser.session.Session
|
||||||
import mozilla.components.browser.session.SessionManager
|
|
||||||
import mozilla.components.concept.engine.prompt.ShareData
|
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.HomeActivity
|
||||||
import org.mozilla.fenix.R
|
|
||||||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
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.components
|
||||||
import org.mozilla.fenix.ext.sessionsOfType
|
import org.mozilla.fenix.ext.sessionsOfType
|
||||||
|
import org.mozilla.fenix.home.HomeFragment
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [TabTrayDialogFragment] controller.
|
* [TabTrayDialogFragment] controller.
|
||||||
*
|
*
|
||||||
* Delegated by View Interactors, handles container business logic and operates changes on it.
|
* Delegated by View Interactors, handles container business logic and operates changes on it.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
interface TabTrayController {
|
interface TabTrayController {
|
||||||
fun onNewTabTapped(private: Boolean)
|
fun onNewTabTapped(private: Boolean)
|
||||||
fun onTabTrayDismissed()
|
fun onTabTrayDismissed()
|
||||||
fun onShareTabsClicked(private: Boolean)
|
fun onShareTabsClicked(private: Boolean)
|
||||||
fun onSaveToCollectionClicked()
|
fun onSaveToCollectionClicked(selectedTabs: Set<Tab>)
|
||||||
fun onCloseAllTabsClicked(private: Boolean)
|
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")
|
@Suppress("TooManyFunctions")
|
||||||
class DefaultTabTrayController(
|
class DefaultTabTrayController(
|
||||||
private val activity: HomeActivity,
|
private val activity: HomeActivity,
|
||||||
private val navController: NavController,
|
private val navController: NavController,
|
||||||
private val dismissTabTray: () -> Unit,
|
private val dismissTabTray: () -> Unit,
|
||||||
private val showUndoSnackbar: (String, SessionManager.Snapshot) -> Unit,
|
private val dismissTabTrayAndNavigateHome: (String) -> Unit,
|
||||||
private val registerCollectionStorageObserver: () -> 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 {
|
) : TabTrayController {
|
||||||
|
private val tabCollectionStorage = activity.components.core.tabCollectionStorage
|
||||||
|
|
||||||
override fun onNewTabTapped(private: Boolean) {
|
override fun onNewTabTapped(private: Boolean) {
|
||||||
val startTime = activity.components.core.engine.profiler?.getProfilerTime()
|
val startTime = activity.components.core.engine.profiler?.getProfilerTime()
|
||||||
activity.browsingModeManager.mode = BrowsingMode.fromBoolean(private)
|
activity.browsingModeManager.mode = BrowsingMode.fromBoolean(private)
|
||||||
navController.navigate(TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true))
|
navController.navigate(TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true))
|
||||||
dismissTabTray()
|
dismissTabTray()
|
||||||
activity.components.core.engine.profiler?.addMarker("DefaultTabTrayController.onNewTabTapped", startTime)
|
activity.components.core.engine.profiler?.addMarker(
|
||||||
|
"DefaultTabTrayController.onNewTabTapped",
|
||||||
|
startTime
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTabTrayDismissed() {
|
override fun onTabTrayDismissed() {
|
||||||
dismissTabTray()
|
dismissTabTray()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveToCollectionClicked() {
|
override fun onSaveToCollectionClicked(selectedTabs: Set<Tab>) {
|
||||||
val tabs = getListOfSessions(false)
|
val sessionList = selectedTabs.map {
|
||||||
val tabIds = tabs.map { it.id }.toList().toTypedArray()
|
activity.components.core.sessionManager.findSessionById(it.id) ?: return
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (navController.currentDestination?.id == R.id.collectionCreationFragment) return
|
|
||||||
|
|
||||||
// Only register the observer right before moving to collection creation
|
// Only register the observer right before moving to collection creation
|
||||||
registerCollectionStorageObserver()
|
registerCollectionStorageObserver()
|
||||||
|
|
||||||
val directions = TabTrayDialogFragmentDirections.actionGlobalCollectionCreationFragment(
|
when {
|
||||||
tabIds = tabIds,
|
tabCollectionStorage.cachedTabCollections.isNotEmpty() -> {
|
||||||
saveCollectionStep = step,
|
showChooseCollectionDialog(sessionList)
|
||||||
selectedTabIds = tabIds
|
}
|
||||||
)
|
else -> {
|
||||||
navController.navigate(directions)
|
showAddNewCollectionDialog(sessionList)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShareTabsClicked(private: Boolean) {
|
override fun onShareTabsClicked(private: Boolean) {
|
||||||
|
@ -89,39 +111,48 @@ class DefaultTabTrayController(
|
||||||
navController.navigate(directions)
|
navController.navigate(directions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
override fun onCloseAllTabsClicked(private: Boolean) {
|
override fun onCloseAllTabsClicked(private: Boolean) {
|
||||||
val sessionManager = activity.components.core.sessionManager
|
val sessionsToClose = if (private) {
|
||||||
val tabs = getListOfSessions(private)
|
HomeFragment.ALL_PRIVATE_TABS
|
||||||
|
|
||||||
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)
|
|
||||||
} else {
|
} else {
|
||||||
activity.getString(R.string.snackbar_tabs_closed)
|
HomeFragment.ALL_NORMAL_TABS
|
||||||
}
|
}
|
||||||
|
|
||||||
showUndoSnackbar(snackbarMessage, snapshot)
|
dismissTabTrayAndNavigateHome(sessionsToClose)
|
||||||
dismissTabTray()
|
}
|
||||||
|
|
||||||
|
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)
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
private fun getListOfSessions(private: Boolean): List<Session> {
|
private fun getListOfSessions(private: Boolean): List<Session> {
|
||||||
return activity.components.core.sessionManager.sessionsOfType(private = private).toList()
|
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
|
package org.mozilla.fenix.tabtray
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
import android.widget.EditText
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatDialogFragment
|
import androidx.appcompat.app.AppCompatDialogFragment
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.fragment.findNavController
|
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.view.*
|
||||||
import kotlinx.android.synthetic.main.component_tabstray_fab.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.*
|
||||||
import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.view.*
|
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.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import mozilla.components.browser.session.Session
|
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.state.state.TabSessionState
|
||||||
|
import mozilla.components.browser.thumbnails.loader.ThumbnailLoader
|
||||||
import mozilla.components.feature.tab.collections.TabCollection
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
import mozilla.components.feature.tabs.TabsUseCases
|
import mozilla.components.feature.tabs.TabsUseCases
|
||||||
import mozilla.components.feature.tabs.tabstray.TabsFeature
|
import mozilla.components.feature.tabs.tabstray.TabsFeature
|
||||||
import mozilla.components.lib.state.ext.consumeFrom
|
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.base.feature.ViewBoundFeatureWrapper
|
||||||
|
import mozilla.components.support.ktx.android.view.showKeyboard
|
||||||
import org.mozilla.fenix.HomeActivity
|
import org.mozilla.fenix.HomeActivity
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.browser.BrowserFragmentDirections
|
||||||
import org.mozilla.fenix.components.FenixSnackbar
|
import org.mozilla.fenix.components.FenixSnackbar
|
||||||
|
import org.mozilla.fenix.components.StoreProvider
|
||||||
import org.mozilla.fenix.components.TabCollectionStorage
|
import org.mozilla.fenix.components.TabCollectionStorage
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
import org.mozilla.fenix.ext.components
|
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.requireComponents
|
||||||
import org.mozilla.fenix.ext.settings
|
import org.mozilla.fenix.ext.settings
|
||||||
|
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentState.Mode
|
||||||
import org.mozilla.fenix.utils.allowUndo
|
import org.mozilla.fenix.utils.allowUndo
|
||||||
|
|
||||||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||||
class TabTrayDialogFragment : AppCompatDialogFragment() {
|
class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
|
||||||
|
private val args by navArgs<TabTrayDialogFragmentArgs>()
|
||||||
|
|
||||||
private val tabsFeature = ViewBoundFeatureWrapper<TabsFeature>()
|
private val tabsFeature = ViewBoundFeatureWrapper<TabsFeature>()
|
||||||
private var _tabTrayView: TabTrayView? = null
|
private var _tabTrayView: TabTrayView? = null
|
||||||
private val tabTrayView: TabTrayView
|
private val tabTrayView: TabTrayView
|
||||||
get() = _tabTrayView!!
|
get() = _tabTrayView!!
|
||||||
|
private lateinit var tabTrayDialogStore: TabTrayDialogFragmentStore
|
||||||
|
|
||||||
private val snackbarAnchor: View?
|
private val snackbarAnchor: View?
|
||||||
get() = if (tabTrayView.fabView.new_tab_button.isVisible) tabTrayView.fabView.new_tab_button
|
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 {
|
private val removeTabUseCase = object : TabsUseCases.RemoveTabUseCase {
|
||||||
override fun invoke(sessionId: String) {
|
override fun invoke(sessionId: String) {
|
||||||
requireContext().components.analytics.metrics.track(Event.ClosedExistingTab)
|
requireContext().components.analytics.metrics.track(Event.ClosedExistingTab)
|
||||||
showUndoSnackbarForTab(sessionId)
|
showUndoSnackbarForTab(sessionId)
|
||||||
requireComponents.useCases.tabsUseCases.removeTab(sessionId)
|
removeIfNotLastTab(sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invoke(session: Session) {
|
override fun invoke(session: Session) {
|
||||||
requireContext().components.analytics.metrics.track(Event.ClosedExistingTab)
|
requireContext().components.analytics.metrics.track(Event.ClosedExistingTab)
|
||||||
showUndoSnackbarForTab(session.id)
|
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,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
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) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
super.onConfigurationChanged(newConfig)
|
super.onConfigurationChanged(newConfig)
|
||||||
|
@ -120,15 +165,23 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
val isPrivate = (activity as HomeActivity).browsingModeManager.mode.isPrivate
|
val isPrivate = (activity as HomeActivity).browsingModeManager.mode.isPrivate
|
||||||
|
|
||||||
|
val thumbnailLoader = ThumbnailLoader(requireContext().components.core.thumbnailStorage)
|
||||||
|
val adapter = FenixTabsAdapter(requireContext(), thumbnailLoader)
|
||||||
|
|
||||||
_tabTrayView = TabTrayView(
|
_tabTrayView = TabTrayView(
|
||||||
view.tabLayout,
|
view.tabLayout,
|
||||||
|
adapter,
|
||||||
interactor = TabTrayFragmentInteractor(
|
interactor = TabTrayFragmentInteractor(
|
||||||
DefaultTabTrayController(
|
DefaultTabTrayController(
|
||||||
activity = (activity as HomeActivity),
|
activity = (activity as HomeActivity),
|
||||||
navController = findNavController(),
|
navController = findNavController(),
|
||||||
dismissTabTray = ::dismissAllowingStateLoss,
|
dismissTabTray = ::dismissAllowingStateLoss,
|
||||||
showUndoSnackbar = ::showUndoSnackbar,
|
dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome,
|
||||||
registerCollectionStorageObserver = ::registerCollectionStorageObserver
|
registerCollectionStorageObserver = ::registerCollectionStorageObserver,
|
||||||
|
tabTrayDialogFragmentStore = tabTrayDialogStore,
|
||||||
|
selectTabUseCase = selectTabUseCase,
|
||||||
|
showChooseCollectionDialog = ::showChooseCollectionDialog,
|
||||||
|
showAddNewCollectionDialog = ::showAddNewCollectionDialog
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
isPrivate = isPrivate,
|
isPrivate = isPrivate,
|
||||||
|
@ -145,7 +198,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
|
||||||
|
|
||||||
tabsFeature.set(
|
tabsFeature.set(
|
||||||
TabsFeature(
|
TabsFeature(
|
||||||
tabTrayView.view.tabsTray,
|
adapter,
|
||||||
view.context.components.core.store,
|
view.context.components.core.store,
|
||||||
selectTabUseCase,
|
selectTabUseCase,
|
||||||
removeTabUseCase,
|
removeTabUseCase,
|
||||||
|
@ -176,8 +229,11 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
consumeFrom(requireComponents.core.store) {
|
consumeFrom(requireComponents.core.store) {
|
||||||
|
tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.BrowserStateChanged(it))
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeFrom(tabTrayDialogStore) {
|
||||||
tabTrayView.updateState(it)
|
tabTrayView.updateState(it)
|
||||||
navigateHomeIfNeeded(it)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,11 +247,21 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
|
||||||
|
|
||||||
private fun showUndoSnackbarForTab(sessionId: String) {
|
private fun showUndoSnackbarForTab(sessionId: String) {
|
||||||
val sessionManager = view?.context?.components?.core?.sessionManager
|
val sessionManager = view?.context?.components?.core?.sessionManager
|
||||||
|
|
||||||
val snapshot = sessionManager
|
val snapshot = sessionManager
|
||||||
?.findSessionById(sessionId)?.let {
|
?.findSessionById(sessionId)?.let {
|
||||||
sessionManager.createSessionSnapshot(it)
|
sessionManager.createSessionSnapshot(it)
|
||||||
} ?: return
|
} ?: 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 state = snapshot.engineSession?.saveState()
|
||||||
val isSelected = sessionId == requireComponents.core.store.state.selectedTabId ?: false
|
val isSelected = sessionId == requireComponents.core.store.state.selectedTabId ?: false
|
||||||
|
|
||||||
|
@ -205,13 +271,8 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
|
||||||
getString(R.string.snackbar_tab_closed)
|
getString(R.string.snackbar_tab_closed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is the last tab of this session type
|
lifecycleScope.allowUndo(
|
||||||
val isLastOpenTab = sessionManager.sessions.filter { snapshot.session.private }.size == 1
|
requireView().tabLayout,
|
||||||
val rootView = if (isLastOpenTab) { requireActivity().getRootView()!! } else { requireView().tabLayout }
|
|
||||||
val anchorView = if (isLastOpenTab) { null } else { snackbarAnchor }
|
|
||||||
|
|
||||||
requireActivity().lifecycleScope.allowUndo(
|
|
||||||
rootView,
|
|
||||||
snackbarMessage,
|
snackbarMessage,
|
||||||
getString(R.string.snackbar_deleted_undo),
|
getString(R.string.snackbar_deleted_undo),
|
||||||
{
|
{
|
||||||
|
@ -220,18 +281,14 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
|
||||||
},
|
},
|
||||||
operation = { },
|
operation = { },
|
||||||
elevation = ELEVATION,
|
elevation = ELEVATION,
|
||||||
paddedForBottomToolbar = isLastOpenTab,
|
anchorView = snackbarAnchor
|
||||||
anchorView = anchorView
|
|
||||||
)
|
)
|
||||||
|
|
||||||
dismissTabTrayIfNecessary()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun dismissTabTrayIfNecessary() {
|
private fun dismissTabTrayAndNavigateHome(sessionId: String) {
|
||||||
if (requireComponents.core.sessionManager.sessions.size == 1) {
|
val directions = BrowserFragmentDirections.actionGlobalHome(sessionToDelete = sessionId)
|
||||||
findNavController().popBackStack(R.id.homeFragment, false)
|
findNavController().navigate(directions)
|
||||||
dismissAllowingStateLoss()
|
dismissAllowingStateLoss()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
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() {
|
private fun registerCollectionStorageObserver() {
|
||||||
requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this)
|
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) {
|
private fun showCollectionSnackbar(tabSize: Int, isNewCollection: Boolean = false) {
|
||||||
view.let {
|
view.let {
|
||||||
val messageStringRes = when {
|
val messageStringRes = when {
|
||||||
|
@ -313,21 +341,101 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
override fun onBackPressed(): Boolean {
|
||||||
private const val ELEVATION = 80f
|
if (!tabTrayView.onBackPressed()) {
|
||||||
private const val FRAGMENT_TAG = "tabTrayDialogFragment"
|
dismiss()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
fun show(fragmentManager: FragmentManager) {
|
private fun showChooseCollectionDialog(sessionList: List<Session>) {
|
||||||
// If we've killed the fragmentManager. Let's not try to show the tabs tray.
|
context?.let {
|
||||||
if (fragmentManager.isDestroyed) {
|
val tabCollectionStorage = it.components.core.tabCollectionStorage
|
||||||
return
|
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
|
val builder = AlertDialog.Builder(it).setTitle(R.string.tab_tray_select_collection)
|
||||||
// a user somehow manages to trigger `show()` twice before we present the dialog.
|
.setView(customLayout)
|
||||||
if (fragmentManager.findFragmentByTag(FRAGMENT_TAG) == null) {
|
.setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||||
TabTrayDialogFragment().showNow(fragmentManager, FRAGMENT_TAG)
|
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
|
package org.mozilla.fenix.tabtray
|
||||||
|
|
||||||
|
import mozilla.components.concept.tabstray.Tab
|
||||||
|
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
interface TabTrayInteractor {
|
interface TabTrayInteractor {
|
||||||
|
/**
|
||||||
|
* Called when user clicks the new tab button.
|
||||||
|
*/
|
||||||
fun onNewTabTapped(private: Boolean)
|
fun onNewTabTapped(private: Boolean)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when tab tray should be dismissed.
|
||||||
|
*/
|
||||||
fun onTabTrayDismissed()
|
fun onTabTrayDismissed()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when user clicks the share tabs button.
|
||||||
|
*/
|
||||||
fun onShareTabsClicked(private: Boolean)
|
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)
|
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.
|
* Interactor for the tab tray fragment.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
class TabTrayFragmentInteractor(private val controller: TabTrayController) : TabTrayInteractor {
|
class TabTrayFragmentInteractor(private val controller: TabTrayController) : TabTrayInteractor {
|
||||||
override fun onNewTabTapped(private: Boolean) {
|
override fun onNewTabTapped(private: Boolean) {
|
||||||
controller.onNewTabTapped(private)
|
controller.onNewTabTapped(private)
|
||||||
|
@ -28,11 +81,35 @@ class TabTrayFragmentInteractor(private val controller: TabTrayController) : Tab
|
||||||
controller.onShareTabsClicked(private)
|
controller.onShareTabsClicked(private)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveToCollectionClicked() {
|
override fun onSaveToCollectionClicked(selectedTabs: Set<Tab>) {
|
||||||
controller.onSaveToCollectionClicked()
|
controller.onSaveToCollectionClicked(selectedTabs)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCloseAllTabsClicked(private: Boolean) {
|
override fun onCloseAllTabsClicked(private: Boolean) {
|
||||||
controller.onCloseAllTabsClicked(private)
|
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.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.accessibility.AccessibilityEvent
|
import android.view.accessibility.AccessibilityEvent
|
||||||
|
import androidx.annotation.IdRes
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.constraintlayout.widget.ConstraintSet
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.lifecycle.LifecycleCoroutineScope
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import kotlinx.android.extensions.LayoutContainer
|
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.view.*
|
||||||
import kotlinx.android.synthetic.main.component_tabstray_fab.view.*
|
import kotlinx.android.synthetic.main.component_tabstray_fab.view.*
|
||||||
import kotlinx.android.synthetic.main.tabs_tray_tab_counter.*
|
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.normalTabs
|
||||||
import mozilla.components.browser.state.selector.privateTabs
|
import mozilla.components.browser.state.selector.privateTabs
|
||||||
import mozilla.components.browser.state.state.BrowserState
|
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.R
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
|
@ -41,6 +45,7 @@ import org.mozilla.fenix.ext.settings
|
||||||
@Suppress("LongParameterList", "TooManyFunctions", "LargeClass")
|
@Suppress("LongParameterList", "TooManyFunctions", "LargeClass")
|
||||||
class TabTrayView(
|
class TabTrayView(
|
||||||
private val container: ViewGroup,
|
private val container: ViewGroup,
|
||||||
|
private val tabsAdapter: FenixTabsAdapter,
|
||||||
private val interactor: TabTrayInteractor,
|
private val interactor: TabTrayInteractor,
|
||||||
isPrivate: Boolean,
|
isPrivate: Boolean,
|
||||||
startingInLandscape: Boolean,
|
startingInLandscape: Boolean,
|
||||||
|
@ -50,16 +55,20 @@ class TabTrayView(
|
||||||
val fabView = LayoutInflater.from(container.context)
|
val fabView = LayoutInflater.from(container.context)
|
||||||
.inflate(R.layout.component_tabstray_fab, container, true)
|
.inflate(R.layout.component_tabstray_fab, container, true)
|
||||||
|
|
||||||
|
private val hasAccessibilityEnabled = container.context.settings().accessibilityServicesEnabled
|
||||||
|
|
||||||
val view = LayoutInflater.from(container.context)
|
val view = LayoutInflater.from(container.context)
|
||||||
.inflate(R.layout.component_tabstray, container, true)
|
.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 behavior = BottomSheetBehavior.from(view.tab_wrapper)
|
||||||
|
|
||||||
private val tabTrayItemMenu: TabTrayItemMenu
|
private val tabTrayItemMenu: TabTrayItemMenu
|
||||||
private var menu: BrowserMenu? = null
|
private var menu: BrowserMenu? = null
|
||||||
|
|
||||||
|
private var tabsTouchHelper: TabsTouchHelper
|
||||||
|
|
||||||
private var hasLoaded = false
|
private var hasLoaded = false
|
||||||
|
|
||||||
override val containerView: View?
|
override val containerView: View?
|
||||||
|
@ -68,8 +77,6 @@ class TabTrayView(
|
||||||
init {
|
init {
|
||||||
container.context.components.analytics.metrics.track(Event.TabsTrayOpened)
|
container.context.components.analytics.metrics.track(Event.TabsTrayOpened)
|
||||||
|
|
||||||
val hasAccessibilityEnabled = view.context.settings().accessibilityServicesEnabled
|
|
||||||
|
|
||||||
toggleFabText(isPrivate)
|
toggleFabText(isPrivate)
|
||||||
|
|
||||||
behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
||||||
|
@ -118,27 +125,34 @@ class TabTrayView(
|
||||||
|
|
||||||
setTopOffset(startingInLandscape)
|
setTopOffset(startingInLandscape)
|
||||||
|
|
||||||
(view.tabsTray as? BrowserTabsTray)?.also { tray ->
|
view.tabsTray.apply {
|
||||||
TabsTouchHelper(tray.tabsAdapter).attachToRecyclerView(tray)
|
layoutManager = LinearLayoutManager(container.context).apply {
|
||||||
(tray.tabsAdapter as? FenixTabsAdapter)?.also { adapter ->
|
reverseLayout = true
|
||||||
adapter.onTabsUpdated = {
|
stackFromEnd = true
|
||||||
if (hasAccessibilityEnabled) {
|
}
|
||||||
adapter.notifyDataSetChanged()
|
adapter = tabsAdapter
|
||||||
}
|
|
||||||
if (!hasLoaded) {
|
tabsTouchHelper = TabsTouchHelper(tabsAdapter)
|
||||||
hasLoaded = true
|
tabsTouchHelper.attachToRecyclerView(this)
|
||||||
scrollToTab(view.context.components.core.store.state.selectedTabId)
|
|
||||||
if (view.context.settings().accessibilityServicesEnabled) {
|
tabsAdapter.tabTrayInteractor = interactor
|
||||||
lifecycleScope.launch {
|
tabsAdapter.onTabsUpdated = {
|
||||||
delay(SELECTION_DELAY.toLong())
|
if (hasAccessibilityEnabled) {
|
||||||
lifecycleScope.launch(Main) {
|
tabsAdapter.notifyDataSetChanged()
|
||||||
tray.layoutManager?.findViewByPosition(selectedBrowserTabIndex)
|
}
|
||||||
?.requestFocus()
|
if (!hasLoaded) {
|
||||||
tray.layoutManager?.findViewByPosition(selectedBrowserTabIndex)
|
hasLoaded = true
|
||||||
?.sendAccessibilityEvent(
|
scrollToTab(view.context.components.core.store.state.selectedTabId)
|
||||||
AccessibilityEvent.TYPE_VIEW_FOCUSED
|
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(
|
is TabTrayItemMenu.Item.ShareAllTabs -> interactor.onShareTabsClicked(
|
||||||
isPrivateModeSelected
|
isPrivateModeSelected
|
||||||
)
|
)
|
||||||
is TabTrayItemMenu.Item.SaveToCollection -> interactor.onSaveToCollectionClicked()
|
is TabTrayItemMenu.Item.SaveToCollection -> interactor.onEnterMultiselect()
|
||||||
is TabTrayItemMenu.Item.CloseAllTabs -> interactor.onCloseAllTabsClicked(
|
is TabTrayItemMenu.Item.CloseAllTabs -> interactor.onCloseAllTabsClicked(
|
||||||
isPrivateModeSelected
|
isPrivateModeSelected
|
||||||
)
|
)
|
||||||
|
@ -173,6 +187,10 @@ class TabTrayView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
adjustNewTabButtonsForNormalMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun adjustNewTabButtonsForNormalMode() {
|
||||||
view.tab_tray_new_tab.apply {
|
view.tab_tray_new_tab.apply {
|
||||||
isVisible = hasAccessibilityEnabled
|
isVisible = hasAccessibilityEnabled
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
|
@ -208,7 +226,7 @@ class TabTrayView(
|
||||||
toggleFabText(isPrivateModeSelected)
|
toggleFabText(isPrivateModeSelected)
|
||||||
filterTabs.invoke(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)
|
scrollToTab(view.context.components.core.store.state.selectedTabId)
|
||||||
|
|
||||||
if (isPrivateModeSelected) {
|
if (isPrivateModeSelected) {
|
||||||
|
@ -224,32 +242,168 @@ class TabTrayView(
|
||||||
override fun onTabUnselected(tab: TabLayout.Tab?) { /*noop*/
|
override fun onTabUnselected(tab: TabLayout.Tab?) { /*noop*/
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateState(state: BrowserState) {
|
var mode: TabTrayDialogFragmentState.Mode = TabTrayDialogFragmentState.Mode.Normal
|
||||||
view.let {
|
private set
|
||||||
val hasNoTabs = if (isPrivateModeSelected) {
|
|
||||||
state.privateTabs.isEmpty()
|
|
||||||
} else {
|
|
||||||
state.normalTabs.isEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
view.tab_tray_empty_view.isVisible = hasNoTabs
|
fun updateState(state: TabTrayDialogFragmentState) {
|
||||||
if (hasNoTabs) {
|
val oldMode = mode
|
||||||
view.tab_tray_empty_view.text = if (isPrivateModeSelected) {
|
|
||||||
view.context.getString(R.string.no_private_tabs_description)
|
if (oldMode::class != state.mode::class && view.context.settings().accessibilityServicesEnabled) {
|
||||||
} else {
|
view.announceForAccessibility(
|
||||||
view.context?.getString(R.string.no_open_tabs_description)
|
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) {
|
if (oldMode.selectedItems != state.mode.selectedItems) {
|
||||||
View.INVISIBLE
|
val unselectedItems = oldMode.selectedItems - state.mode.selectedItems
|
||||||
} else {
|
|
||||||
View.VISIBLE
|
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}"
|
private fun ConstraintLayout.setChildWPercent(percentage: Float, @IdRes childId: Int) {
|
||||||
updateTabCounterContentDescription(state.normalTabs.size)
|
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?) {
|
fun scrollToTab(sessionId: String?) {
|
||||||
(view.tabsTray as? BrowserTabsTray)?.also { tray ->
|
view.tabsTray.apply {
|
||||||
val tabs = if (isPrivateModeSelected) {
|
val tabs = if (isPrivateModeSelected) {
|
||||||
view.context.components.core.store.state.privateTabs
|
view.context.components.core.store.state.privateTabs
|
||||||
} else {
|
} else {
|
||||||
|
@ -298,7 +456,7 @@ class TabTrayView(
|
||||||
val selectedBrowserTabIndex = tabs
|
val selectedBrowserTabIndex = tabs
|
||||||
.indexOfFirst { it.id == sessionId }
|
.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 EXPAND_AT_SIZE = 3
|
||||||
private const val SLIDE_OFFSET = 0
|
private const val SLIDE_OFFSET = 0
|
||||||
private const val SELECTION_DELAY = 500
|
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.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.appcompat.widget.AppCompatImageButton
|
import androidx.appcompat.widget.AppCompatImageButton
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import mozilla.components.browser.state.state.MediaState
|
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.TabViewHolder
|
||||||
|
import mozilla.components.browser.tabstray.TabsTrayStyling
|
||||||
import mozilla.components.browser.tabstray.thumbnail.TabThumbnailView
|
import mozilla.components.browser.tabstray.thumbnail.TabThumbnailView
|
||||||
import mozilla.components.browser.toolbar.MAX_URI_LENGTH
|
import mozilla.components.browser.toolbar.MAX_URI_LENGTH
|
||||||
import mozilla.components.concept.tabstray.Tab
|
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 mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
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.components
|
||||||
|
import org.mozilla.fenix.ext.getMediaStateForSession
|
||||||
import org.mozilla.fenix.ext.increaseTapArea
|
import org.mozilla.fenix.ext.increaseTapArea
|
||||||
import org.mozilla.fenix.ext.removeAndDisable
|
import org.mozilla.fenix.ext.removeAndDisable
|
||||||
import org.mozilla.fenix.ext.removeTouchDelegate
|
import org.mozilla.fenix.ext.removeTouchDelegate
|
||||||
import org.mozilla.fenix.ext.showAndEnable
|
import org.mozilla.fenix.ext.showAndEnable
|
||||||
import org.mozilla.fenix.ext.toTab
|
import org.mozilla.fenix.utils.Do
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -40,8 +45,10 @@ import kotlin.math.max
|
||||||
class TabTrayViewHolder(
|
class TabTrayViewHolder(
|
||||||
itemView: View,
|
itemView: View,
|
||||||
private val imageLoader: ImageLoader,
|
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) {
|
) : TabViewHolder(itemView) {
|
||||||
|
|
||||||
private val titleView: TextView = itemView.findViewById(R.id.mozac_browser_tabstray_title)
|
private val titleView: TextView = itemView.findViewById(R.id.mozac_browser_tabstray_title)
|
||||||
private val closeView: AppCompatImageButton =
|
private val closeView: AppCompatImageButton =
|
||||||
itemView.findViewById(R.id.mozac_browser_tabstray_close)
|
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.
|
* 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>) {
|
override fun bind(
|
||||||
// This is a hack to workaround a bug in a-c.
|
tab: Tab,
|
||||||
// https://github.com/mozilla-mobile/android-components/issues/7186
|
isSelected: Boolean,
|
||||||
val isSelected2 = tab.id == getSelectedTabId()
|
styling: TabsTrayStyling,
|
||||||
|
observable: Observable<TabsTray.Observer>
|
||||||
|
) {
|
||||||
this.tab = tab
|
this.tab = tab
|
||||||
|
|
||||||
// Basic text
|
// Basic text
|
||||||
|
@ -69,7 +78,7 @@ class TabTrayViewHolder(
|
||||||
updateCloseButtonDescription(tab.title)
|
updateCloseButtonDescription(tab.title)
|
||||||
|
|
||||||
// Drawables and theme
|
// Drawables and theme
|
||||||
updateBackgroundColor(isSelected2)
|
updateBackgroundColor(isSelected)
|
||||||
|
|
||||||
if (tab.thumbnail != null) {
|
if (tab.thumbnail != null) {
|
||||||
thumbnailView.setImageBitmap(tab.thumbnail)
|
thumbnailView.setImageBitmap(tab.thumbnail)
|
||||||
|
@ -79,16 +88,15 @@ class TabTrayViewHolder(
|
||||||
|
|
||||||
// Media state
|
// Media state
|
||||||
playPauseButtonView.increaseTapArea(PLAY_PAUSE_BUTTON_EXTRA_DPS)
|
playPauseButtonView.increaseTapArea(PLAY_PAUSE_BUTTON_EXTRA_DPS)
|
||||||
val session = itemView.context?.components?.core?.sessionManager?.findSessionById(tab.id)
|
|
||||||
with(playPauseButtonView) {
|
with(playPauseButtonView) {
|
||||||
invalidate()
|
invalidate()
|
||||||
when (session?.toTab(itemView.context)?.mediaState) {
|
Do exhaustive when (store.state.getMediaStateForSession(tab.id)) {
|
||||||
MediaState.State.PAUSED -> {
|
MediaState.State.PAUSED -> {
|
||||||
showAndEnable()
|
showAndEnable()
|
||||||
contentDescription =
|
contentDescription =
|
||||||
context.getString(R.string.mozac_feature_media_notification_action_play)
|
context.getString(R.string.mozac_feature_media_notification_action_play)
|
||||||
setImageDrawable(
|
setImageDrawable(
|
||||||
androidx.appcompat.content.res.AppCompatResources.getDrawable(
|
AppCompatResources.getDrawable(
|
||||||
context,
|
context,
|
||||||
R.drawable.tab_tray_play_with_background
|
R.drawable.tab_tray_play_with_background
|
||||||
)
|
)
|
||||||
|
@ -100,7 +108,7 @@ class TabTrayViewHolder(
|
||||||
contentDescription =
|
contentDescription =
|
||||||
context.getString(R.string.mozac_feature_media_notification_action_pause)
|
context.getString(R.string.mozac_feature_media_notification_action_pause)
|
||||||
setImageDrawable(
|
setImageDrawable(
|
||||||
androidx.appcompat.content.res.AppCompatResources.getDrawable(
|
AppCompatResources.getDrawable(
|
||||||
context,
|
context,
|
||||||
R.drawable.tab_tray_pause_with_background
|
R.drawable.tab_tray_pause_with_background
|
||||||
)
|
)
|
||||||
|
@ -115,16 +123,15 @@ class TabTrayViewHolder(
|
||||||
}
|
}
|
||||||
|
|
||||||
playPauseButtonView.setOnClickListener {
|
playPauseButtonView.setOnClickListener {
|
||||||
val mState = session?.toTab(itemView.context)?.mediaState
|
Do exhaustive when (store.state.getMediaStateForSession(tab.id)) {
|
||||||
when (mState) {
|
|
||||||
MediaState.State.PLAYING -> {
|
MediaState.State.PLAYING -> {
|
||||||
itemView.context.components.analytics.metrics.track(Event.TabMediaPause)
|
metrics.track(Event.TabMediaPause)
|
||||||
itemView.context.components.core.store.state.media.pauseIfPlaying()
|
store.state.media.pauseIfPlaying()
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaState.State.PAUSED -> {
|
MediaState.State.PAUSED -> {
|
||||||
itemView.context.components.analytics.metrics.track(Event.TabMediaPlay)
|
metrics.track(Event.TabMediaPlay)
|
||||||
itemView.context.components.core.store.state.media.playIfPaused()
|
store.state.media.playIfPaused()
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaState.State.NONE -> throw AssertionError(
|
MediaState.State.NONE -> throw AssertionError(
|
||||||
|
@ -133,10 +140,6 @@ class TabTrayViewHolder(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
itemView.setOnClickListener {
|
|
||||||
observable.notifyObservers { onTabSelected(tab) }
|
|
||||||
}
|
|
||||||
|
|
||||||
closeView.setOnClickListener {
|
closeView.setOnClickListener {
|
||||||
observable.notifyObservers { onTabClosed(tab) }
|
observable.notifyObservers { onTabClosed(tab) }
|
||||||
}
|
}
|
||||||
|
@ -189,7 +192,7 @@ class TabTrayViewHolder(
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun updateAccessibilityRowIndex(item: View, newIndex: Int) {
|
internal fun updateAccessibilityRowIndex(item: View, newIndex: Int) {
|
||||||
item.setAccessibilityDelegate(object : View.AccessibilityDelegate() {
|
item.accessibilityDelegate = object : View.AccessibilityDelegate() {
|
||||||
override fun onInitializeAccessibilityNodeInfo(
|
override fun onInitializeAccessibilityNodeInfo(
|
||||||
host: View?,
|
host: View?,
|
||||||
info: AccessibilityNodeInfo?
|
info: AccessibilityNodeInfo?
|
||||||
|
@ -208,7 +211,7 @@ class TabTrayViewHolder(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -13,7 +13,6 @@ import android.view.LayoutInflater
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.core.view.isGone
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.marginTop
|
import androidx.core.view.marginTop
|
||||||
import kotlinx.android.synthetic.main.tracking_protection_onboarding_popup.*
|
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.R
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
import org.mozilla.fenix.components.metrics.MetricController
|
import org.mozilla.fenix.components.metrics.MetricController
|
||||||
|
import org.mozilla.fenix.components.toolbar.ToolbarPosition
|
||||||
import org.mozilla.fenix.ext.increaseTapArea
|
import org.mozilla.fenix.ext.increaseTapArea
|
||||||
import org.mozilla.fenix.utils.Settings
|
import org.mozilla.fenix.utils.Settings
|
||||||
|
|
||||||
|
@ -63,10 +63,10 @@ class TrackingProtectionOverlay(
|
||||||
|
|
||||||
val layout = LayoutInflater.from(context)
|
val layout = LayoutInflater.from(context)
|
||||||
.inflate(R.layout.tracking_protection_onboarding_popup, null)
|
.inflate(R.layout.tracking_protection_onboarding_popup, null)
|
||||||
val isBottomToolbar = settings.shouldUseBottomToolbar
|
val toolbarPosition = settings.toolbarPosition
|
||||||
|
|
||||||
layout.drop_down_triangle.isGone = isBottomToolbar
|
layout.drop_down_triangle.isVisible = toolbarPosition == ToolbarPosition.TOP
|
||||||
layout.pop_up_triangle.isVisible = isBottomToolbar
|
layout.pop_up_triangle.isVisible = toolbarPosition == ToolbarPosition.BOTTOM
|
||||||
|
|
||||||
layout.onboarding_message.text =
|
layout.onboarding_message.text =
|
||||||
context.getString(
|
context.getString(
|
||||||
|
@ -91,11 +91,7 @@ class TrackingProtectionOverlay(
|
||||||
|
|
||||||
val xOffset = triangleMarginStartPx + triangleWidthPx / 2
|
val xOffset = triangleMarginStartPx + triangleWidthPx / 2
|
||||||
|
|
||||||
val gravity = if (isBottomToolbar) {
|
val gravity = Gravity.START or toolbarPosition.androidGravity
|
||||||
Gravity.START or Gravity.BOTTOM
|
|
||||||
} else {
|
|
||||||
Gravity.START or Gravity.TOP
|
|
||||||
}
|
|
||||||
|
|
||||||
trackingOnboardingDialog.apply {
|
trackingOnboardingDialog.apply {
|
||||||
setContentView(layout)
|
setContentView(layout)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* 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
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
package org.mozilla.fenix.trackingprotection
|
package org.mozilla.fenix.trackingprotection
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* 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
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
package org.mozilla.fenix.trackingprotectionexceptions
|
package org.mozilla.fenix.trackingprotectionexceptions
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* 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
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
package org.mozilla.fenix.trackingprotectionexceptions
|
package org.mozilla.fenix.trackingprotectionexceptions
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ import org.mozilla.fenix.Config
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||||
import org.mozilla.fenix.components.metrics.MozillaProductDetector
|
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.components
|
||||||
import org.mozilla.fenix.ext.getPreferenceKey
|
import org.mozilla.fenix.ext.getPreferenceKey
|
||||||
import org.mozilla.fenix.settings.PhoneFeature
|
import org.mozilla.fenix.settings.PhoneFeature
|
||||||
|
@ -463,6 +464,9 @@ class Settings(private val appContext: Context) : PreferencesHolder {
|
||||||
default = !touchExplorationIsEnabled && !switchServiceIsEnabled
|
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,
|
* 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
|
* 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() {
|
get() {
|
||||||
return when (savedLoginsSortingStrategyString) {
|
return when (savedLoginsSortingStrategyString) {
|
||||||
SavedLoginsFragment.SORTING_STRATEGY_ALPHABETICALLY -> SortingStrategy.Alphabetically(
|
SavedLoginsFragment.SORTING_STRATEGY_ALPHABETICALLY -> SortingStrategy.Alphabetically(
|
||||||
appContext
|
appContext.components.publicSuffixList
|
||||||
)
|
)
|
||||||
SavedLoginsFragment.SORTING_STRATEGY_LAST_USED -> SortingStrategy.LastUsed(
|
SavedLoginsFragment.SORTING_STRATEGY_LAST_USED -> SortingStrategy.LastUsed
|
||||||
appContext
|
else -> SortingStrategy.Alphabetically(appContext.components.publicSuffixList)
|
||||||
)
|
|
||||||
else -> SortingStrategy.Alphabetically(appContext)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue