1
0
Fork 0

Copione merged onto master
continuous-integration/drone/push Build is failing Details

master
blallo 2020-07-27 20:42:38 +02:00
commit 9a4ae5a0b1
174 changed files with 4389 additions and 1710 deletions

1
.gitignore vendored
View File

@ -83,7 +83,6 @@ gen-external-apklibs
.leanplum_token
.adjust_token
.sentry_token
.digital_asset_links_token
.mls_token

View File

@ -354,21 +354,6 @@ android.applicationVariants.all { variant ->
println("X_X")
}
// -------------------------------------------------------------------------------------------------
// Digital Asset Links: Read token from local file if it exists
// -------------------------------------------------------------------------------------------------
print("Digital Asset Links token: ")
try {
def token = new File("${rootDir}/.digital_asset_links_token").text.trim()
buildConfigField 'String', 'DIGITAL_ASSET_LINKS_TOKEN', '"' + token + '"'
println "(Added from .digital_asset_links_token file)"
} catch (FileNotFoundException ignored) {
buildConfigField 'String', 'DIGITAL_ASSET_LINKS_TOKEN', 'null'
println("X_X")
}
// -------------------------------------------------------------------------------------------------
// MLS: Read token from local file if it exists
// -------------------------------------------------------------------------------------------------
@ -413,6 +398,7 @@ dependencies {
implementation Deps.leanplum_fcm
implementation Deps.mozilla_concept_engine
implementation Deps.mozilla_concept_menu
implementation Deps.mozilla_concept_push
implementation Deps.mozilla_concept_storage
implementation Deps.mozilla_concept_sync

View File

@ -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
)
)
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -96,6 +96,7 @@ class ContextMenusTest {
}
}
@Ignore("Test failures: https://github.com/mozilla-mobile/fenix/issues/12473")
@Test
fun verifyContextCopyLink() {
val pageLinks =

View File

@ -9,6 +9,7 @@ import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
@ -124,6 +125,7 @@ class DeepLinkTest {
}
}
@Ignore("Crashing, see: https://github.com/mozilla-mobile/fenix/issues/11239")
@Test
fun openSettingsSearchEngine() {
robot.openSettingsSearchEngine {

View File

@ -11,6 +11,7 @@ import mozilla.components.browser.storage.sync.PlacesHistoryStorage
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
@ -164,6 +165,7 @@ class HistoryTest {
}
}
@Ignore("Failing test: https://github.com/mozilla-mobile/fenix/issues/12893")
@Test
fun deleteAllHistoryTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)

View File

@ -66,6 +66,7 @@ class NavigationToolbarTest {
}
}
@Ignore("Flaky test: https://github.com/mozilla-mobile/fenix/issues/12894")
@Test
fun goForwardTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)

View File

@ -66,6 +66,7 @@ class SearchTest {
}
}
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/12968")
@Test
fun shortcutSearchEngineSettingsTest() {
homeScreen {

View File

@ -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 {
}
}
}

View File

@ -60,6 +60,7 @@ class SettingsBasicsTest {
}
}
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/12968")
@Test
// Walks through settings menu and sub-menus to ensure all items are present
fun settingsMenuBasicsItemsTests() {
@ -90,6 +91,7 @@ class SettingsBasicsTest {
}
}
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/12968")
@Test
fun selectNewDefaultSearchEngine() {
// Goes through the settings and changes the default search engine, then verifies it has changed.

View File

@ -10,6 +10,7 @@ import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
@ -216,6 +217,7 @@ class SmokeTest {
}
}
@Ignore("Flaky test: https://github.com/mozilla-mobile/fenix/issues/12899")
@Test
fun verifyETPToolbarShieldIconIsNotDisplayedIfETPIsOFFGloballyTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)

View File

@ -366,7 +366,7 @@ class BrowserRobot {
}
fun openNavigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition {
mDevice.waitForIdle(waitingTime)
navURLBar().click()
NavigationToolbarRobot().interact()

View File

@ -19,6 +19,7 @@ import androidx.test.espresso.intent.matcher.IntentMatchers.toPackage
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
@ -74,7 +75,9 @@ class SettingsRobot {
// ADVANCED SECTION
fun verifyAdvancedHeading() = assertAdvancedHeading()
fun verifyAddons() = assertAddons()
fun verifyAddons() = assertAddonsButton()
// DEVELOPER TOOLS SECTION
fun verifyRemoteDebug() = assertRemoteDebug()
fun verifyLeakCanaryButton() = assertLeakCanaryButton()
@ -211,6 +214,13 @@ class SettingsRobot {
SettingsSubMenuDataCollectionRobot().interact()
return SettingsSubMenuDataCollectionRobot.Transition()
}
fun openAddonsManagerMenu(interact: SettingsSubMenuAddonsManagerRobot.() -> Unit): SettingsSubMenuAddonsManagerRobot.Transition {
addonsManagerButton().click()
SettingsSubMenuAddonsManagerRobot().interact()
return SettingsSubMenuAddonsManagerRobot.Transition()
}
}
companion object {
@ -349,15 +359,25 @@ private fun assertDeveloperToolsHeading() {
// ADVANCED SECTION
private fun assertAdvancedHeading() {
scrollToElementByText("Advanced")
onView(withText("Advanced"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onView(withId(R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText("Add-ons"))
)
)
onView(withText("Add-ons"))
.check(matches(isCompletelyDisplayed()))
}
private fun assertAddons() {
scrollToElementByText("Add-ons")
onView(withText("Add-ons"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertAddonsButton() {
onView(withId(R.id.recycler_view)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText("Add-ons"))
)
)
addonsManagerButton()
.check(matches(isCompletelyDisplayed()))
}
private fun assertRemoteDebug() {
@ -414,5 +434,7 @@ fun isPackageInstalled(packageName: String): Boolean {
}
}
private fun addonsManagerButton() = onView(withText(R.string.preferences_addons))
private fun goBackButton() =
onView(CoreMatchers.allOf(ViewMatchers.withContentDescription("Navigate up")))

View File

@ -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))

View File

@ -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)))
}
}

View File

@ -29,6 +29,8 @@ import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import androidx.test.uiautomator.Until.findObject
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.anyOf
import org.hamcrest.CoreMatchers.containsString
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
@ -179,10 +181,21 @@ private fun tabMediaControlButton() = onView(withId(R.id.play_pause_button))
private fun closeTabButton() = onView(withId(R.id.mozac_browser_tabstray_close))
private fun assertCloseTabsButton(title: String) =
onView(allOf(withId(R.id.mozac_browser_tabstray_close), withContentDescription("Close tab $title")))
onView(
allOf(
withId(R.id.mozac_browser_tabstray_close),
withContentDescription("Close tab $title")
)
)
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun normalBrowsingButton() = onView(withContentDescription("Open tabs"))
private fun normalBrowsingButton() = onView(
anyOf(
withContentDescription(containsString("open tabs. Tap to switch tabs.")),
withContentDescription(containsString("open tab. Tap to switch tabs."))
)
)
private fun privateBrowsingButton() = onView(withContentDescription("Private tabs"))
private fun newTabButton() = onView(withId(R.id.new_tab_button))
private fun threeDotMenu() = onView(withId(R.id.tab_tray_overflow))

View File

@ -347,6 +347,17 @@ class ThreeDotMenuMainRobot {
ThreeDotMenuMainRobot().interact()
return ThreeDotMenuMainRobot.Transition()
}
fun openAddonsManagerMenu(interact: SettingsSubMenuAddonsManagerRobot.() -> Unit): SettingsSubMenuAddonsManagerRobot.Transition {
clickAddonsManagerButton()
mDevice.waitNotNull(
Until.findObject(By.text("Recommended")),
waitingTime
)
SettingsSubMenuAddonsManagerRobot().interact()
return SettingsSubMenuAddonsManagerRobot.Transition()
}
}
}
@ -423,7 +434,9 @@ private fun addNewCollectionButton() = onView(allOf(withText("Add new collection
private fun assertaddNewCollectionButton() = addNewCollectionButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun collectionNameTextField() = onView(allOf(withResourceName("name_collection_edittext")))
private fun collectionNameTextField() =
onView(allOf(withResourceName("name_collection_edittext")))
private fun assertCollectionNameTextField() = collectionNameTextField()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
@ -473,6 +486,7 @@ private fun assertReaderViewAppearanceButton(visible: Boolean) = readerViewAppea
private fun addToFirefoxHomeButton() =
onView(allOf(withText(R.string.browser_menu_add_to_top_sites)))
private fun assertAddToFirefoxHome() {
onView(withId(R.id.mozac_browser_menu_recyclerView))
.perform(
@ -514,3 +528,10 @@ private fun assertOpenInAppButton() {
)
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun addonsManagerButton() = onView(withText("Add-ons Manager"))
private fun clickAddonsManagerButton() {
onView(withText("Add-ons")).click()
addonsManagerButton().click()
}

View File

@ -23,6 +23,7 @@
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/NormalTheme"

View File

@ -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
/**

View File

@ -28,7 +28,6 @@ import androidx.navigation.NavDirections
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_home.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -41,10 +40,7 @@ import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.WebExtensionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.tabstray.BrowserTabsTray
import mozilla.components.browser.thumbnails.loader.ThumbnailLoader
import mozilla.components.concept.engine.EngineView
import mozilla.components.concept.tabstray.TabsTray
import mozilla.components.feature.contextmenu.DefaultSelectionActionDelegate
import mozilla.components.feature.search.BrowserStoreSearchAdapter
import mozilla.components.feature.search.SearchAdapter
@ -97,7 +93,6 @@ import org.mozilla.fenix.settings.search.AddSearchEngineFragmentDirections
import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirections
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections
import org.mozilla.fenix.sync.SyncedTabsFragmentDirections
import org.mozilla.fenix.tabtray.FenixTabsAdapter
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
import org.mozilla.fenix.theme.DefaultThemeManager
import org.mozilla.fenix.theme.ThemeManager
@ -315,17 +310,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
actionSorter = ::actionSorter
)
}.asView()
TabsTray::class.java.name -> {
val layout = LinearLayoutManager(context).apply {
reverseLayout = true
stackFromEnd = true
}
val thumbnailLoader = ThumbnailLoader(components.core.thumbnailStorage)
val adapter = FenixTabsAdapter(context, thumbnailLoader)
BrowserTabsTray(context, attrs, 0, adapter, layout)
}
else -> super.onCreateView(parent, name, context, attrs)
}

View File

@ -12,6 +12,7 @@ import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityManager
import androidx.annotation.CallSuper
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.net.toUri
@ -93,8 +94,10 @@ import org.mozilla.fenix.components.toolbar.BrowserToolbarViewInteractor
import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarController
import org.mozilla.fenix.components.toolbar.SwipeRefreshScrollingViewBehavior
import org.mozilla.fenix.components.toolbar.ToolbarIntegration
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.downloads.DownloadService
import org.mozilla.fenix.downloads.DynamicDownloadDialog
import org.mozilla.fenix.ext.accessibilityManager
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.enterToImmersiveMode
import org.mozilla.fenix.ext.hideToolbar
@ -104,7 +107,6 @@ import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.SharedViewModel
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.allowUndo
import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration
@ -118,7 +120,7 @@ import java.lang.ref.WeakReference
@ExperimentalCoroutinesApi
@Suppress("TooManyFunctions", "LargeClass")
abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, SessionManager.Observer,
OnBackLongPressedListener {
OnBackLongPressedListener, AccessibilityManager.AccessibilityStateChangeListener {
private lateinit var browserFragmentStore: BrowserFragmentStore
private lateinit var browserAnimator: BrowserAnimator
@ -228,7 +230,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
tabCollectionStorage = requireComponents.core.tabCollectionStorage,
topSiteStorage = requireComponents.core.topSiteStorage,
onTabCounterClicked = {
TabTrayDialogFragment.show(parentFragmentManager)
findNavController().nav(
R.id.browserFragment,
BrowserFragmentDirections.actionGlobalTabTrayDialogFragment()
)
},
onCloseTab = {
val snapshot = sessionManager.createSessionSnapshot(it)
@ -243,7 +248,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
}
viewLifecycleOwner.lifecycleScope.allowUndo(
requireView(),
requireView().browserLayout,
snackbarMessage,
requireContext().getString(R.string.snackbar_deleted_undo),
{
@ -264,7 +269,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
_browserToolbarView = BrowserToolbarView(
container = view.browserLayout,
shouldUseBottomToolbar = context.settings().shouldUseBottomToolbar,
toolbarPosition = context.settings().toolbarPosition,
interactor = browserInteractor,
customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) },
lifecycleOwner = viewLifecycleOwner
@ -307,7 +312,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
feature = ContextMenuFeature(
fragmentManager = parentFragmentManager,
store = store,
candidates = getContextMenuCandidates(context, view),
candidates = getContextMenuCandidates(context, view.browserLayout),
engineView = view.engineView,
useCases = context.components.useCases.contextMenuUseCases,
tabId = customTabSessionId
@ -677,10 +682,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
private fun initializeEngineView(toolbarHeight: Int) {
engineView.setDynamicToolbarMaxHeight(toolbarHeight)
val behavior = if (requireContext().settings().shouldUseBottomToolbar) {
EngineViewBottomBehavior(context, null)
} else {
SwipeRefreshScrollingViewBehavior(requireContext(), null, engineView, browserToolbarView)
val context = requireContext()
val behavior = when (context.settings().toolbarPosition) {
ToolbarPosition.BOTTOM -> EngineViewBottomBehavior(context, null)
ToolbarPosition.TOP -> SwipeRefreshScrollingViewBehavior(context, null, engineView, browserToolbarView)
}
(swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams).behavior = behavior
@ -713,6 +718,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
super.onStart()
requireComponents.core.sessionManager.register(this, this, autoPause = true)
sitePermissionWifiIntegration.get()?.maybeAddWifiConnectedListener()
requireContext().accessibilityManager.addAccessibilityStateChangeListener(this)
}
@CallSuper
@ -825,7 +831,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
if (session.hasParentSession) {
sessionManager.remove(session, true)
}
val goToOverview = isLastSession || !session.hasParentSession
// We want to return to home if this removed session was the last session of its type
// and didn't have a parent session to select.
val goToOverview = isLastSession && !session.hasParentSession
!goToOverview
}
}
@ -843,7 +851,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
* Returns the layout [android.view.Gravity] for the quick settings and ETP dialog.
*/
protected fun getAppropriateLayoutGravity(): Int =
if (context?.settings()?.shouldUseBottomToolbar == true) Gravity.BOTTOM else Gravity.TOP
context?.settings()?.toolbarPosition?.androidGravity ?: Gravity.BOTTOM
/**
* Updates the site permissions rules based on user settings.
@ -1011,6 +1019,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
super.onDestroyView()
_browserToolbarView = null
_browserInteractor = null
requireContext().accessibilityManager.removeAccessibilityStateChangeListener(this)
}
companion object {
@ -1019,4 +1028,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
private const val REQUEST_CODE_PROMPT_PERMISSIONS = 2
private const val REQUEST_CODE_APP_PERMISSIONS = 3
}
override fun onAccessibilityStateChanged(enabled: Boolean) {
browserToolbarView.setScrollFlags(enabled)
}
}

View File

@ -21,6 +21,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import mozilla.components.concept.engine.EngineView
import org.mozilla.fenix.R
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.settings
import java.lang.ref.WeakReference
@ -155,12 +156,15 @@ class BrowserAnimator(
fun getToolbarNavOptions(context: Context): NavOptions {
val navOptions = NavOptions.Builder()
if (!context.settings().shouldUseBottomToolbar) {
navOptions.setEnterAnim(R.anim.fade_in)
navOptions.setExitAnim(R.anim.fade_out)
} else {
navOptions.setEnterAnim(R.anim.fade_in_up)
navOptions.setExitAnim(R.anim.fade_out_down)
when (context.settings().toolbarPosition) {
ToolbarPosition.TOP -> {
navOptions.setEnterAnim(R.anim.fade_in)
navOptions.setExitAnim(R.anim.fade_out)
}
ToolbarPosition.BOTTOM -> {
navOptions.setEnterAnim(R.anim.fade_in_up)
navOptions.setExitAnim(R.anim.fade_out_down)
}
}
return navOptions.build()

View File

@ -1,6 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.browser

View File

@ -12,6 +12,7 @@ import mozilla.components.browser.session.SessionManager
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry
import org.mozilla.fenix.utils.Settings
@ -44,25 +45,29 @@ class UriOpenedObserver(
session.register(singleSessionObserver, owner)
}
private fun saveOpenTabsCount() {
settings.setOpenTabsCount(sessionManager.sessionsOfType(private = false).count())
}
override fun onAllSessionsRemoved() {
settings.setOpenTabsCount(sessionManager.sessions.filter { !it.private }.size)
saveOpenTabsCount()
sessionManager.sessions.forEach {
it.unregister(singleSessionObserver)
}
}
override fun onSessionAdded(session: Session) {
settings.setOpenTabsCount(sessionManager.sessions.filter { !it.private }.size)
saveOpenTabsCount()
session.register(singleSessionObserver, owner)
}
override fun onSessionRemoved(session: Session) {
settings.setOpenTabsCount(sessionManager.sessions.filter { !it.private }.size)
saveOpenTabsCount()
session.unregister(singleSessionObserver)
}
override fun onSessionsRestored() {
settings.setOpenTabsCount(sessionManager.sessions.filter { !it.private }.size)
saveOpenTabsCount()
sessionManager.sessions.forEach {
it.register(singleSessionObserver, owner)
}

View File

@ -11,7 +11,6 @@ import android.graphics.drawable.ColorDrawable
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.marginTop
import kotlinx.android.synthetic.main.search_widget_cfr.view.*
@ -21,6 +20,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.components.SearchWidgetCreator
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.utils.Settings
/**
@ -50,18 +50,14 @@ class SearchWidgetCFR(
val searchWidgetCFRDialog = Dialog(context)
val layout = LayoutInflater.from(context)
.inflate(R.layout.search_widget_cfr, null)
val isBottomToolbar = settings.shouldUseBottomToolbar
val toolbarPosition = settings.toolbarPosition
layout.drop_down_triangle.isGone = isBottomToolbar
layout.pop_up_triangle.isVisible = isBottomToolbar
layout.drop_down_triangle.isVisible = toolbarPosition == ToolbarPosition.TOP
layout.pop_up_triangle.isVisible = toolbarPosition == ToolbarPosition.BOTTOM
val toolbar = getToolbar()
val gravity = if (isBottomToolbar) {
Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM
} else {
Gravity.CENTER_HORIZONTAL or Gravity.TOP
}
val gravity = Gravity.CENTER_HORIZONTAL or toolbarPosition.androidGravity
layout.cfr_neg_button.setOnClickListener {
metrics.track(Event.SearchWidgetCFRNotNowPressed)

View File

@ -15,6 +15,8 @@ import mozilla.components.feature.tab.collections.TabCollection
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.getDefaultCollectionNumber
import org.mozilla.fenix.ext.normalSessionSize
import org.mozilla.fenix.home.Tab
interface CollectionCreationController {
@ -92,7 +94,7 @@ class DefaultCollectionCreationController(
}
metrics.track(
Event.CollectionSaved(normalSessionSize(sessionManager), sessionBundle.size)
Event.CollectionSaved(sessionManager.normalSessionSize(), sessionBundle.size)
)
}
@ -134,7 +136,7 @@ class DefaultCollectionCreationController(
}
metrics.track(
Event.CollectionTabsAdded(normalSessionSize(sessionManager), sessionBundle.size)
Event.CollectionTabsAdded(sessionManager.normalSessionSize(), sessionBundle.size)
)
}
@ -146,7 +148,7 @@ class DefaultCollectionCreationController(
} else {
SaveCollectionStep.SelectCollection
},
defaultCollectionNumber = getDefaultCollectionNumber()
defaultCollectionNumber = store.state.tabCollections.getDefaultCollectionNumber()
)
)
}
@ -155,26 +157,11 @@ class DefaultCollectionCreationController(
store.dispatch(
CollectionCreationAction.StepChanged(
SaveCollectionStep.NameCollection,
getDefaultCollectionNumber()
store.state.tabCollections.getDefaultCollectionNumber()
)
)
}
/**
* Returns the new default name recommendation for a collection
*
* Algorithm: Go through all collections, make a list of their names and keep only the default ones.
* Then get the numbers from all these default names, compute the maximum number and add one.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun getDefaultCollectionNumber(): Int {
return (store.state.tabCollections
.map { it.title }
.filter { it.matches(Regex("Collection\\s\\d+")) }
.map { Integer.valueOf(it.split(" ")[DEFAULT_COLLECTION_NUMBER_POSITION]) }
.max() ?: 0) + DEFAULT_INCREMENT_VALUE
}
override fun addTabToSelection(tab: Tab) {
store.dispatch(CollectionCreationAction.TabAdded(tab))
}
@ -209,14 +196,4 @@ class DefaultCollectionCreationController(
SaveCollectionStep.SelectTabs, SaveCollectionStep.RenameCollection -> null
}
}
/**
* @return the number of currently active sessions that are neither custom nor private
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun normalSessionSize(sessionManager: SessionManager): Int {
return sessionManager.sessions.filter { session ->
(!session.isCustomTabSession() && !session.private)
}.size
}
}

View File

@ -17,14 +17,16 @@ import kotlinx.android.synthetic.main.fragment_create_collection.view.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.plus
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.lib.state.ext.consumeFrom
import org.mozilla.fenix.R
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.ext.getMediaStateForSession
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.toTab
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.home.Tab
@ExperimentalCoroutinesApi
@ -47,12 +49,11 @@ class CollectionCreationFragment : DialogFragment() {
val view = inflater.inflate(R.layout.fragment_create_collection, container, false)
val args: CollectionCreationFragmentArgs by navArgs()
val sessionManager = requireComponents.core.sessionManager
val store = requireComponents.core.store
val publicSuffixList = requireComponents.publicSuffixList
val tabs = sessionManager.getTabs(args.tabIds, store, publicSuffixList)
val tabs = store.state.getTabs(args.tabIds, publicSuffixList)
val selectedTabs = if (args.selectedTabIds != null) {
sessionManager.getTabs(args.selectedTabIds, store, publicSuffixList).toSet()
store.state.getTabs(args.selectedTabIds, publicSuffixList).toSet()
} else {
if (tabs.size == 1) setOf(tabs.first()) else emptySet()
}
@ -112,14 +113,30 @@ class CollectionCreationFragment : DialogFragment() {
}
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun SessionManager.getTabs(
@VisibleForTesting
internal fun BrowserState.getTabs(
tabIds: Array<String>?,
store: BrowserStore,
publicSuffixList: PublicSuffixList
): List<Tab> {
return tabIds
?.mapNotNull { this.findSessionById(it) }
?.map { it.toTab(store, publicSuffixList) }
?: emptyList()
?.mapNotNull { id -> findTab(id) }
?.map { it.toTab(this, publicSuffixList) }
.orEmpty()
}
private fun TabSessionState.toTab(
state: BrowserState,
publicSuffixList: PublicSuffixList,
selected: Boolean? = null
): Tab {
val url = readerState.activeUrl ?: content.url
return Tab(
sessionId = this.id,
url = url,
hostname = url.toShortUrl(publicSuffixList),
title = content.title,
selected = selected,
icon = content.icon,
mediaState = state.getMediaStateForSession(this.id)
)
}

View File

@ -10,7 +10,6 @@ import org.mozilla.fenix.home.Tab
/**
* Diff callback for comparing tab lists with selected state.
*/
@Suppress("LongParameterList")
internal class TabDiffUtil(
private val old: List<Tab>,
private val new: List<Tab>,

View File

@ -47,6 +47,7 @@ import org.mozilla.fenix.utils.Settings
* background worker.
*/
@Mockable
@Suppress("LongParameterList")
class BackgroundServices(
private val context: Context,
private val push: Push,

View File

@ -43,10 +43,10 @@ import mozilla.components.feature.webnotifications.WebNotificationFeature
import mozilla.components.lib.dataprotect.SecureAbove22Preferences
import mozilla.components.lib.dataprotect.generateEncryptionKey
import mozilla.components.service.digitalassetlinks.RelationChecker
import mozilla.components.service.digitalassetlinks.api.DigitalAssetLinksApi
import mozilla.components.service.digitalassetlinks.local.StatementApi
import mozilla.components.service.digitalassetlinks.local.StatementRelationChecker
import mozilla.components.service.sync.logins.SyncableLoginsStorage
import org.mozilla.fenix.AppRequestInterceptor
import org.mozilla.fenix.BuildConfig.DIGITAL_ASSET_LINKS_TOKEN
import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
@ -147,7 +147,7 @@ class Core(private val context: Context) {
* The [RelationChecker] checks Digital Asset Links relationships for Trusted Web Activities.
*/
val relationChecker: RelationChecker by lazy {
DigitalAssetLinksApi(client, DIGITAL_ASSET_LINKS_TOKEN)
StatementRelationChecker(StatementApi(client))
}
/**

View File

@ -1,3 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.components
import android.view.View

View File

@ -25,6 +25,7 @@ import org.mozilla.fenix.utils.Mockable
* Component group for miscellaneous components.
*/
@Mockable
@Suppress("LongParameterList")
class IntentProcessors(
private val context: Context,
private val sessionManager: SessionManager,

View File

@ -1,3 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.components
import android.content.Context

View File

@ -1,6 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.components

View File

@ -27,6 +27,7 @@ import org.mozilla.fenix.utils.Mockable
* modules and can be triggered by UI interactions.
*/
@Mockable
@Suppress("LongParameterList")
class UseCases(
private val context: Context,
private val engine: Engine,

View File

@ -50,6 +50,7 @@ import org.mozilla.fenix.GleanMetrics.TopSites
import org.mozilla.fenix.GleanMetrics.TrackingProtection
import org.mozilla.fenix.GleanMetrics.UserSpecifiedSearchEngines
import org.mozilla.fenix.GleanMetrics.VoiceSearch
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.utils.BrowsersCache
@ -724,10 +725,9 @@ class GleanMetricsService(private val context: Context) : MetricsService {
}
toolbarPosition.set(
if (context.settings().shouldUseBottomToolbar) {
Event.ToolbarPositionChanged.Position.BOTTOM.name
} else {
Event.ToolbarPositionChanged.Position.TOP.name
when (context.settings().toolbarPosition) {
ToolbarPosition.BOTTOM -> Event.ToolbarPositionChanged.Position.BOTTOM.name
ToolbarPosition.TOP -> Event.ToolbarPositionChanged.Position.TOP.name
}
)
}

View File

@ -4,47 +4,38 @@
package org.mozilla.fenix.components.toolbar
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.PopupWindow
import androidx.annotation.LayoutRes
import androidx.annotation.VisibleForTesting
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.LifecycleOwner
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED
import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL
import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.browser_toolbar_popup_window.view.*
import kotlinx.android.synthetic.main.component_browser_top_toolbar.*
import kotlinx.android.synthetic.main.component_browser_top_toolbar.view.*
import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
import mozilla.components.browser.session.Session
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.browser.toolbar.behavior.BrowserToolbarBottomBehavior
import mozilla.components.browser.toolbar.display.DisplayToolbar
import mozilla.components.support.ktx.android.util.dpToFloat
import mozilla.components.support.utils.URLStringUtils
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.customtabs.CustomTabToolbarIntegration
import org.mozilla.fenix.customtabs.CustomTabToolbarMenu
import org.mozilla.fenix.ext.bookmarkStorage
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.ToolbarPopupWindow
import java.lang.ref.WeakReference
interface BrowserToolbarViewInteractor {
fun onBrowserToolbarPaste(text: String)
@ -56,10 +47,11 @@ interface BrowserToolbarViewInteractor {
fun onScrolled(offset: Int)
fun onReaderModePressed(enabled: Boolean)
}
@SuppressWarnings("LargeClass")
class BrowserToolbarView(
private val container: ViewGroup,
private val shouldUseBottomToolbar: Boolean,
private val toolbarPosition: ToolbarPosition,
private val interactor: BrowserToolbarViewInteractor,
private val customTabSession: Session?,
private val lifecycleOwner: LifecycleOwner
@ -71,9 +63,9 @@ class BrowserToolbarView(
private val settings = container.context.settings()
@LayoutRes
private val toolbarLayout = when {
settings.shouldUseBottomToolbar -> R.layout.component_bottom_browser_toolbar
else -> R.layout.component_browser_top_toolbar
private val toolbarLayout = when (settings.toolbarPosition) {
ToolbarPosition.BOTTOM -> R.layout.component_bottom_browser_toolbar
ToolbarPosition.TOP -> R.layout.component_browser_top_toolbar
}
private val layout = LayoutInflater.from(container.context)
@ -88,63 +80,19 @@ class BrowserToolbarView(
val isCustomTabSession = customTabSession != null
view.display.setOnUrlLongClickListener {
val clipboard = view.context.components.clipboardHandler
val customView = LayoutInflater.from(view.context)
.inflate(R.layout.browser_toolbar_popup_window, null)
val popupWindow = PopupWindow(
customView,
LinearLayout.LayoutParams.WRAP_CONTENT,
view.context.resources.getDimensionPixelSize(R.dimen.context_menu_height),
true
ToolbarPopupWindow.show(
WeakReference(view),
customTabSession,
interactor::onBrowserToolbarPasteAndGo,
interactor::onBrowserToolbarPaste
)
popupWindow.elevation =
view.context.resources.getDimension(R.dimen.mozac_browser_menu_elevation)
// This is a workaround for SDK<23 to allow popup dismissal on outside or back button press
// See: https://github.com/mozilla-mobile/fenix/issues/10027
popupWindow.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
customView.paste.isVisible = !clipboard.text.isNullOrEmpty() && !isCustomTabSession
customView.paste_and_go.isVisible =
!clipboard.text.isNullOrEmpty() && !isCustomTabSession
customView.copy.setOnClickListener {
popupWindow.dismiss()
clipboard.text = getUrlForClipboard(it.context.components.core.store, customTabSession)
FenixSnackbar.make(
view = view,
duration = Snackbar.LENGTH_SHORT,
isDisplayedWithBrowserToolbar = true
)
.setText(view.context.getString(R.string.browser_toolbar_url_copied_to_clipboard_snackbar))
.show()
}
customView.paste.setOnClickListener {
popupWindow.dismiss()
interactor.onBrowserToolbarPaste(clipboard.text!!)
}
customView.paste_and_go.setOnClickListener {
popupWindow.dismiss()
interactor.onBrowserToolbarPasteAndGo(clipboard.text!!)
}
popupWindow.showAsDropDown(
view,
view.context.resources.getDimensionPixelSize(R.dimen.context_menu_x_offset),
0,
Gravity.START
)
true
}
with(container.context) {
val sessionManager = components.core.sessionManager
if (!shouldUseBottomToolbar) {
if (toolbarPosition == ToolbarPosition.TOP) {
val offsetChangedListener =
AppBarLayout.OnOffsetChangedListener { _: AppBarLayout?, verticalOffset: Int ->
interactor.onScrolled(verticalOffset)
@ -167,10 +115,9 @@ class BrowserToolbarView(
false
}
display.progressGravity = if (shouldUseBottomToolbar) {
DisplayToolbar.Gravity.TOP
} else {
DisplayToolbar.Gravity.BOTTOM
display.progressGravity = when (toolbarPosition) {
ToolbarPosition.BOTTOM -> DisplayToolbar.Gravity.TOP
ToolbarPosition.TOP -> DisplayToolbar.Gravity.BOTTOM
}
val primaryTextColor = ContextCompat.getColor(
@ -207,7 +154,7 @@ class BrowserToolbarView(
this,
sessionManager,
customTabSession?.id,
shouldReverseItems = !shouldUseBottomToolbar,
shouldReverseItems = toolbarPosition == ToolbarPosition.TOP,
onItemTapped = {
interactor.onBrowserToolbarMenuItemTapped(it)
}
@ -216,7 +163,7 @@ class BrowserToolbarView(
menuToolbar = DefaultToolbarMenu(
context = this,
hasAccountProblem = components.backgroundServices.accountManager.accountNeedsReauth(),
shouldReverseItems = !shouldUseBottomToolbar,
shouldReverseItems = toolbarPosition == ToolbarPosition.TOP,
onItemTapped = { interactor.onBrowserToolbarMenuItemTapped(it) },
lifecycleOwner = lifecycleOwner,
sessionManager = sessionManager,
@ -243,7 +190,7 @@ class BrowserToolbarView(
menuToolbar,
ShippedDomainsProvider().also { it.initialize(this) },
components.core.historyStorage,
components.core.sessionManager,
lifecycleOwner,
sessionId = null,
isPrivate = sessionManager.selectedSession?.private ?: false,
interactor = interactor,
@ -254,12 +201,15 @@ class BrowserToolbarView(
}
fun expand() {
if (settings.shouldUseBottomToolbar) {
(view.layoutParams as CoordinatorLayout.LayoutParams).apply {
(behavior as BrowserToolbarBottomBehavior).forceExpand(view)
when (settings.toolbarPosition) {
ToolbarPosition.BOTTOM -> {
(view.layoutParams as CoordinatorLayout.LayoutParams).apply {
(behavior as BrowserToolbarBottomBehavior).forceExpand(view)
}
}
ToolbarPosition.TOP -> {
layout.app_bar?.setExpanded(true)
}
} else if (!settings.shouldUseBottomToolbar) {
layout.app_bar?.setExpanded(true)
}
}
@ -268,43 +218,30 @@ class BrowserToolbarView(
* Note that the bottom toolbar has a feature flag for being dynamic, so it may not get flags set.
*/
fun setScrollFlags(shouldDisableScroll: Boolean = false) {
if (view.context.settings().shouldUseBottomToolbar) {
if (view.layoutParams is CoordinatorLayout.LayoutParams) {
(view.layoutParams as CoordinatorLayout.LayoutParams).apply {
when (settings.toolbarPosition) {
ToolbarPosition.BOTTOM -> {
(view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply {
behavior = BrowserToolbarBottomBehavior(view.context, null)
}
}
return
}
val params = view.layoutParams as AppBarLayout.LayoutParams
params.scrollFlags = when (view.context.settings().shouldUseFixedTopToolbar || shouldDisableScroll) {
true -> {
// Force expand the toolbar so the user is not stuck with a hidden toolbar
expand()
0
}
false -> {
SCROLL_FLAG_SCROLL or SCROLL_FLAG_ENTER_ALWAYS or SCROLL_FLAG_SNAP or SCROLL_FLAG_EXIT_UNTIL_COLLAPSED
ToolbarPosition.TOP -> {
view.updateLayoutParams<AppBarLayout.LayoutParams> {
scrollFlags = if (settings.shouldUseFixedTopToolbar || shouldDisableScroll) {
// Force expand the toolbar so the user is not stuck with a hidden toolbar
expand()
0
} else {
SCROLL_FLAG_SCROLL or
SCROLL_FLAG_ENTER_ALWAYS or
SCROLL_FLAG_SNAP or
SCROLL_FLAG_EXIT_UNTIL_COLLAPSED
}
}
}
}
view.layoutParams = params
}
companion object {
private const val TOOLBAR_ELEVATION = 16
@VisibleForTesting
internal fun getUrlForClipboard(store: BrowserStore, customTabSession: Session? = null): String? {
return if (customTabSession != null) {
customTabSession.url
} else {
val selectedTab = store.state.selectedTab
selectedTab?.readerState?.activeUrl ?: selectedTab?.content?.url
}
}
}
}

View File

@ -42,7 +42,7 @@ import org.mozilla.fenix.theme.ThemeManager
* @param lifecycleOwner View lifecycle owner used to determine when to cancel UI jobs.
* @param bookmarksStorage Used to check if a page is bookmarked.
*/
@Suppress("LargeClass")
@Suppress("LargeClass", "LongParameterList")
class DefaultToolbarMenu(
private val context: Context,
private val sessionManager: SessionManager,

View File

@ -1,3 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.components.toolbar
sealed class TabCounterMenuItem {

View File

@ -8,17 +8,21 @@ import android.content.Context
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.browser.menu.BrowserMenu
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.BrowserMenuDivider
import mozilla.components.browser.menu.item.BrowserMenuImageText
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.concept.toolbar.Toolbar
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.theme.ThemeManager
import java.lang.ref.WeakReference
@ -26,8 +30,9 @@ import java.lang.ref.WeakReference
/**
* A [Toolbar.Action] implementation that shows a [TabCounter].
*/
@OptIn(ExperimentalCoroutinesApi::class)
class TabCounterToolbarButton(
private val sessionManager: SessionManager,
private val lifecycleOwner: LifecycleOwner,
private val isPrivate: Boolean,
private val onItemTapped: (TabCounterMenuItem) -> Unit = {},
private val showTabs: () -> Unit
@ -35,7 +40,11 @@ class TabCounterToolbarButton(
private var reference: WeakReference<TabCounter> = WeakReference<TabCounter>(null)
override fun createView(parent: ViewGroup): View {
sessionManager.register(sessionManagerObserver, view = parent)
parent.context.components.core.store.flowScoped(lifecycleOwner) { flow ->
flow.map { state -> state.getNormalOrPrivateTabs(isPrivate).size }
.ifChanged()
.collect { tabs -> updateCount(tabs) }
}
val view = TabCounter(parent.context).apply {
reference = WeakReference(this)
@ -50,10 +59,11 @@ class TabCounterToolbarButton(
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) {
setCount(sessionManager.sessionsOfType(private = isPrivate).count())
setCount(context.components.core.store.state.getNormalOrPrivateTabs(isPrivate).size)
}
override fun onViewDetachedFromWindow(v: View?) { /* no-op */ }
override fun onViewDetachedFromWindow(v: View?) { /* no-op */
}
})
}
@ -70,12 +80,8 @@ class TabCounterToolbarButton(
override fun bind(view: View) = Unit
private fun updateCount() {
val count = sessionManager.sessionsOfType(private = isPrivate).count()
reference.get()?.let {
it.setCountWithAnimation(count)
}
private fun updateCount(count: Int) {
reference.get()?.setCountWithAnimation(count)
}
private fun getTabContextMenu(context: Context): BrowserMenu {
@ -113,29 +119,10 @@ class TabCounterToolbarButton(
)
return BrowserMenuBuilder(
if (context.settings().shouldUseBottomToolbar) {
menuItems.reversed()
} else {
menuItems
when (context.settings().toolbarPosition) {
ToolbarPosition.BOTTOM -> menuItems.reversed()
ToolbarPosition.TOP -> menuItems
}
).build(context)
}
private val sessionManagerObserver = object : SessionManager.Observer {
override fun onSessionAdded(session: Session) {
updateCount()
}
override fun onSessionRemoved(session: Session) {
updateCount()
}
override fun onSessionsRestored() {
updateCount()
}
override fun onAllSessionsRemoved() {
updateCount()
}
}
}

View File

@ -6,10 +6,10 @@ package org.mozilla.fenix.components.toolbar
import android.content.Context
import androidx.appcompat.content.res.AppCompatResources
import androidx.lifecycle.LifecycleOwner
import com.airbnb.lottie.LottieCompositionFactory
import com.airbnb.lottie.LottieDrawable
import mozilla.components.browser.domains.autocomplete.DomainAutocompleteProvider
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.browser.toolbar.display.DisplayToolbar
import mozilla.components.concept.engine.Engine
@ -74,7 +74,7 @@ class DefaultToolbarIntegration(
toolbarMenu: ToolbarMenu,
domainAutocompleteProvider: DomainAutocompleteProvider,
historyStorage: HistoryStorage,
sessionManager: SessionManager,
lifecycleOwner: LifecycleOwner,
sessionId: String? = null,
isPrivate: Boolean,
interactor: BrowserToolbarViewInteractor,
@ -135,10 +135,11 @@ class DefaultToolbarIntegration(
val onTabCounterMenuItemTapped = { item: TabCounterMenuItem ->
interactor.onTabCounterMenuItemTapped(item)
}
val tabsAction = TabCounterToolbarButton(sessionManager, isPrivate, onTabCounterMenuItemTapped) {
toolbar.hideKeyboard()
interactor.onTabCounterClicked()
}
val tabsAction =
TabCounterToolbarButton(lifecycleOwner, isPrivate, onTabCounterMenuItemTapped) {
toolbar.hideKeyboard()
interactor.onTabCounterClicked()
}
toolbar.addBrowserAction(tabsAction)
val engineForSpeculativeConnects = if (!isPrivate) engine else null

View File

@ -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)
}

View File

@ -21,8 +21,8 @@ import org.mozilla.fenix.ext.settings
* [DynamicDownloadDialog] is used to show a view in the current tab to the user, triggered when
* downloadFeature.onDownloadStopped gets invoked. It uses [DynamicDownloadDialogBehavior] to
* hide when the users scrolls through a website as to not impede his activities.
* */
*/
@Suppress("LongParameterList")
class DynamicDownloadDialog(
private val container: ViewGroup,
private val downloadState: DownloadState?,

View File

@ -7,8 +7,6 @@ package org.mozilla.fenix.ext
import android.app.Activity
import android.view.View
import android.view.WindowManager
import mozilla.components.support.base.log.Log
import org.mozilla.fenix.perf.Performance
/**
* Attempts to call immersive mode using the View to hide the status bar and navigation buttons.
@ -24,17 +22,3 @@ fun Activity.enterToImmersiveMode() {
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
}
/**
* Calls [Activity.reportFullyDrawn] while also preventing crashes under some circumstances.
*/
fun Activity.reportFullyDrawnSafe() {
try {
reportFullyDrawn()
} catch (e: SecurityException) {
// This exception is throw on some Samsung devices. We were unable to identify the root
// cause but suspect it's related to Samsung security features. See
// https://github.com/mozilla-mobile/fenix/issues/12345#issuecomment-655058864 for details.
Log.log(Log.Priority.ERROR, Performance.TAG, e, "Unable to call reportFullyDrawn")
}
}

View File

@ -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
}
}

View File

@ -9,6 +9,7 @@ import android.content.Context
import android.view.ContextThemeWrapper
import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityManager
import androidx.annotation.StringRes
import mozilla.components.browser.search.SearchEngineManager
import mozilla.components.support.locale.LocaleManager
@ -81,3 +82,10 @@ fun Context.getStringWithArgSafe(@StringRes resId: Int, formatArg: String): Stri
return format(localizedContext.getString(resId), formatArg)
}
}
/**
* Used to obtain a reference to an AccessibilityManager
* @return accessibilityManager
*/
val Context.accessibilityManager: AccessibilityManager get() =
getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager

View File

@ -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
}
}

View File

@ -11,3 +11,12 @@ import mozilla.components.browser.session.SessionManager
*/
fun SessionManager.sessionsOfType(private: Boolean) =
sessions.asSequence().filter { it.private == private }
/**
* @return the number of currently active sessions that are neither custom nor private
*/
fun SessionManager.normalSessionSize(): Int {
return this.sessions.filter { session ->
(!session.isCustomTabSession() && !session.private)
}.size
}

View File

@ -4,13 +4,11 @@
package org.mozilla.fenix.ext
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Patterns
import android.webkit.URLUtil
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
@ -94,8 +92,8 @@ private fun Uri.isIpv6(): Boolean {
/**
* Trim a host's prefix and suffix
*/
fun String.urlToTrimmedHost(context: Context): String = runBlocking {
urlToTrimmedHost(context.components.publicSuffixList).await()
fun String.urlToTrimmedHost(publicSuffixList: PublicSuffixList): String = runBlocking {
urlToTrimmedHost(publicSuffixList).await()
}
/**
@ -115,16 +113,6 @@ fun String.simplifiedUrl(): String {
return afterScheme
}
/**
* Gets a rounded drawable from a URL if possible, else null.
*/
suspend fun String.toRoundedDrawable(context: Context, client: Client) = bitmapForUrl(this, client)?.let { bitmap ->
RoundedBitmapDrawableFactory.create(context.resources, bitmap).also {
it.isCircular = true
it.setAntiAlias(true)
}
}
suspend fun bitmapForUrl(url: String, client: Client): Bitmap? = withContext(Dispatchers.IO) {
// Code below will cache it in Gecko's cache, which ensures that as long as we've fetched it once,
// we will be able to display this avatar as long as the cache isn't purged (e.g. via 'clear user data').

View File

@ -9,6 +9,7 @@ import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import mozilla.components.feature.tab.collections.TabCollection
import org.mozilla.fenix.R
import org.mozilla.fenix.collections.DefaultCollectionCreationController
import kotlin.math.abs
/**
@ -22,3 +23,17 @@ fun TabCollection.getIconColor(context: Context): Int {
iconColors.recycle()
return color
}
/**
* Returns the new default name recommendation for a collection
*
* Algorithm: Go through all collections, make a list of their names and keep only the default ones.
* Then get the numbers from all these default names, compute the maximum number and add one.
*/
fun List<TabCollection>.getDefaultCollectionNumber(): Int {
return (this
.map { it.title }
.filter { it.matches(Regex("Collection\\s\\d+")) }
.map { Integer.valueOf(it.split(" ")[DefaultCollectionCreationController.DEFAULT_COLLECTION_NUMBER_POSITION]) }
.max() ?: 0) + DefaultCollectionCreationController.DEFAULT_INCREMENT_VALUE
}

View File

@ -42,7 +42,6 @@ import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_home.*
import kotlinx.android.synthetic.main.fragment_home.view.*
@ -60,9 +59,11 @@ import mozilla.components.browser.menu.item.BrowserMenuImageText
import mozilla.components.browser.menu.view.MenuButton
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount
@ -83,6 +84,7 @@ import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.tips.FenixTipManager
import org.mozilla.fenix.components.tips.providers.MigrationTipProvider
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.metrics
@ -91,7 +93,6 @@ import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.resetPoliciesAfter
import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.toTab
import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
import org.mozilla.fenix.home.sessioncontrol.SessionControlView
@ -100,13 +101,12 @@ import org.mozilla.fenix.onboarding.FenixOnboarding
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.SupportUtils.SumoTopic.HELP
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.FragmentPreDrawManager
import org.mozilla.fenix.utils.ToolbarPopupWindow
import org.mozilla.fenix.utils.allowUndo
import org.mozilla.fenix.whatsnew.WhatsNew
import java.lang.ref.WeakReference
import kotlin.math.abs
import kotlin.math.min
@ExperimentalCoroutinesApi
@ -120,16 +120,12 @@ class HomeFragment : Fragment() {
}
private val snackbarAnchorView: View?
get() {
return if (requireContext().settings().shouldUseBottomToolbar) {
toolbarLayout
} else {
null
}
get() = when (requireContext().settings().toolbarPosition) {
ToolbarPosition.BOTTOM -> toolbarLayout
ToolbarPosition.TOP -> null
}
private val browsingModeManager get() = (activity as HomeActivity).browsingModeManager
private var homeAppBarOffset = 0
private val collectionStorageObserver = object : TabCollectionStorage.Observer {
override fun onCollectionCreated(title: String, sessions: List<Session>) {
@ -147,8 +143,9 @@ class HomeFragment : Fragment() {
private val sessionManager: SessionManager
get() = requireComponents.core.sessionManager
private val store: BrowserStore
get() = requireComponents.core.store
private lateinit var homeAppBarOffSetListener: AppBarLayout.OnOffsetChangedListener
private val onboarding by lazy { FenixOnboarding(requireContext()) }
private lateinit var homeFragmentStore: HomeFragmentStore
private var _sessionControlInteractor: SessionControlInteractor? = null
@ -218,7 +215,6 @@ class HomeFragment : Fragment() {
)
)
updateLayout(view)
setOffset(view)
sessionControlView = SessionControlView(
view.sessionControlRecyclerView,
sessionControlInteractor,
@ -253,45 +249,36 @@ class HomeFragment : Fragment() {
}
private fun updateLayout(view: View) {
val shouldUseBottomToolbar = view.context.settings().shouldUseBottomToolbar
if (!shouldUseBottomToolbar) {
view.toolbarLayout.layoutParams = CoordinatorLayout.LayoutParams(
ConstraintLayout.LayoutParams.MATCH_PARENT,
ConstraintLayout.LayoutParams.WRAP_CONTENT
)
.apply {
when (view.context.settings().toolbarPosition) {
ToolbarPosition.TOP -> {
view.toolbarLayout.layoutParams = CoordinatorLayout.LayoutParams(
ConstraintLayout.LayoutParams.MATCH_PARENT,
ConstraintLayout.LayoutParams.WRAP_CONTENT
).apply {
gravity = Gravity.TOP
}
ConstraintSet().apply {
clone(view.toolbarLayout)
clear(view.bottom_bar.id, BOTTOM)
clear(view.bottomBarShadow.id, BOTTOM)
connect(view.bottom_bar.id, TOP, PARENT_ID, TOP)
connect(view.bottomBarShadow.id, TOP, view.bottom_bar.id, BOTTOM)
connect(view.bottomBarShadow.id, BOTTOM, PARENT_ID, BOTTOM)
applyTo(view.toolbarLayout)
ConstraintSet().apply {
clone(view.toolbarLayout)
clear(view.bottom_bar.id, BOTTOM)
clear(view.bottomBarShadow.id, BOTTOM)
connect(view.bottom_bar.id, TOP, PARENT_ID, TOP)
connect(view.bottomBarShadow.id, TOP, view.bottom_bar.id, BOTTOM)
connect(view.bottomBarShadow.id, BOTTOM, PARENT_ID, BOTTOM)
applyTo(view.toolbarLayout)
}
view.bottom_bar.background = resources.getDrawable(
ThemeManager.resolveAttribute(R.attr.bottomBarBackgroundTop, requireContext()),
null
)
view.homeAppBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = HEADER_MARGIN.dpToPx(resources.displayMetrics)
}
}
view.bottom_bar.background = resources.getDrawable(
ThemeManager.resolveAttribute(R.attr.bottomBarBackgroundTop, requireContext()),
null
)
view.homeAppBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = HEADER_MARGIN.dpToPx(resources.displayMetrics)
ToolbarPosition.BOTTOM -> {
}
createNewAppBarListener(HEADER_MARGIN.dpToPx(resources.displayMetrics).toFloat())
view.homeAppBar.addOnOffsetChangedListener(
homeAppBarOffSetListener
)
} else {
createNewAppBarListener(0F)
view.homeAppBar.addOnOffsetChangedListener(
homeAppBarOffSetListener
)
}
}
@ -352,6 +339,16 @@ class HomeFragment : Fragment() {
requireComponents.analytics.metrics.track(Event.SearchBarTapped(Event.SearchBarTapped.Source.HOME))
}
view.toolbar_wrapper.setOnLongClickListener {
ToolbarPopupWindow.show(
WeakReference(view),
handlePasteAndGo = sessionControlInteractor::onPasteAndGo,
handlePaste = sessionControlInteractor::onPaste,
copyVisible = false
)
true
}
view.tab_button.setOnClickListener {
openTabTray()
}
@ -407,46 +404,95 @@ class HomeFragment : Fragment() {
}
bundleArgs.getString(SESSION_TO_DELETE)?.also {
sessionManager.findSessionById(it)?.let { session ->
val snapshot = sessionManager.createSessionSnapshot(session)
val state = snapshot.engineSession?.saveState()
val isSelected =
session.id == requireComponents.core.store.state.selectedTabId ?: false
val snackbarMessage = if (snapshot.session.private) {
requireContext().getString(R.string.snackbar_private_tab_closed)
} else {
requireContext().getString(R.string.snackbar_tab_closed)
}
viewLifecycleOwner.lifecycleScope.allowUndo(
requireView(),
snackbarMessage,
requireContext().getString(R.string.snackbar_deleted_undo),
{
sessionManager.add(
snapshot.session,
isSelected,
engineSessionState = state
)
findNavController().navigate(HomeFragmentDirections.actionHomeFragmentToBrowserFragment(null))
},
operation = { },
anchorView = snackbarAnchorView
)
requireComponents.useCases.tabsUseCases.removeTab.invoke(session)
if (it == ALL_NORMAL_TABS || it == ALL_PRIVATE_TABS) {
removeAllTabsAndShowSnackbar(it)
} else {
removeTabAndShowSnackbar(it)
}
}
updateTabCounter(requireComponents.core.store.state)
}
private fun removeAllTabsAndShowSnackbar(sessionCode: String) {
val tabs = sessionManager.sessionsOfType(private = sessionCode == ALL_PRIVATE_TABS).toList()
val selectedIndex = sessionManager
.selectedSession?.let { sessionManager.sessions.indexOf(it) } ?: 0
val snapshot = tabs
.map(sessionManager::createSessionSnapshot)
.map {
it.copy(
engineSession = null,
engineSessionState = it.engineSession?.saveState()
)
}
.let { SessionManager.Snapshot(it, selectedIndex) }
tabs.forEach {
sessionManager.remove(it)
}
val snackbarMessage = if (sessionCode == ALL_PRIVATE_TABS) {
getString(R.string.snackbar_private_tabs_closed)
} else {
getString(R.string.snackbar_tabs_closed)
}
viewLifecycleOwner.lifecycleScope.allowUndo(
requireView(),
snackbarMessage,
requireContext().getString(R.string.snackbar_deleted_undo),
{
sessionManager.restore(snapshot)
},
operation = { },
anchorView = snackbarAnchorView
)
}
private fun removeTabAndShowSnackbar(sessionId: String) {
sessionManager.findSessionById(sessionId)?.let { session ->
val snapshot = sessionManager.createSessionSnapshot(session)
val state = snapshot.engineSession?.saveState()
val isSelected =
session.id == requireComponents.core.store.state.selectedTabId ?: false
sessionManager.remove(session)
val snackbarMessage = if (snapshot.session.private) {
requireContext().getString(R.string.snackbar_private_tab_closed)
} else {
requireContext().getString(R.string.snackbar_tab_closed)
}
viewLifecycleOwner.lifecycleScope.allowUndo(
requireView(),
snackbarMessage,
requireContext().getString(R.string.snackbar_deleted_undo),
{
sessionManager.add(
snapshot.session,
isSelected,
engineSessionState = state
)
findNavController().navigate(
HomeFragmentDirections.actionHomeFragmentToBrowserFragment(
null
)
)
},
operation = { },
anchorView = snackbarAnchorView
)
}
}
override fun onDestroyView() {
super.onDestroyView()
_sessionControlInteractor = null
sessionControlView = null
bundleArgs.clear()
requireView().homeAppBar.removeOnOffsetChangedListener(homeAppBarOffSetListener)
requireActivity().window.clearFlags(FLAG_SECURE)
}
@ -561,7 +607,6 @@ class HomeFragment : Fragment() {
)
)
}
calculateNewOffset()
}
private fun recommendPrivateBrowsingShortcut() {
@ -776,10 +821,6 @@ class HomeFragment : Fragment() {
}
}
private fun getNumberOfSessions(private: Boolean = browsingModeManager.mode.isPrivate): Int {
return sessionManager.sessionsOfType(private = private).count()
}
private fun registerCollectionStorageObserver() {
requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this)
}
@ -791,7 +832,9 @@ class HomeFragment : Fragment() {
viewLifecycleOwner.lifecycleScope.launch {
val recyclerView = sessionControlView!!.view
delay(ANIM_SCROLL_DELAY)
val tabsSize = getNumberOfSessions()
val tabsSize = store.state
.getNormalOrPrivateTabs(browsingModeManager.mode.isPrivate)
.size
var indexOfCollection = tabsSize + NON_TAB_ITEM_NUM
changedCollection?.let { changedCollection ->
@ -890,40 +933,11 @@ class HomeFragment : Fragment() {
}
}
private fun List<Session>.toTabs(): List<Tab> {
val selected = sessionManager.selectedSession
return map {
it.toTab(requireContext(), it == selected)
}
}
private fun calculateNewOffset() {
homeAppBarOffset = ((homeAppBar.layoutParams as CoordinatorLayout.LayoutParams)
.behavior as AppBarLayout.Behavior).topAndBottomOffset
}
private fun setOffset(currentView: View) {
if (homeAppBarOffset <= 0) {
(currentView.homeAppBar.layoutParams as CoordinatorLayout.LayoutParams)
.behavior = AppBarLayout.Behavior().apply {
topAndBottomOffset = this@HomeFragment.homeAppBarOffset
}
} else {
currentView.homeAppBar.setExpanded(false)
}
}
private fun createNewAppBarListener(margin: Float) {
homeAppBarOffSetListener =
AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset ->
val reduceScrollRanged = appBarLayout.totalScrollRange.toFloat() - margin
appBarLayout.alpha = 1.0f - abs(verticalOffset / reduceScrollRanged)
}
}
private fun openTabTray() {
TabTrayDialogFragment.show(parentFragmentManager)
findNavController().nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalTabTrayDialogFragment()
)
}
private fun updateTabCounter(browserState: BrowserState) {
@ -938,6 +952,9 @@ class HomeFragment : Fragment() {
}
companion object {
const val ALL_NORMAL_TABS = "all_normal"
const val ALL_PRIVATE_TABS = "all_private"
private const val FOCUS_ON_ADDRESS_BAR = "focusOnAddressBar"
private const val SESSION_TO_DELETE = "session_to_delete"
private const val ANIMATION_DELAY = 100L

View File

@ -15,6 +15,7 @@ import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tab.collections.ext.restore
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.feature.top.sites.TopSite
import mozilla.components.support.ktx.kotlin.isUrl
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
@ -24,9 +25,13 @@ import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.TopSiteStorage
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.metrics.MetricsUtils
import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentDirections
@ -38,7 +43,7 @@ import mozilla.components.feature.tab.collections.Tab as ComponentTab
* [HomeFragment] controller. An interface that handles the view manipulation of the Tabs triggered
* by the Interactor.
*/
@SuppressWarnings("TooManyFunctions")
@Suppress("TooManyFunctions")
interface SessionControlController {
/**
* @see [CollectionInteractor.onCollectionAddTabTapped]
@ -120,15 +125,28 @@ interface SessionControlController {
*/
fun handleToggleCollectionExpanded(collection: TabCollection, expand: Boolean)
/**
* @see [TipInteractor.onCloseTip]
*/
fun handleCloseTip(tip: Tip)
/**
* @see [ToolbarInteractor.onPasteAndGo]
*/
fun handlePasteAndGo(clipboardText: String)
/**
* @see [ToolbarInteractor.onPaste]
*/
fun handlePaste(clipboardText: String)
/**
* @see [CollectionInteractor.onAddTabsToCollectionTapped]
*/
fun handleCreateCollection()
}
@SuppressWarnings("TooManyFunctions", "LargeClass", "LongParameterList")
@Suppress("TooManyFunctions", "LargeClass")
class DefaultSessionControlController(
private val activity: HomeActivity,
private val engine: Engine,
@ -192,8 +210,12 @@ class DefaultSessionControlController(
metrics.track(Event.CollectionTabRemoved)
if (collection.tabs.size == 1) {
val title = activity.resources.getString(R.string.delete_tab_and_collection_dialog_title, collection.title)
val message = activity.resources.getString(R.string.delete_tab_and_collection_dialog_message)
val title = activity.resources.getString(
R.string.delete_tab_and_collection_dialog_title,
collection.title
)
val message =
activity.resources.getString(R.string.delete_tab_and_collection_dialog_message)
showDeleteCollectionPrompt(collection, title, message)
} else {
viewLifecycleScope.launch(Dispatchers.IO) {
@ -208,7 +230,8 @@ class DefaultSessionControlController(
}
override fun handleDeleteCollectionTapped(collection: TabCollection) {
val message = activity.resources.getString(R.string.tab_collection_dialog_message, collection.title)
val message =
activity.resources.getString(R.string.tab_collection_dialog_message, collection.title)
showDeleteCollectionPrompt(collection, null, message)
}
@ -254,8 +277,12 @@ class DefaultSessionControlController(
override fun handleSelectTopSite(url: String, isDefault: Boolean) {
metrics.track(Event.TopSiteOpenInNewTab)
if (isDefault) { metrics.track(Event.TopSiteOpenDefault) }
if (url == SupportUtils.POCKET_TRENDING_URL) { metrics.track(Event.PocketTopSiteClicked) }
if (isDefault) {
metrics.track(Event.TopSiteOpenDefault)
}
if (url == SupportUtils.POCKET_TRENDING_URL) {
metrics.track(Event.PocketTopSiteClicked)
}
addTabUseCase.invoke(
url = url,
selectTab = true,
@ -297,6 +324,13 @@ class DefaultSessionControlController(
fragmentStore.dispatch(HomeFragmentAction.RemoveTip(tip))
}
private fun showTabTrayCollectionCreation() {
val directions = HomeFragmentDirections.actionGlobalTabTrayDialogFragment(
enterMultiselect = true
)
navController.nav(R.id.homeFragment, directions)
}
private fun showCollectionCreationFragment(
step: SaveCollectionStep,
selectedTabIds: Array<String>? = null,
@ -322,7 +356,7 @@ class DefaultSessionControlController(
}
override fun handleCreateCollection() {
showCollectionCreationFragment(step = SaveCollectionStep.SelectTabs)
showTabTrayCollectionCreation()
}
private fun showShareFragment(data: List<ShareData>) {
@ -331,4 +365,37 @@ class DefaultSessionControlController(
)
navController.nav(R.id.homeFragment, directions)
}
override fun handlePasteAndGo(clipboardText: String) {
activity.openToBrowserAndLoad(
searchTermOrURL = clipboardText,
newTab = true,
from = BrowserDirection.FromHome,
engine = activity.components.search.provider.getDefaultEngine(activity)
)
val event = if (clipboardText.isUrl()) {
Event.EnteredUrl(false)
} else {
val searchAccessPoint = Event.PerformedSearch.SearchAccessPoint.ACTION
activity.settings().incrementActiveSearchCount()
searchAccessPoint.let { sap ->
MetricsUtils.createSearchEvent(
activity.components.search.provider.getDefaultEngine(activity),
activity,
sap
)
}
}
event?.let { activity.metrics.track(it) }
}
override fun handlePaste(clipboardText: String) {
val directions = HomeFragmentDirections.actionGlobalSearch(
sessionId = null,
pastedText = clipboardText
)
navController.nav(R.id.homeFragment, directions)
}
}

View File

@ -95,6 +95,18 @@ interface CollectionInteractor {
fun onAddTabsToCollectionTapped()
}
interface ToolbarInteractor {
/**
* Navigates to browser with clipboard text.
*/
fun onPasteAndGo(clipboardText: String)
/**
* Navigates to search with clipboard text.
*/
fun onPaste(clipboardText: String)
}
/**
* Interface for onboarding related actions in the [SessionControlInteractor].
*/
@ -163,7 +175,8 @@ interface TopSiteInteractor {
@SuppressWarnings("TooManyFunctions")
class SessionControlInteractor(
private val controller: SessionControlController
) : CollectionInteractor, OnboardingInteractor, TopSiteInteractor, TipInteractor, TabSessionInteractor {
) : CollectionInteractor, OnboardingInteractor, TopSiteInteractor, TipInteractor,
TabSessionInteractor, ToolbarInteractor {
override fun onCollectionAddTabTapped(collection: TabCollection) {
controller.handleCollectionAddTabTapped(collection)
}
@ -235,4 +248,12 @@ class SessionControlInteractor(
override fun onPrivateBrowsingLearnMoreClicked() {
controller.handlePrivateBrowsingLearnMoreClicked()
}
override fun onPasteAndGo(clipboardText: String) {
controller.handlePasteAndGo(clipboardText)
}
override fun onPaste(clipboardText: String) {
controller.handlePaste(clipboardText)
}
}

View File

@ -20,7 +20,7 @@ import org.mozilla.fenix.home.OnboardingState
// This method got a little complex with the addition of the tab tray feature flag
// When we remove the tabs from the home screen this will get much simpler again.
@SuppressWarnings("LongParameterList", "ComplexMethod")
@Suppress("ComplexMethod")
private fun normalModeAdapterItems(
topSites: List<TopSite>,
collections: List<TabCollection>,

View File

@ -10,6 +10,7 @@ import kotlinx.android.synthetic.main.onboarding_toolbar_position_picker.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.Event.OnboardingToolbarPosition.Position
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.asActivity
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.onboarding.OnboardingRadioButton
@ -29,10 +30,9 @@ class OnboardingToolbarPositionPickerViewHolder(view: View) : RecyclerView.ViewH
radioBottomToolbar.addIllustration(view.toolbar_bottom_image)
val settings = view.context.components.settings
radio = if (settings.shouldUseBottomToolbar) {
radioBottomToolbar
} else {
radioTopToolbar
radio = when (settings.toolbarPosition) {
ToolbarPosition.BOTTOM -> radioBottomToolbar
ToolbarPosition.TOP -> radioTopToolbar
}
radio.updateRadioValue(true)

View File

@ -1,23 +1,32 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.home.tips
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.button_tip_item.view.*
import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.button_tip_item.*
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.components.tips.TipType
import org.mozilla.fenix.ext.addUnderline
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.utils.view.ViewHolder
class ButtonTipViewHolder(
val view: View,
val interactor: SessionControlInteractor
) : RecyclerView.ViewHolder(view) {
view: View,
private val interactor: SessionControlInteractor,
private val metrics: MetricController = view.context.components.analytics.metrics,
private val settings: Settings = view.context.components.settings
) : ViewHolder(view) {
var tip: Tip? = null
fun bind(tip: Tip) {
@ -25,44 +34,39 @@ class ButtonTipViewHolder(
this.tip = tip
view.apply {
context.components.analytics.metrics.track(Event.TipDisplayed(tip.identifier))
metrics.track(Event.TipDisplayed(tip.identifier))
tip_header_text.text = tip.title
tip_description_text.text = tip.description
tip_button.text = tip.type.text
tip_header_text.text = tip.title
tip_description_text.text = tip.description
tip_button.text = tip.type.text
if (tip.learnMoreURL == null) {
tip_learn_more.visibility = View.GONE
} else {
tip_learn_more.addUnderline()
tip_learn_more.isVisible = tip.learnMoreURL != null
if (tip.learnMoreURL != null) {
tip_learn_more.addUnderline()
tip_learn_more.setOnClickListener {
(context as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = tip.learnMoreURL,
newTab = true,
from = BrowserDirection.FromHome
)
}
}
tip_button.setOnClickListener {
tip.type.action.invoke()
context.components.analytics.metrics.track(
Event.TipPressed(tip.identifier)
tip_learn_more.setOnClickListener {
(itemView.context as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = tip.learnMoreURL,
newTab = true,
from = BrowserDirection.FromHome
)
}
}
tip_close.setOnClickListener {
context.components.analytics.metrics.track(Event.TipClosed(tip.identifier))
tip_button.setOnClickListener {
tip.type.action.invoke()
metrics.track(Event.TipPressed(tip.identifier))
}
context.settings().preferences
.edit()
.putBoolean(tip.identifier, false)
.apply()
tip_close.setOnClickListener {
metrics.track(Event.TipClosed(tip.identifier))
interactor.onCloseTip(tip)
}
settings.preferences
.edit()
.putBoolean(tip.identifier, false)
.apply()
interactor.onCloseTip(tip)
}
}

View File

@ -28,7 +28,7 @@ import org.mozilla.fenix.ext.nav
* [BookmarkFragment] controller.
* Delegated by View Interactors, handles container business logic and operates changes on it.
*/
@SuppressWarnings("TooManyFunctions")
@Suppress("TooManyFunctions")
interface BookmarkController {
fun handleBookmarkChanged(item: BookmarkNode)
fun handleBookmarkTapped(item: BookmarkNode)
@ -47,7 +47,7 @@ interface BookmarkController {
fun handleBackPressed()
}
@SuppressWarnings("TooManyFunctions", "LongParameterList")
@Suppress("TooManyFunctions")
class DefaultBookmarkController(
private val activity: HomeActivity,
private val navController: NavController,

View File

@ -51,7 +51,6 @@ import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.library.LibraryPageFragment
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
import org.mozilla.fenix.utils.allowUndo
/**
@ -240,7 +239,7 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
private fun showTabTray() {
invokePendingDeletion()
TabTrayDialogFragment.show(parentFragmentManager)
navigate(BookmarkFragmentDirections.actionGlobalTabTrayDialogFragment())
}
private fun navigate(directions: NavDirections) {

View File

@ -1,8 +1,6 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* * License, v. 2.0. If a copy of the MPL was not distributed with this
* * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.library.history

View File

@ -44,7 +44,6 @@ import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.library.LibraryPageFragment
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
import org.mozilla.fenix.utils.allowUndo
@SuppressWarnings("TooManyFunctions", "LargeClass")
@ -207,7 +206,10 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
private fun showTabTray() {
invokePendingDeletion()
TabTrayDialogFragment.show(parentFragmentManager)
findNavController().nav(
R.id.historyFragment,
HistoryFragmentDirections.actionGlobalTabTrayDialogFragment()
)
}
private fun getMultiSelectSnackBarMessage(historyItems: Set<HistoryItem>): String {
@ -259,7 +261,10 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
launch(Main) {
viewModel.invalidate()
historyStore.dispatch(HistoryFragmentAction.ExitDeletionMode)
showSnackBar(requireView(), getString(R.string.preferences_delete_browsing_data_snackbar))
showSnackBar(
requireView(),
getString(R.string.preferences_delete_browsing_data_snackbar)
)
}
}

View File

@ -1,6 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.library.history

View File

@ -1,6 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.library.history

View File

@ -1,6 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.loginexceptions

View File

@ -1,6 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.loginexceptions

View File

@ -8,6 +8,7 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.onboarding.FenixOnboarding
import android.provider.Settings as AndroidSettings
@ -17,6 +18,8 @@ import android.provider.Settings as AndroidSettings
*/
object Performance {
const val TAG = "FenixPerf"
val logger = Logger(TAG)
private const val EXTRA_IS_PERFORMANCE_TEST = "performancetest"
/**

View File

@ -7,9 +7,9 @@ package org.mozilla.fenix.perf
import android.app.Activity
import android.view.View
import androidx.core.view.doOnPreDraw
import mozilla.components.support.ktx.android.view.reportFullyDrawnSafe
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.reportFullyDrawnSafe
import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.TopSiteItemViewHolder
import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupDestination.APP_LINK
import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupDestination.HOMESCREEN
@ -65,6 +65,6 @@ class StartupReportFullyDrawn {
// - the difference in timing is minimal (< 7ms on Pixel 2)
// - if we compare against another app using a preDrawListener, as we are with Fennec, it
// should be comparable
view.doOnPreDraw { activity.reportFullyDrawnSafe() }
view.doOnPreDraw { activity.reportFullyDrawnSafe(Performance.logger) }
}
}

View File

@ -1,7 +1,6 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* * License, v. 2.0. If a copy of the MPL was not distributed with this
* * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.search
@ -9,6 +8,7 @@ import android.content.Intent
import androidx.navigation.NavController
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.support.ktx.kotlin.isUrl
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
@ -17,15 +17,14 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.Event.PerformedSearch.SearchAccessPoint.ACTION
import org.mozilla.fenix.components.metrics.Event.PerformedSearch.SearchAccessPoint.NONE
import org.mozilla.fenix.components.metrics.Event.PerformedSearch.SearchAccessPoint.SUGGESTION
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.metrics.MetricsUtils
import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore
import org.mozilla.fenix.crashes.CrashListActivity
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.SupportUtils.MozillaPage.MANIFESTO
import org.mozilla.fenix.utils.Settings
/**
* An interface that handles the view manipulation of the Search, triggered by the Interactor
@ -43,11 +42,14 @@ interface SearchController {
fun handleSearchShortcutsButtonClicked()
}
@Suppress("TooManyFunctions")
@Suppress("TooManyFunctions", "LongParameterList")
class DefaultSearchController(
private val activity: HomeActivity,
private val sessionManager: SessionManager,
private val store: SearchFragmentStore,
private val navController: NavController,
private val settings: Settings,
private val metrics: MetricController,
private val clearToolbarFocus: () -> Unit
) : SearchController {
@ -77,7 +79,7 @@ class DefaultSearchController(
val event = if (url.isUrl()) {
Event.EnteredUrl(false)
} else {
activity.settings().incrementActiveSearchCount()
settings.incrementActiveSearchCount()
val searchAccessPoint = when (store.state.searchAccessPoint) {
NONE -> ACTION
@ -93,7 +95,7 @@ class DefaultSearchController(
}
}
event?.let { activity.metrics.track(it) }
event?.let { metrics.track(it) }
}
override fun handleEditingCancelled() {
@ -101,7 +103,6 @@ class DefaultSearchController(
}
override fun handleTextChanged(text: String) {
val settings = activity.settings()
// Display the search shortcuts on each entry of the search fragment (see #5308)
val textMatchesCurrentUrl = store.state.url == text
val textMatchesCurrentSearch = store.state.searchTerms == text
@ -130,11 +131,11 @@ class DefaultSearchController(
from = BrowserDirection.FromSearch
)
activity.metrics.track(Event.EnteredUrl(false))
metrics.track(Event.EnteredUrl(false))
}
override fun handleSearchTermsTapped(searchTerms: String) {
activity.settings().incrementActiveSearchCount()
settings.incrementActiveSearchCount()
activity.openToBrowserAndLoad(
searchTermOrURL = searchTerms,
@ -156,14 +157,14 @@ class DefaultSearchController(
sap
)
}
event?.let { activity.metrics.track(it) }
event?.let { metrics.track(it) }
}
override fun handleSearchShortcutEngineSelected(searchEngine: SearchEngine) {
store.dispatch(SearchFragmentAction.SearchShortcutEngineSelected(searchEngine))
val isCustom =
CustomSearchEngineStore.isCustomSearchEngine(activity, searchEngine.identifier)
activity.metrics.track(Event.SearchShortcutSelected(searchEngine, isCustom))
metrics.track(Event.SearchShortcutSelected(searchEngine, isCustom))
}
override fun handleSearchShortcutsButtonClicked() {
@ -177,14 +178,14 @@ class DefaultSearchController(
}
override fun handleExistingSessionSelected(session: Session) {
activity.components.core.sessionManager.select(session)
sessionManager.select(session)
activity.openToBrowser(
from = BrowserDirection.FromSearch
)
}
override fun handleExistingSessionSelected(tabId: String) {
val session = activity.components.core.sessionManager.findSessionById(tabId)
val session = sessionManager.findSessionById(tabId)
if (session != null) {
handleExistingSessionSelected(session)
}

View File

@ -86,6 +86,7 @@ class SearchFragment : Fragment(), UserInteractionHandler {
savedInstanceState: Bundle?
): View? {
val activity = activity as HomeActivity
val settings = activity.settings()
val args by navArgs<SearchFragmentArgs>()
val tabId = args.sessionId
@ -112,13 +113,13 @@ class SearchFragment : Fragment(), UserInteractionHandler {
defaultEngineSource = currentSearchEngine,
showSearchSuggestions = shouldShowSearchSuggestions(isPrivate),
showSearchSuggestionsHint = false,
showSearchShortcuts = requireContext().settings().shouldShowSearchShortcuts &&
showSearchShortcuts = settings.shouldShowSearchShortcuts &&
url.isEmpty() &&
areShortcutsAvailable,
areShortcutsAvailable = areShortcutsAvailable,
showClipboardSuggestions = requireContext().settings().shouldShowClipboardSuggestions,
showHistorySuggestions = requireContext().settings().shouldShowHistorySuggestions,
showBookmarkSuggestions = requireContext().settings().shouldShowBookmarkSuggestions,
showClipboardSuggestions = settings.shouldShowClipboardSuggestions,
showHistorySuggestions = settings.shouldShowHistorySuggestions,
showBookmarkSuggestions = settings.shouldShowBookmarkSuggestions,
tabId = tabId,
pastedText = args.pastedText,
searchAccessPoint = args.searchAccessPoint
@ -128,8 +129,11 @@ class SearchFragment : Fragment(), UserInteractionHandler {
val searchController = DefaultSearchController(
activity = activity,
sessionManager = requireComponents.core.sessionManager,
store = searchStore,
navController = findNavController(),
settings = settings,
metrics = requireComponents.analytics.metrics,
clearToolbarFocus = ::clearToolbarFocus
)
@ -160,7 +164,11 @@ class SearchFragment : Fragment(), UserInteractionHandler {
BrowserToolbar.Button(
ContextCompat.getDrawable(requireContext(), R.drawable.ic_microphone)!!,
requireContext().getString(R.string.voice_search_content_description),
visible = { requireContext().settings().shouldShowVoiceSearch && speechIsAvailable() },
visible = {
currentSearchEngine.searchEngine.identifier.contains("google") &&
speechIsAvailable() &&
settings.shouldShowVoiceSearch
},
listener = ::launchVoiceSearch
)
)

View File

@ -1,6 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.search

View File

@ -1,6 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.search.toolbar

View File

@ -12,6 +12,7 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.PreferenceFragmentCompat
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
@ -122,8 +123,9 @@ class CustomizationFragment : PreferenceFragmentCompat() {
))
}
topPreference.setCheckedWithoutClickListener(!requireContext().settings().shouldUseBottomToolbar)
bottomPreference.setCheckedWithoutClickListener(requireContext().settings().shouldUseBottomToolbar)
val toolbarPosition = requireContext().settings().toolbarPosition
topPreference.setCheckedWithoutClickListener(toolbarPosition == ToolbarPosition.TOP)
bottomPreference.setCheckedWithoutClickListener(toolbarPosition == ToolbarPosition.BOTTOM)
addToRadioGroup(topPreference, bottomPreference)
}

View File

@ -5,7 +5,6 @@
package org.mozilla.fenix.settings
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
@ -13,16 +12,13 @@ import android.os.Bundle
import android.os.Handler
import android.provider.Settings
import android.widget.Toast
import androidx.appcompat.content.res.AppCompatResources
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDirections
import androidx.navigation.findNavController
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import mozilla.components.concept.sync.AccountObserver
@ -41,19 +37,19 @@ import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.ext.toRoundedDrawable
import org.mozilla.fenix.settings.account.AccountAuthErrorPreference
import org.mozilla.fenix.settings.account.AccountPreference
import org.mozilla.fenix.settings.account.AccountUiView
import kotlin.system.exitProcess
@Suppress("LargeClass", "TooManyFunctions")
class SettingsFragment : PreferenceFragmentCompat() {
private lateinit var accountUiView: AccountUiView
private val accountObserver = object : AccountObserver {
private fun updateAccountUi(profile: Profile? = null) {
val context = context ?: return
lifecycleScope.launch {
updateAccountUIState(
accountUiView.updateAccountUIState(
context = context,
profile = profile
?: context.components.backgroundServices.accountManager.accountProfile()
@ -75,6 +71,13 @@ class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
accountUiView = AccountUiView(
fragment = this,
accountManager = requireComponents.backgroundServices.accountManager,
httpClient = requireComponents.core.client,
updateFxASyncOverrideMenu = ::updateFxASyncOverrideMenu
)
// Observe account changes to keep the UI up-to-date.
requireComponents.backgroundServices.accountManager.register(
accountObserver,
@ -88,7 +91,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
// For example, if user is signed-in, and we don't perform this call in onCreate, we'll briefly
// display a "Sign In" preference, which will then get replaced by the correct account information
// once this call is ran in onResume shortly after.
updateAccountUIState(
accountUiView.updateAccountUIState(
requireContext(),
requireComponents.backgroundServices.accountManager.accountProfile()
)
@ -162,7 +165,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
setupPreferences()
if (shouldUpdateAccountUIState) {
updateAccountUIState(
accountUiView.updateAccountUIState(
requireContext(),
requireComponents.backgroundServices.accountManager.accountProfile()
)
@ -295,9 +298,9 @@ class SettingsFragment : PreferenceFragmentCompat() {
}
}
preferenceRemoteDebugging?.setOnPreferenceChangeListener { preference, newValue ->
preferenceRemoteDebugging?.setOnPreferenceChangeListener<Boolean> { preference, newValue ->
preference.context.settings().preferences.edit()
.putBoolean(preference.key, newValue as Boolean).apply()
.putBoolean(preference.key, newValue).apply()
requireComponents.core.engine.settings.remoteDebuggingEnabled = newValue
true
}
@ -378,68 +381,6 @@ class SettingsFragment : PreferenceFragmentCompat() {
}
}
/**
* Updates the UI to reflect current account state.
* Possible conditions are logged-in without problems, logged-out, and logged-in but needs to re-authenticate.
*/
private fun updateAccountUIState(context: Context, profile: Profile?) {
val preferenceSignIn =
requirePreference<Preference>(R.string.pref_key_sign_in)
val preferenceFirefoxAccount =
requirePreference<AccountPreference>(R.string.pref_key_account)
val preferenceFirefoxAccountAuthError =
requirePreference<AccountAuthErrorPreference>(R.string.pref_key_account_auth_error)
val accountPreferenceCategory =
requirePreference<PreferenceCategory>(R.string.pref_key_account_category)
val accountManager = requireComponents.backgroundServices.accountManager
val account = accountManager.authenticatedAccount()
updateFxASyncOverrideMenu()
// Signed-in, no problems.
if (account != null && !accountManager.accountNeedsReauth()) {
preferenceSignIn.isVisible = false
profile?.avatar?.url?.let { avatarUrl ->
lifecycleScope.launch(Main) {
val roundedDrawable =
avatarUrl.toRoundedDrawable(context, requireComponents.core.client)
preferenceFirefoxAccount.icon =
roundedDrawable ?: AppCompatResources.getDrawable(
context,
R.drawable.ic_account
)
}
}
preferenceSignIn.onPreferenceClickListener = null
preferenceFirefoxAccountAuthError.isVisible = false
preferenceFirefoxAccount.isVisible = true
accountPreferenceCategory.isVisible = true
preferenceFirefoxAccount.displayName = profile?.displayName
preferenceFirefoxAccount.email = profile?.email
// Signed-in, need to re-authenticate.
} else if (account != null && accountManager.accountNeedsReauth()) {
preferenceFirefoxAccount.isVisible = false
preferenceFirefoxAccountAuthError.isVisible = true
accountPreferenceCategory.isVisible = true
preferenceSignIn.isVisible = false
preferenceSignIn.onPreferenceClickListener = null
preferenceFirefoxAccountAuthError.email = profile?.email
// Signed-out.
} else {
preferenceSignIn.isVisible = true
preferenceFirefoxAccount.isVisible = false
preferenceFirefoxAccountAuthError.isVisible = false
accountPreferenceCategory.isVisible = false
}
}
private fun updateFxASyncOverrideMenu() {
val preferenceFxAOverride =
findPreference<Preference>(getPreferenceKey(R.string.pref_key_override_fxa_server))

View File

@ -1,3 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.settings
import androidx.core.content.edit

View File

@ -1,4 +1,4 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

View File

@ -1,6 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.settings.account

View File

@ -1,6 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.settings.account

View File

@ -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)
}
}
}

View File

@ -4,20 +4,19 @@
package org.mozilla.fenix.settings.logins
import android.content.Context
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import org.mozilla.fenix.ext.urlToTrimmedHost
sealed class SortingStrategy {
abstract operator fun invoke(logins: List<SavedLogin>): List<SavedLogin>
abstract val appContext: Context
data class Alphabetically(override val appContext: Context) : SortingStrategy() {
data class Alphabetically(private val publicSuffixList: PublicSuffixList) : SortingStrategy() {
override fun invoke(logins: List<SavedLogin>): List<SavedLogin> {
return logins.sortedBy { it.origin.urlToTrimmedHost(appContext) }
return logins.sortedBy { it.origin.urlToTrimmedHost(publicSuffixList) }
}
}
data class LastUsed(override val appContext: Context) : SortingStrategy() {
object LastUsed : SortingStrategy() {
override fun invoke(logins: List<SavedLogin>): List<SavedLogin> {
return logins.sortedByDescending { it.timeLastUsed }
}

View File

@ -6,12 +6,12 @@ package org.mozilla.fenix.settings.logins.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import android.view.Menu
import android.view.MenuInflater
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
@ -31,17 +31,18 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.redirectToReAuth
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.settings.logins.LoginsAction
import org.mozilla.fenix.settings.logins.LoginsFragmentStore
import org.mozilla.fenix.settings.logins.controller.LoginsListController
import org.mozilla.fenix.settings.logins.LoginsListState
import org.mozilla.fenix.settings.logins.interactor.SavedLoginsInteractor
import org.mozilla.fenix.settings.logins.SavedLoginsSortingStrategyMenu
import org.mozilla.fenix.settings.logins.view.SavedLoginsListView
import org.mozilla.fenix.settings.logins.SortingStrategy
import org.mozilla.fenix.settings.logins.controller.LoginsListController
import org.mozilla.fenix.settings.logins.controller.SavedLoginsStorageController
import org.mozilla.fenix.settings.logins.interactor.SavedLoginsInteractor
import org.mozilla.fenix.settings.logins.view.SavedLoginsListView
@SuppressWarnings("TooManyFunctions")
class SavedLoginsFragment : Fragment() {
@ -228,16 +229,14 @@ class SavedLoginsFragment : Fragment() {
SavedLoginsSortingStrategyMenu.Item.AlphabeticallySort -> {
savedLoginsInteractor.onSortingStrategyChanged(
SortingStrategy.Alphabetically(
requireContext().applicationContext
requireComponents.publicSuffixList
)
)
}
SavedLoginsSortingStrategyMenu.Item.LastUsedSort -> {
savedLoginsInteractor.onSortingStrategyChanged(
SortingStrategy.LastUsed(
requireContext().applicationContext
)
SortingStrategy.LastUsed
)
}
}

View File

@ -17,7 +17,15 @@ import org.mozilla.fenix.utils.Settings
fun PhoneFeature.shouldBeVisible(
sitePermissions: SitePermissions?,
settings: Settings
) = getStatus(sitePermissions, settings) != SitePermissions.Status.NO_DECISION
): Boolean {
// We have to check if the site have a site permission exception,
// if it doesn't the feature shouldn't be visible
return if (sitePermissions == null) {
false
} else {
getStatus(sitePermissions, settings) != SitePermissions.Status.NO_DECISION
}
}
/**
* Common [PhoneFeature] extensions used for **quicksettings**.

View File

@ -14,6 +14,7 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import android.widget.LinearLayout
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
@ -93,7 +94,7 @@ class AddSearchEngineFragment : Fragment(), CompoundButton.OnCheckedChangeListen
availableEngines.forEachIndexed(setupSearchEngineItem)
val engineItem = makeCustomButton(layoutInflater, res = resources)
val engineItem = makeCustomButton(layoutInflater)
engineItem.id = CUSTOM_INDEX
engineItem.radio_button.isChecked = selectedIndex == CUSTOM_INDEX
engineViews.add(engineItem)
@ -249,12 +250,11 @@ class AddSearchEngineFragment : Fragment(), CompoundButton.OnCheckedChangeListen
toggleCustomForm(selectedIndex == -1)
}
private fun makeCustomButton(layoutInflater: LayoutInflater, res: Resources): View {
private fun makeCustomButton(layoutInflater: LayoutInflater): View {
val wrapper = layoutInflater
.inflate(R.layout.custom_search_engine_radio_button, null) as ConstraintLayout
wrapper.setOnClickListener { wrapper.radio_button.isChecked = true }
wrapper.radio_button.setOnCheckedChangeListener(this)
wrapper.minHeight = res.getDimensionPixelSize(R.dimen.radio_button_preference_height)
return wrapper
}
@ -271,7 +271,7 @@ class AddSearchEngineFragment : Fragment(), CompoundButton.OnCheckedChangeListen
res: Resources
): View {
val wrapper = layoutInflater
.inflate(R.layout.search_engine_radio_button, null) as ConstraintLayout
.inflate(R.layout.search_engine_radio_button, null) as LinearLayout
wrapper.setOnClickListener { wrapper.radio_button.isChecked = true }
wrapper.radio_button.setOnCheckedChangeListener(this)
wrapper.engine_text.text = engine.name
@ -280,7 +280,6 @@ class AddSearchEngineFragment : Fragment(), CompoundButton.OnCheckedChangeListen
engineIcon.setBounds(0, 0, iconSize, iconSize)
wrapper.engine_icon.setImageDrawable(engineIcon)
wrapper.overflow_menu.visibility = View.GONE
wrapper.minHeight = res.getDimensionPixelSize(R.dimen.radio_button_preference_height)
return wrapper
}

View File

@ -12,8 +12,8 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import android.widget.LinearLayout
import android.widget.RadioGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import androidx.navigation.Navigation
import androidx.preference.Preference
@ -117,9 +117,10 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
res: Resources,
allowDeletion: Boolean
): View {
val isCustomSearchEngine = CustomSearchEngineStore.isCustomSearchEngine(context, engine.identifier)
val isCustomSearchEngine =
CustomSearchEngineStore.isCustomSearchEngine(context, engine.identifier)
val wrapper = layoutInflater.inflate(itemResId, null) as ConstraintLayout
val wrapper = layoutInflater.inflate(itemResId, null) as LinearLayout
wrapper.setOnClickListener { wrapper.radio_button.isChecked = true }
wrapper.radio_button.setOnCheckedChangeListener(this)
wrapper.engine_text.text = engine.name
@ -132,7 +133,11 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
onItemTapped = {
when (it) {
is SearchEngineMenu.Item.Edit -> editCustomSearchEngine(engine)
is SearchEngineMenu.Item.Delete -> deleteSearchEngine(context, engine, isCustomSearchEngine)
is SearchEngineMenu.Item.Delete -> deleteSearchEngine(
context,
engine,
isCustomSearchEngine
)
}
}
).menuBuilder.build(context).show(wrapper.overflow_menu)
@ -146,7 +151,8 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
searchEngineList.list.forEach { engine ->
val wrapper: ConstraintLayout = searchEngineGroup?.findViewWithTag(engine.identifier) ?: return
val wrapper: LinearLayout =
searchEngineGroup?.findViewWithTag(engine.identifier) ?: return
when (wrapper.radio_button == buttonView) {
true -> onSearchEngineSelected(engine)
@ -165,12 +171,20 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
Navigation.findNavController(searchEngineGroup!!).navigate(directions)
}
private fun deleteSearchEngine(context: Context, engine: SearchEngine, isCustomSearchEngine: Boolean) {
private fun deleteSearchEngine(
context: Context,
engine: SearchEngine,
isCustomSearchEngine: Boolean
) {
val isDefaultEngine = engine == context.components.search.provider.getDefaultEngine(context)
val initialEngineList = searchEngineList.copy()
val initialDefaultEngine = searchEngineList.default
context.components.search.provider.uninstallSearchEngine(context, engine, isCustomSearchEngine)
context.components.search.provider.uninstallSearchEngine(
context,
engine,
isCustomSearchEngine
)
MainScope().allowUndo(
view = context.getRootView()!!,
@ -178,7 +192,11 @@ abstract class SearchEngineListPreference @JvmOverloads constructor(
.getString(R.string.search_delete_search_engine_success_message, engine.name),
undoActionTitle = context.getString(R.string.snackbar_deleted_undo),
onCancel = {
context.components.search.provider.installSearchEngine(context, engine, isCustomSearchEngine)
context.components.search.provider.installSearchEngine(
context,
engine,
isCustomSearchEngine
)
searchEngineList = initialEngineList.copy(
default = initialDefaultEngine

View File

@ -6,7 +6,6 @@ package org.mozilla.fenix.shortcut
import androidx.navigation.NavController
import mozilla.components.browser.session.Session
import mozilla.components.concept.engine.manifest.WebAppManifest
import mozilla.components.feature.pwa.WebAppUseCases
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserFragmentDirections
@ -22,8 +21,8 @@ class PwaOnboardingObserver(
private val webAppUseCases: WebAppUseCases
) : Session.Observer {
override fun onWebAppManifestChanged(session: Session, manifest: WebAppManifest?) {
if (webAppUseCases.isInstallable() && !settings.userKnowsAboutPwas) {
override fun onLoadingStateChanged(session: Session, loading: Boolean) {
if (!loading && webAppUseCases.isInstallable() && !settings.userKnowsAboutPwas) {
settings.incrementVisitedInstallableCount()
if (settings.shouldShowPwaOnboarding) {
val directions =

View File

@ -8,16 +8,15 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import mozilla.components.concept.sync.Device as SyncDevice
import mozilla.components.browser.storage.sync.Tab as SyncTab
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import org.mozilla.fenix.sync.SyncedTabsViewHolder.DeviceViewHolder
import org.mozilla.fenix.sync.SyncedTabsViewHolder.TabViewHolder
import mozilla.components.browser.storage.sync.Tab as SyncTab
import mozilla.components.concept.sync.Device as SyncDevice
class SyncedTabsAdapter(
private val listener: (SyncTab) -> Unit
) : ListAdapter<SyncedTabsAdapter.AdapterItem, SyncedTabsViewHolder>(
DiffCallback
) {
) : ListAdapter<SyncedTabsAdapter.AdapterItem, SyncedTabsViewHolder>(DiffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SyncedTabsViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
@ -30,23 +29,35 @@ class SyncedTabsAdapter(
}
override fun onBindViewHolder(holder: SyncedTabsViewHolder, position: Int) {
val item = when (holder) {
is DeviceViewHolder -> getItem(position) as AdapterItem.Device
is TabViewHolder -> getItem(position) as AdapterItem.Tab
}
holder.bind(item, listener)
holder.bind(getItem(position), listener)
}
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID
is AdapterItem.Tab -> TabViewHolder.LAYOUT_ID
override fun getItemViewType(position: Int) = when (getItem(position)) {
is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID
is AdapterItem.Tab -> TabViewHolder.LAYOUT_ID
}
fun updateData(syncedTabs: List<SyncedDeviceTabs>) {
val allDeviceTabs = mutableListOf<AdapterItem>()
syncedTabs.forEach { (device, tabs) ->
if (tabs.isNotEmpty()) {
allDeviceTabs.add(AdapterItem.Device(device))
tabs.mapTo(allDeviceTabs) { AdapterItem.Tab(it) }
}
}
submitList(allDeviceTabs)
}
private object DiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
override fun areItemsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =
areContentsTheSame(oldItem, newItem)
when (oldItem) {
is AdapterItem.Device ->
newItem is AdapterItem.Device && oldItem.device.id == newItem.device.id
is AdapterItem.Tab ->
oldItem == newItem
}
@Suppress("DiffUtilEquals")
override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) =

View File

@ -10,10 +10,10 @@ import android.view.View
import android.widget.FrameLayout
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.component_sync_tabs.view.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import org.mozilla.fenix.R
@ -43,15 +43,7 @@ class SyncedTabsLayout @JvmOverloads constructor(
// We may still be displaying a "loading" spinner, hide it.
stopLoading()
val stringResId = when (error) {
SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE -> R.string.synced_tabs_connect_another_device
SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE -> R.string.synced_tabs_enable_tab_syncing
SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> R.string.synced_tabs_connect_to_sync_account
SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth
SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> R.string.synced_tabs_no_tabs
}
sync_tabs_status.text = context.getText(stringResId)
sync_tabs_status.text = context.getText(stringResourceForError(error))
synced_tabs_list.visibility = View.GONE
sync_tabs_status.visibility = View.VISIBLE
@ -65,19 +57,7 @@ class SyncedTabsLayout @JvmOverloads constructor(
synced_tabs_list.visibility = View.VISIBLE
sync_tabs_status.visibility = View.GONE
val allDeviceTabs = emptyList<SyncedTabsAdapter.AdapterItem>().toMutableList()
syncedTabs.forEach { (device, tabs) ->
if (tabs.isEmpty()) {
return@forEach
}
val deviceTabs = tabs.map { SyncedTabsAdapter.AdapterItem.Tab(it) }
allDeviceTabs += listOf(SyncedTabsAdapter.AdapterItem.Device(device)) + deviceTabs
}
adapter.submitList(allDeviceTabs)
adapter.updateData(syncedTabs)
}
}
@ -110,5 +90,13 @@ class SyncedTabsLayout @JvmOverloads constructor(
SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE,
SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> true
}
internal fun stringResourceForError(error: SyncedTabsView.ErrorType) = when (error) {
SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE -> R.string.synced_tabs_connect_another_device
SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE -> R.string.synced_tabs_enable_tab_syncing
SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> R.string.synced_tabs_connect_to_sync_account
SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth
SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> R.string.synced_tabs_no_tabs
}
}
}

View File

@ -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
}
}

View File

@ -6,26 +6,41 @@ package org.mozilla.fenix.tabtray
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.tab_tray_item.view.*
import mozilla.components.browser.tabstray.TabViewHolder
import mozilla.components.browser.tabstray.TabsAdapter
import mozilla.components.concept.tabstray.Tab
import mozilla.components.concept.tabstray.Tabs
import mozilla.components.support.images.loader.ImageLoader
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
class FenixTabsAdapter(
context: Context,
private val context: Context,
imageLoader: ImageLoader
) : TabsAdapter(
viewHolderProvider = { parentView, _ ->
viewHolderProvider = { parentView ->
TabTrayViewHolder(
LayoutInflater.from(context).inflate(
R.layout.tab_tray_item,
parentView,
false),
false
),
imageLoader
)
}
) {
var tabTrayInteractor: TabTrayInteractor? = null
private val mode: TabTrayDialogFragmentState.Mode?
get() = tabTrayInteractor?.onModeRequested()
val selectedItems get() = mode?.selectedItems ?: setOf()
var onTabsUpdated: (() -> Unit)? = null
var tabCount = 0
@ -35,9 +50,59 @@ class FenixTabsAdapter(
tabCount = tabs.list.size
}
override fun onBindViewHolder(
holder: TabViewHolder,
position: Int,
payloads: MutableList<Any>
) {
if (payloads.isNullOrEmpty()) {
onBindViewHolder(holder, position)
return
}
holder.tab?.let { showCheckedIfSelected(it, holder.itemView) }
}
override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
super.onBindViewHolder(holder, position)
val newIndex = tabCount - position - 1
(holder as TabTrayViewHolder).updateAccessibilityRowIndex(holder.itemView, newIndex)
holder.tab?.let { tab ->
showCheckedIfSelected(tab, holder.itemView)
val tabIsPrivate =
context.components.core.sessionManager.findSessionById(tab.id)?.private == true
if (!tabIsPrivate) {
holder.itemView.setOnLongClickListener {
if (mode is TabTrayDialogFragmentState.Mode.Normal) {
context.metrics.track(Event.CollectionTabLongPressed)
tabTrayInteractor?.onAddSelectedTab(
tab
)
}
true
}
}
holder.itemView.setOnClickListener {
if (mode is TabTrayDialogFragmentState.Mode.MultiSelect) {
if (mode?.selectedItems?.contains(tab) == true) {
tabTrayInteractor?.onRemoveSelectedTab(tab = tab)
} else {
tabTrayInteractor?.onAddSelectedTab(tab = tab)
}
} else {
tabTrayInteractor?.onOpenTab(tab = tab)
}
}
}
}
private fun showCheckedIfSelected(tab: Tab, view: View) {
val shouldBeChecked =
mode is TabTrayDialogFragmentState.Mode.MultiSelect && selectedItems.contains(tab)
view.checkmark.isVisible = shouldBeChecked
view.selected_mask.isVisible = shouldBeChecked
}
}

View File

@ -6,76 +6,98 @@ package org.mozilla.fenix.tabtray
import androidx.annotation.VisibleForTesting
import androidx.navigation.NavController
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.concept.tabstray.Tab
import mozilla.components.feature.tabs.TabsUseCases
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.collections.SaveCollectionStep
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.home.HomeFragment
/**
* [TabTrayDialogFragment] controller.
*
* Delegated by View Interactors, handles container business logic and operates changes on it.
*/
@Suppress("TooManyFunctions")
interface TabTrayController {
fun onNewTabTapped(private: Boolean)
fun onTabTrayDismissed()
fun onShareTabsClicked(private: Boolean)
fun onSaveToCollectionClicked()
fun onSaveToCollectionClicked(selectedTabs: Set<Tab>)
fun onCloseAllTabsClicked(private: Boolean)
fun handleBackPressed(): Boolean
fun onModeRequested(): TabTrayDialogFragmentState.Mode
fun handleAddSelectedTab(tab: Tab)
fun handleRemoveSelectedTab(tab: Tab)
fun handleOpenTab(tab: Tab)
fun handleEnterMultiselect()
}
/**
* Default behavior of [TabTrayController]. Other implementations are possible.
*
* @param activity [HomeActivity] used for context and other Android interactions.
* @param navController [NavController] used for navigation.
* @param dismissTabTray callback allowing to request this entire Fragment to be dismissed.
* @param tabTrayDialogFragmentStore [TabTrayDialogFragmentStore] holding the State for all Views displayed
* in this Controller's Fragment.
* @param dismissTabTrayAndNavigateHome callback allowing showing an undo snackbar after tab deletion.
* @param selectTabUseCase [TabsUseCases.SelectTabUseCase] callback allowing for selecting a tab.
* @param registerCollectionStorageObserver callback allowing for registering the [TabCollectionStorage.Observer]
* when needed.
* @param showChooseCollectionDialog callback allowing saving a list of sessions to an existing collection.
* @param showAddNewCollectionDialog callback allowing for saving a list of sessions to a new collection.
*/
@Suppress("TooManyFunctions")
class DefaultTabTrayController(
private val activity: HomeActivity,
private val navController: NavController,
private val dismissTabTray: () -> Unit,
private val showUndoSnackbar: (String, SessionManager.Snapshot) -> Unit,
private val registerCollectionStorageObserver: () -> Unit
private val dismissTabTrayAndNavigateHome: (String) -> Unit,
private val registerCollectionStorageObserver: () -> Unit,
private val tabTrayDialogFragmentStore: TabTrayDialogFragmentStore,
private val selectTabUseCase: TabsUseCases.SelectTabUseCase,
private val showChooseCollectionDialog: (List<Session>) -> Unit,
private val showAddNewCollectionDialog: (List<Session>) -> Unit
) : TabTrayController {
private val tabCollectionStorage = activity.components.core.tabCollectionStorage
override fun onNewTabTapped(private: Boolean) {
val startTime = activity.components.core.engine.profiler?.getProfilerTime()
activity.browsingModeManager.mode = BrowsingMode.fromBoolean(private)
navController.navigate(TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true))
dismissTabTray()
activity.components.core.engine.profiler?.addMarker("DefaultTabTrayController.onNewTabTapped", startTime)
activity.components.core.engine.profiler?.addMarker(
"DefaultTabTrayController.onNewTabTapped",
startTime
)
}
override fun onTabTrayDismissed() {
dismissTabTray()
}
override fun onSaveToCollectionClicked() {
val tabs = getListOfSessions(false)
val tabIds = tabs.map { it.id }.toList().toTypedArray()
val tabCollectionStorage = activity.components.core.tabCollectionStorage
val step = when {
// Show the SelectTabs fragment if there are multiple opened tabs to select which tabs
// you want to save to a collection.
tabs.size > 1 -> SaveCollectionStep.SelectTabs
// If there is an existing tab collection, show the SelectCollection fragment to save
// the selected tab to a collection of your choice.
tabCollectionStorage.cachedTabCollections.isNotEmpty() -> SaveCollectionStep.SelectCollection
// Show the NameCollection fragment to create a new collection for the selected tab.
else -> SaveCollectionStep.NameCollection
override fun onSaveToCollectionClicked(selectedTabs: Set<Tab>) {
val sessionList = selectedTabs.map {
activity.components.core.sessionManager.findSessionById(it.id) ?: return
}
if (navController.currentDestination?.id == R.id.collectionCreationFragment) return
// Only register the observer right before moving to collection creation
registerCollectionStorageObserver()
val directions = TabTrayDialogFragmentDirections.actionGlobalCollectionCreationFragment(
tabIds = tabIds,
saveCollectionStep = step,
selectedTabIds = tabIds
)
navController.navigate(directions)
when {
tabCollectionStorage.cachedTabCollections.isNotEmpty() -> {
showChooseCollectionDialog(sessionList)
}
else -> {
showAddNewCollectionDialog(sessionList)
}
}
}
override fun onShareTabsClicked(private: Boolean) {
@ -89,39 +111,48 @@ class DefaultTabTrayController(
navController.navigate(directions)
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun onCloseAllTabsClicked(private: Boolean) {
val sessionManager = activity.components.core.sessionManager
val tabs = getListOfSessions(private)
val selectedIndex = sessionManager
.selectedSession?.let { sessionManager.sessions.indexOf(it) } ?: 0
val snapshot = tabs
.map(sessionManager::createSessionSnapshot)
.map {
it.copy(
engineSession = null,
engineSessionState = it.engineSession?.saveState()
)
}
.let { SessionManager.Snapshot(it, selectedIndex) }
tabs.forEach {
sessionManager.remove(it)
}
val snackbarMessage = if (private) {
activity.getString(R.string.snackbar_private_tabs_closed)
val sessionsToClose = if (private) {
HomeFragment.ALL_PRIVATE_TABS
} else {
activity.getString(R.string.snackbar_tabs_closed)
HomeFragment.ALL_NORMAL_TABS
}
showUndoSnackbar(snackbarMessage, snapshot)
dismissTabTray()
dismissTabTrayAndNavigateHome(sessionsToClose)
}
override fun handleAddSelectedTab(tab: Tab) {
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.AddItemForCollection(tab))
}
override fun handleRemoveSelectedTab(tab: Tab) {
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.RemoveItemForCollection(tab))
}
override fun handleBackPressed(): Boolean {
return if (tabTrayDialogFragmentStore.state.mode is TabTrayDialogFragmentState.Mode.MultiSelect) {
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
true
} else {
false
}
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
private fun getListOfSessions(private: Boolean): List<Session> {
return activity.components.core.sessionManager.sessionsOfType(private = private).toList()
}
override fun onModeRequested(): TabTrayDialogFragmentState.Mode {
return tabTrayDialogFragmentStore.state.mode
}
override fun handleOpenTab(tab: Tab) {
selectTabUseCase.invoke(tab.id)
}
override fun handleEnterMultiselect() {
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.EnterMultiSelectMode)
}
}

View File

@ -4,51 +4,66 @@
package org.mozilla.fenix.tabtray
import android.app.Dialog
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.component_tabstray.view.*
import kotlinx.android.synthetic.main.component_tabstray_fab.view.*
import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.*
import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.view.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.thumbnails.loader.ThumbnailLoader
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.feature.tabs.tabstray.TabsFeature
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.view.showKeyboard
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.getDefaultCollectionNumber
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.normalSessionSize
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentState.Mode
import org.mozilla.fenix.utils.allowUndo
@SuppressWarnings("TooManyFunctions", "LargeClass")
class TabTrayDialogFragment : AppCompatDialogFragment() {
class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
private val args by navArgs<TabTrayDialogFragmentArgs>()
private val tabsFeature = ViewBoundFeatureWrapper<TabsFeature>()
private var _tabTrayView: TabTrayView? = null
private val tabTrayView: TabTrayView
get() = _tabTrayView!!
private lateinit var tabTrayDialogStore: TabTrayDialogFragmentStore
private val snackbarAnchor: View?
get() = if (tabTrayView.fabView.new_tab_button.isVisible) tabTrayView.fabView.new_tab_button
@ -78,17 +93,36 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return object : Dialog(requireContext(), this.theme) {
override fun onBackPressed() {
this@TabTrayDialogFragment.onBackPressed()
}
}
}
private val removeTabUseCase = object : TabsUseCases.RemoveTabUseCase {
override fun invoke(sessionId: String) {
requireContext().components.analytics.metrics.track(Event.ClosedExistingTab)
showUndoSnackbarForTab(sessionId)
requireComponents.useCases.tabsUseCases.removeTab(sessionId)
removeIfNotLastTab(sessionId)
}
override fun invoke(session: Session) {
requireContext().components.analytics.metrics.track(Event.ClosedExistingTab)
showUndoSnackbarForTab(session.id)
requireComponents.useCases.tabsUseCases.removeTab(session)
removeIfNotLastTab(session.id)
}
}
private fun removeIfNotLastTab(sessionId: String) {
// We only want to *immediately* remove a tab if there are more than one in the tab tray
// If there is only one, the HomeFragment handles deleting the tab (to better support snackbars)
val sessionManager = view?.context?.components?.core?.sessionManager
val sessionToRemove = sessionManager?.findSessionById(sessionId)
if (sessionManager?.sessions?.filter { sessionToRemove?.private == it.private }?.size != 1) {
requireComponents.useCases.tabsUseCases.removeTab(sessionId)
}
}
@ -101,7 +135,18 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.fragment_tab_tray_dialog, container, false)
): View? {
tabTrayDialogStore = StoreProvider.get(this) {
TabTrayDialogFragmentStore(
TabTrayDialogFragmentState(
requireComponents.core.store.state,
if (args.enterMultiselect) Mode.MultiSelect(setOf()) else Mode.Normal
)
)
}
return inflater.inflate(R.layout.fragment_tab_tray_dialog, container, false)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
@ -120,15 +165,23 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
super.onViewCreated(view, savedInstanceState)
val isPrivate = (activity as HomeActivity).browsingModeManager.mode.isPrivate
val thumbnailLoader = ThumbnailLoader(requireContext().components.core.thumbnailStorage)
val adapter = FenixTabsAdapter(requireContext(), thumbnailLoader)
_tabTrayView = TabTrayView(
view.tabLayout,
adapter,
interactor = TabTrayFragmentInteractor(
DefaultTabTrayController(
activity = (activity as HomeActivity),
navController = findNavController(),
dismissTabTray = ::dismissAllowingStateLoss,
showUndoSnackbar = ::showUndoSnackbar,
registerCollectionStorageObserver = ::registerCollectionStorageObserver
dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome,
registerCollectionStorageObserver = ::registerCollectionStorageObserver,
tabTrayDialogFragmentStore = tabTrayDialogStore,
selectTabUseCase = selectTabUseCase,
showChooseCollectionDialog = ::showChooseCollectionDialog,
showAddNewCollectionDialog = ::showAddNewCollectionDialog
)
),
isPrivate = isPrivate,
@ -145,7 +198,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
tabsFeature.set(
TabsFeature(
tabTrayView.view.tabsTray,
adapter,
view.context.components.core.store,
selectTabUseCase,
removeTabUseCase,
@ -176,8 +229,11 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
}
consumeFrom(requireComponents.core.store) {
tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.BrowserStateChanged(it))
}
consumeFrom(tabTrayDialogStore) {
tabTrayView.updateState(it)
navigateHomeIfNeeded(it)
}
}
@ -191,11 +247,21 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
private fun showUndoSnackbarForTab(sessionId: String) {
val sessionManager = view?.context?.components?.core?.sessionManager
val snapshot = sessionManager
?.findSessionById(sessionId)?.let {
sessionManager.createSessionSnapshot(it)
} ?: return
// Check if this is the last tab of this session type
val isLastOpenTab =
sessionManager.sessions.filter { snapshot.session.private == it.private }.size == 1
if (isLastOpenTab) {
dismissTabTrayAndNavigateHome(sessionId)
return
}
val state = snapshot.engineSession?.saveState()
val isSelected = sessionId == requireComponents.core.store.state.selectedTabId ?: false
@ -205,13 +271,8 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
getString(R.string.snackbar_tab_closed)
}
// Check if this is the last tab of this session type
val isLastOpenTab = sessionManager.sessions.filter { snapshot.session.private }.size == 1
val rootView = if (isLastOpenTab) { requireActivity().getRootView()!! } else { requireView().tabLayout }
val anchorView = if (isLastOpenTab) { null } else { snackbarAnchor }
requireActivity().lifecycleScope.allowUndo(
rootView,
lifecycleScope.allowUndo(
requireView().tabLayout,
snackbarMessage,
getString(R.string.snackbar_deleted_undo),
{
@ -220,18 +281,14 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
},
operation = { },
elevation = ELEVATION,
paddedForBottomToolbar = isLastOpenTab,
anchorView = anchorView
anchorView = snackbarAnchor
)
dismissTabTrayIfNecessary()
}
private fun dismissTabTrayIfNecessary() {
if (requireComponents.core.sessionManager.sessions.size == 1) {
findNavController().popBackStack(R.id.homeFragment, false)
dismissAllowingStateLoss()
}
private fun dismissTabTrayAndNavigateHome(sessionId: String) {
val directions = BrowserFragmentDirections.actionGlobalHome(sessionToDelete = sessionId)
findNavController().navigate(directions)
dismissAllowingStateLoss()
}
override fun onDestroyView() {
@ -247,39 +304,10 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
}
}
private fun navigateHomeIfNeeded(state: BrowserState) {
val shouldPop = if (tabTrayView.isPrivateModeSelected) {
state.privateTabs.isEmpty()
} else {
state.normalTabs.isEmpty()
}
if (shouldPop) {
findNavController().popBackStack(R.id.homeFragment, false)
}
}
private fun registerCollectionStorageObserver() {
requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this)
}
private fun showUndoSnackbar(snackbarMessage: String, snapshot: SessionManager.Snapshot) {
// Warning: removing this definition and using it directly in the onCancel block will fail silently.
val sessionManager = view?.context?.components?.core?.sessionManager
requireActivity().lifecycleScope.allowUndo(
requireActivity().getRootView()!!,
snackbarMessage,
getString(R.string.snackbar_deleted_undo),
{
sessionManager?.restore(snapshot)
},
operation = { },
elevation = ELEVATION,
paddedForBottomToolbar = true
)
}
private fun showCollectionSnackbar(tabSize: Int, isNewCollection: Boolean = false) {
view.let {
val messageStringRes = when {
@ -313,21 +341,101 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
}
}
companion object {
private const val ELEVATION = 80f
private const val FRAGMENT_TAG = "tabTrayDialogFragment"
override fun onBackPressed(): Boolean {
if (!tabTrayView.onBackPressed()) {
dismiss()
}
return true
}
fun show(fragmentManager: FragmentManager) {
// If we've killed the fragmentManager. Let's not try to show the tabs tray.
if (fragmentManager.isDestroyed) {
return
}
private fun showChooseCollectionDialog(sessionList: List<Session>) {
context?.let {
val tabCollectionStorage = it.components.core.tabCollectionStorage
val collections =
tabCollectionStorage.cachedTabCollections.map { it.title }.toTypedArray()
val customLayout =
LayoutInflater.from(it).inflate(R.layout.add_new_collection_dialog, null)
val list = customLayout.findViewById<RecyclerView>(R.id.recycler_view)
list.layoutManager = LinearLayoutManager(it)
// We want to make sure we don't accidentally show the dialog twice if
// a user somehow manages to trigger `show()` twice before we present the dialog.
if (fragmentManager.findFragmentByTag(FRAGMENT_TAG) == null) {
TabTrayDialogFragment().showNow(fragmentManager, FRAGMENT_TAG)
}
val builder = AlertDialog.Builder(it).setTitle(R.string.tab_tray_select_collection)
.setView(customLayout)
.setPositiveButton(android.R.string.ok) { dialog, _ ->
val selectedCollection =
(list.adapter as CollectionsAdapter).getSelectedCollection()
val collection = tabCollectionStorage.cachedTabCollections[selectedCollection]
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
tabCollectionStorage.addTabsToCollection(collection, sessionList)
it.metrics.track(
Event.CollectionTabsAdded(
it.components.core.sessionManager.normalSessionSize(),
sessionList.size
)
)
launch(Main) {
tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
dialog.dismiss()
}
}
}.setNegativeButton(android.R.string.cancel) { dialog, _ ->
tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
dialog.cancel()
}
val dialog = builder.create()
val adapter =
CollectionsAdapter(arrayOf(it.getString(R.string.tab_tray_add_new_collection)) + collections) {
dialog.dismiss()
showAddNewCollectionDialog(sessionList)
}
list.adapter = adapter
dialog.show()
}
}
private fun showAddNewCollectionDialog(sessionList: List<Session>) {
context?.let {
val tabCollectionStorage = it.components.core.tabCollectionStorage
val customLayout =
LayoutInflater.from(it).inflate(R.layout.name_collection_dialog, null)
val collectionNameEditText: EditText =
customLayout.findViewById(R.id.collection_name)
collectionNameEditText.setText(
it.getString(
R.string.create_collection_default_name,
tabCollectionStorage.cachedTabCollections.getDefaultCollectionNumber()
)
)
AlertDialog.Builder(it).setTitle(R.string.tab_tray_add_new_collection)
.setView(customLayout).setPositiveButton(android.R.string.ok) { dialog, _ ->
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
tabCollectionStorage.createCollection(
collectionNameEditText.text.toString(),
sessionList
)
it.metrics.track(
Event.CollectionSaved(
it.components.core.sessionManager.normalSessionSize(),
sessionList.size
)
)
launch(Main) {
tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
dialog.dismiss()
}
}
}.setNegativeButton(android.R.string.cancel) { dialog, _ ->
tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
dialog.cancel()
}.create().show().also {
collectionNameEditText.setSelection(0, collectionNameEditText.text.length)
collectionNameEditText.showKeyboard()
}
}
}
companion object {
private const val ELEVATION = 80f
}
}

View File

@ -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()
)
)
}
}

View File

@ -4,17 +4,70 @@
package org.mozilla.fenix.tabtray
import mozilla.components.concept.tabstray.Tab
@Suppress("TooManyFunctions")
interface TabTrayInteractor {
/**
* Called when user clicks the new tab button.
*/
fun onNewTabTapped(private: Boolean)
/**
* Called when tab tray should be dismissed.
*/
fun onTabTrayDismissed()
/**
* Called when user clicks the share tabs button.
*/
fun onShareTabsClicked(private: Boolean)
fun onSaveToCollectionClicked()
/**
* Called when user clicks button to save selected tabs to a collection.
*/
fun onSaveToCollectionClicked(selectedTabs: Set<Tab>)
/**
* Called when user clicks the close all tabs button.
*/
fun onCloseAllTabsClicked(private: Boolean)
/**
* Called when the physical back button is clicked.
*/
fun onBackPressed(): Boolean
/**
* Called when a requester needs to know the current mode of the tab tray.
*/
fun onModeRequested(): TabTrayDialogFragmentState.Mode
/**
* Called when a tab should be opened in the browser.
*/
fun onOpenTab(tab: Tab)
/**
* Called when a tab should be selected in multiselect mode.
*/
fun onAddSelectedTab(tab: Tab)
/**
* Called when a tab should be unselected in multiselect mode.
*/
fun onRemoveSelectedTab(tab: Tab)
/**
* Called when multiselect mode should be entered with no tabs selected.
*/
fun onEnterMultiselect()
}
/**
* Interactor for the tab tray fragment.
*/
@Suppress("TooManyFunctions")
class TabTrayFragmentInteractor(private val controller: TabTrayController) : TabTrayInteractor {
override fun onNewTabTapped(private: Boolean) {
controller.onNewTabTapped(private)
@ -28,11 +81,35 @@ class TabTrayFragmentInteractor(private val controller: TabTrayController) : Tab
controller.onShareTabsClicked(private)
}
override fun onSaveToCollectionClicked() {
controller.onSaveToCollectionClicked()
override fun onSaveToCollectionClicked(selectedTabs: Set<Tab>) {
controller.onSaveToCollectionClicked(selectedTabs)
}
override fun onCloseAllTabsClicked(private: Boolean) {
controller.onCloseAllTabsClicked(private)
}
override fun onBackPressed(): Boolean {
return controller.handleBackPressed()
}
override fun onModeRequested(): TabTrayDialogFragmentState.Mode {
return controller.onModeRequested()
}
override fun onAddSelectedTab(tab: Tab) {
controller.handleAddSelectedTab(tab)
}
override fun onRemoveSelectedTab(tab: Tab) {
controller.handleRemoveSelectedTab(tab)
}
override fun onOpenTab(tab: Tab) {
controller.handleOpenTab(tab)
}
override fun onEnterMultiselect() {
controller.handleEnterMultiselect()
}
}

View File

@ -9,14 +9,18 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import androidx.annotation.IdRes
import androidx.cardview.widget.CardView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.tabs.TabLayout
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_tabstray.*
import kotlinx.android.synthetic.main.component_tabstray.view.*
import kotlinx.android.synthetic.main.component_tabstray_fab.view.*
import kotlinx.android.synthetic.main.tabs_tray_tab_counter.*
@ -29,7 +33,7 @@ import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.tabstray.BrowserTabsTray
import mozilla.components.support.ktx.android.util.dpToPx
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
@ -41,6 +45,7 @@ import org.mozilla.fenix.ext.settings
@Suppress("LongParameterList", "TooManyFunctions", "LargeClass")
class TabTrayView(
private val container: ViewGroup,
private val tabsAdapter: FenixTabsAdapter,
private val interactor: TabTrayInteractor,
isPrivate: Boolean,
startingInLandscape: Boolean,
@ -50,16 +55,20 @@ class TabTrayView(
val fabView = LayoutInflater.from(container.context)
.inflate(R.layout.component_tabstray_fab, container, true)
private val hasAccessibilityEnabled = container.context.settings().accessibilityServicesEnabled
val view = LayoutInflater.from(container.context)
.inflate(R.layout.component_tabstray, container, true)
val isPrivateModeSelected: Boolean get() = view.tab_layout.selectedTabPosition == PRIVATE_TAB_ID
private val isPrivateModeSelected: Boolean get() = view.tab_layout.selectedTabPosition == PRIVATE_TAB_ID
private val behavior = BottomSheetBehavior.from(view.tab_wrapper)
private val tabTrayItemMenu: TabTrayItemMenu
private var menu: BrowserMenu? = null
private var tabsTouchHelper: TabsTouchHelper
private var hasLoaded = false
override val containerView: View?
@ -68,8 +77,6 @@ class TabTrayView(
init {
container.context.components.analytics.metrics.track(Event.TabsTrayOpened)
val hasAccessibilityEnabled = view.context.settings().accessibilityServicesEnabled
toggleFabText(isPrivate)
behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
@ -118,27 +125,34 @@ class TabTrayView(
setTopOffset(startingInLandscape)
(view.tabsTray as? BrowserTabsTray)?.also { tray ->
TabsTouchHelper(tray.tabsAdapter).attachToRecyclerView(tray)
(tray.tabsAdapter as? FenixTabsAdapter)?.also { adapter ->
adapter.onTabsUpdated = {
if (hasAccessibilityEnabled) {
adapter.notifyDataSetChanged()
}
if (!hasLoaded) {
hasLoaded = true
scrollToTab(view.context.components.core.store.state.selectedTabId)
if (view.context.settings().accessibilityServicesEnabled) {
lifecycleScope.launch {
delay(SELECTION_DELAY.toLong())
lifecycleScope.launch(Main) {
tray.layoutManager?.findViewByPosition(selectedBrowserTabIndex)
?.requestFocus()
tray.layoutManager?.findViewByPosition(selectedBrowserTabIndex)
?.sendAccessibilityEvent(
AccessibilityEvent.TYPE_VIEW_FOCUSED
)
}
view.tabsTray.apply {
layoutManager = LinearLayoutManager(container.context).apply {
reverseLayout = true
stackFromEnd = true
}
adapter = tabsAdapter
tabsTouchHelper = TabsTouchHelper(tabsAdapter)
tabsTouchHelper.attachToRecyclerView(this)
tabsAdapter.tabTrayInteractor = interactor
tabsAdapter.onTabsUpdated = {
if (hasAccessibilityEnabled) {
tabsAdapter.notifyDataSetChanged()
}
if (!hasLoaded) {
hasLoaded = true
scrollToTab(view.context.components.core.store.state.selectedTabId)
if (view.context.settings().accessibilityServicesEnabled) {
lifecycleScope.launch {
delay(SELECTION_DELAY.toLong())
lifecycleScope.launch(Main) {
layoutManager?.findViewByPosition(selectedBrowserTabIndex)
?.requestFocus()
layoutManager?.findViewByPosition(selectedBrowserTabIndex)
?.sendAccessibilityEvent(
AccessibilityEvent.TYPE_VIEW_FOCUSED
)
}
}
}
@ -152,7 +166,7 @@ class TabTrayView(
is TabTrayItemMenu.Item.ShareAllTabs -> interactor.onShareTabsClicked(
isPrivateModeSelected
)
is TabTrayItemMenu.Item.SaveToCollection -> interactor.onSaveToCollectionClicked()
is TabTrayItemMenu.Item.SaveToCollection -> interactor.onEnterMultiselect()
is TabTrayItemMenu.Item.CloseAllTabs -> interactor.onCloseAllTabsClicked(
isPrivateModeSelected
)
@ -173,6 +187,10 @@ class TabTrayView(
}
}
adjustNewTabButtonsForNormalMode()
}
private fun adjustNewTabButtonsForNormalMode() {
view.tab_tray_new_tab.apply {
isVisible = hasAccessibilityEnabled
setOnClickListener {
@ -208,7 +226,7 @@ class TabTrayView(
toggleFabText(isPrivateModeSelected)
filterTabs.invoke(isPrivateModeSelected)
updateState(view.context.components.core.store.state)
updateUINormalMode(view.context.components.core.store.state)
scrollToTab(view.context.components.core.store.state.selectedTabId)
if (isPrivateModeSelected) {
@ -224,32 +242,168 @@ class TabTrayView(
override fun onTabUnselected(tab: TabLayout.Tab?) { /*noop*/
}
fun updateState(state: BrowserState) {
view.let {
val hasNoTabs = if (isPrivateModeSelected) {
state.privateTabs.isEmpty()
} else {
state.normalTabs.isEmpty()
}
var mode: TabTrayDialogFragmentState.Mode = TabTrayDialogFragmentState.Mode.Normal
private set
view.tab_tray_empty_view.isVisible = hasNoTabs
if (hasNoTabs) {
view.tab_tray_empty_view.text = if (isPrivateModeSelected) {
view.context.getString(R.string.no_private_tabs_description)
} else {
view.context?.getString(R.string.no_open_tabs_description)
fun updateState(state: TabTrayDialogFragmentState) {
val oldMode = mode
if (oldMode::class != state.mode::class && view.context.settings().accessibilityServicesEnabled) {
view.announceForAccessibility(
if (state.mode == TabTrayDialogFragmentState.Mode.Normal) view.context.getString(
R.string.tab_tray_exit_multiselect_content_description
) else view.context.getString(R.string.tab_tray_enter_multiselect_content_description)
)
}
mode = state.mode
when (state.mode) {
TabTrayDialogFragmentState.Mode.Normal -> {
view.tabsTray.apply {
tabsTouchHelper.attachToRecyclerView(this)
}
toggleUIMultiselect(multiselect = false)
updateUINormalMode(state.browserState)
}
is TabTrayDialogFragmentState.Mode.MultiSelect -> {
// Disable swipe to delete while in multiselect
tabsTouchHelper.attachToRecyclerView(null)
toggleUIMultiselect(multiselect = true)
fabView.new_tab_button.isVisible = false
view.tab_tray_new_tab.isVisible = false
view.collect_multi_select.isVisible = state.mode.selectedItems.size > 0
view.multiselect_title.text = view.context.getString(
R.string.tab_tray_multi_select_title,
state.mode.selectedItems.size
)
view.collect_multi_select.setOnClickListener {
interactor.onSaveToCollectionClicked(state.mode.selectedItems)
}
view.exit_multi_select.setOnClickListener {
interactor.onBackPressed()
}
}
}
view.tabsTray.asView().visibility = if (hasNoTabs) {
View.INVISIBLE
} else {
View.VISIBLE
if (oldMode.selectedItems != state.mode.selectedItems) {
val unselectedItems = oldMode.selectedItems - state.mode.selectedItems
state.mode.selectedItems.union(unselectedItems).forEach { item ->
if (view.context.settings().accessibilityServicesEnabled) {
view.announceForAccessibility(
if (unselectedItems.contains(item)) view.context.getString(
R.string.tab_tray_item_unselected_multiselect_content_description,
item.title
) else view.context.getString(
R.string.tab_tray_item_selected_multiselect_content_description,
item.title
)
)
}
updateTabsForSelectionChanged(item.id)
}
view.tab_tray_overflow.isVisible = !hasNoTabs
}
}
counter_text.text = "${state.normalTabs.size}"
updateTabCounterContentDescription(state.normalTabs.size)
private fun ConstraintLayout.setChildWPercent(percentage: Float, @IdRes childId: Int) {
this.findViewById<View>(childId)?.let {
val constraintSet = ConstraintSet()
constraintSet.clone(this)
constraintSet.constrainPercentWidth(it.id, percentage)
constraintSet.applyTo(this)
it.requestLayout()
}
}
private fun updateUINormalMode(browserState: BrowserState) {
val hasNoTabs = if (isPrivateModeSelected) {
browserState.privateTabs.isEmpty()
} else {
browserState.normalTabs.isEmpty()
}
view.tab_tray_empty_view.isVisible = hasNoTabs
if (hasNoTabs) {
view.tab_tray_empty_view.text = if (isPrivateModeSelected) {
view.context.getString(R.string.no_private_tabs_description)
} else {
view.context?.getString(R.string.no_open_tabs_description)
}
}
view.tabsTray.visibility = if (hasNoTabs) {
View.INVISIBLE
} else {
View.VISIBLE
}
view.tab_tray_overflow.isVisible = !hasNoTabs
counter_text.text = "${browserState.normalTabs.size}"
updateTabCounterContentDescription(browserState.normalTabs.size)
adjustNewTabButtonsForNormalMode()
}
private fun toggleUIMultiselect(multiselect: Boolean) {
view.multiselect_title.isVisible = multiselect
view.collect_multi_select.isVisible = multiselect
view.exit_multi_select.isVisible = multiselect
view.topBar.setBackgroundColor(
ContextCompat.getColor(
view.context,
if (multiselect) R.color.accent_normal_theme else R.color.foundation_normal_theme
)
)
val displayMetrics = view.context.resources.displayMetrics
view.handle.updateLayoutParams<ViewGroup.MarginLayoutParams> {
height =
if (multiselect) MULTISELECT_HANDLE_HEIGHT.dpToPx(displayMetrics) else NORMAL_HANDLE_HEIGHT.dpToPx(
displayMetrics
)
topMargin = if (multiselect) 0.dpToPx(displayMetrics) else NORMAL_TOP_MARGIN.dpToPx(
displayMetrics
)
}
view.tab_wrapper.setChildWPercent(
if (multiselect) 1F else NORMAL_HANDLE_PERCENT_WIDTH,
view.handle.id
)
view.handle.setBackgroundColor(
ContextCompat.getColor(
view.context,
if (multiselect) R.color.accent_normal_theme else R.color.secondary_text_normal_theme
)
)
view.tab_layout.isVisible = !multiselect
view.tab_tray_empty_view.isVisible = !multiselect
view.tab_tray_overflow.isVisible = !multiselect
view.tab_layout.isVisible = !multiselect
}
private fun updateTabsForSelectionChanged(itemId: String) {
view.tabsTray.apply {
val tabs = if (isPrivateModeSelected) {
view.context.components.core.store.state.privateTabs
} else {
view.context.components.core.store.state.normalTabs
}
val selectedBrowserTabIndex = tabs.indexOfFirst { it.id == itemId }
this.adapter?.notifyItemChanged(
selectedBrowserTabIndex, true
)
}
}
@ -287,8 +441,12 @@ class TabTrayView(
}
}
fun onBackPressed(): Boolean {
return interactor.onBackPressed()
}
fun scrollToTab(sessionId: String?) {
(view.tabsTray as? BrowserTabsTray)?.also { tray ->
view.tabsTray.apply {
val tabs = if (isPrivateModeSelected) {
view.context.components.core.store.state.privateTabs
} else {
@ -298,7 +456,7 @@ class TabTrayView(
val selectedBrowserTabIndex = tabs
.indexOfFirst { it.id == sessionId }
tray.layoutManager?.scrollToPosition(selectedBrowserTabIndex)
layoutManager?.scrollToPosition(selectedBrowserTabIndex)
}
}
@ -308,6 +466,10 @@ class TabTrayView(
private const val EXPAND_AT_SIZE = 3
private const val SLIDE_OFFSET = 0
private const val SELECTION_DELAY = 500
private const val MULTISELECT_HANDLE_HEIGHT = 11
private const val NORMAL_HANDLE_HEIGHT = 3
private const val NORMAL_TOP_MARGIN = 8
private const val NORMAL_HANDLE_PERCENT_WIDTH = 0.1F
}
}

View File

@ -10,10 +10,13 @@ import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.VisibleForTesting
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.AppCompatImageButton
import androidx.core.content.ContextCompat
import mozilla.components.browser.state.state.MediaState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.tabstray.TabViewHolder
import mozilla.components.browser.tabstray.TabsTrayStyling
import mozilla.components.browser.tabstray.thumbnail.TabThumbnailView
import mozilla.components.browser.toolbar.MAX_URI_LENGTH
import mozilla.components.concept.tabstray.Tab
@ -26,12 +29,14 @@ import mozilla.components.support.images.loader.ImageLoader
import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getMediaStateForSession
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.ext.removeAndDisable
import org.mozilla.fenix.ext.removeTouchDelegate
import org.mozilla.fenix.ext.showAndEnable
import org.mozilla.fenix.ext.toTab
import org.mozilla.fenix.utils.Do
import kotlin.math.max
/**
@ -40,8 +45,10 @@ import kotlin.math.max
class TabTrayViewHolder(
itemView: View,
private val imageLoader: ImageLoader,
val getSelectedTabId: () -> String? = { itemView.context.components.core.store.state.selectedTabId }
private val store: BrowserStore = itemView.context.components.core.store,
private val metrics: MetricController = itemView.context.components.analytics.metrics
) : TabViewHolder(itemView) {
private val titleView: TextView = itemView.findViewById(R.id.mozac_browser_tabstray_title)
private val closeView: AppCompatImageButton =
itemView.findViewById(R.id.mozac_browser_tabstray_close)
@ -57,10 +64,12 @@ class TabTrayViewHolder(
/**
* Displays the data of the given session and notifies the given observable about events.
*/
override fun bind(tab: Tab, isSelected: Boolean, observable: Observable<TabsTray.Observer>) {
// This is a hack to workaround a bug in a-c.
// https://github.com/mozilla-mobile/android-components/issues/7186
val isSelected2 = tab.id == getSelectedTabId()
override fun bind(
tab: Tab,
isSelected: Boolean,
styling: TabsTrayStyling,
observable: Observable<TabsTray.Observer>
) {
this.tab = tab
// Basic text
@ -69,7 +78,7 @@ class TabTrayViewHolder(
updateCloseButtonDescription(tab.title)
// Drawables and theme
updateBackgroundColor(isSelected2)
updateBackgroundColor(isSelected)
if (tab.thumbnail != null) {
thumbnailView.setImageBitmap(tab.thumbnail)
@ -79,16 +88,15 @@ class TabTrayViewHolder(
// Media state
playPauseButtonView.increaseTapArea(PLAY_PAUSE_BUTTON_EXTRA_DPS)
val session = itemView.context?.components?.core?.sessionManager?.findSessionById(tab.id)
with(playPauseButtonView) {
invalidate()
when (session?.toTab(itemView.context)?.mediaState) {
Do exhaustive when (store.state.getMediaStateForSession(tab.id)) {
MediaState.State.PAUSED -> {
showAndEnable()
contentDescription =
context.getString(R.string.mozac_feature_media_notification_action_play)
setImageDrawable(
androidx.appcompat.content.res.AppCompatResources.getDrawable(
AppCompatResources.getDrawable(
context,
R.drawable.tab_tray_play_with_background
)
@ -100,7 +108,7 @@ class TabTrayViewHolder(
contentDescription =
context.getString(R.string.mozac_feature_media_notification_action_pause)
setImageDrawable(
androidx.appcompat.content.res.AppCompatResources.getDrawable(
AppCompatResources.getDrawable(
context,
R.drawable.tab_tray_pause_with_background
)
@ -115,16 +123,15 @@ class TabTrayViewHolder(
}
playPauseButtonView.setOnClickListener {
val mState = session?.toTab(itemView.context)?.mediaState
when (mState) {
Do exhaustive when (store.state.getMediaStateForSession(tab.id)) {
MediaState.State.PLAYING -> {
itemView.context.components.analytics.metrics.track(Event.TabMediaPause)
itemView.context.components.core.store.state.media.pauseIfPlaying()
metrics.track(Event.TabMediaPause)
store.state.media.pauseIfPlaying()
}
MediaState.State.PAUSED -> {
itemView.context.components.analytics.metrics.track(Event.TabMediaPlay)
itemView.context.components.core.store.state.media.playIfPaused()
metrics.track(Event.TabMediaPlay)
store.state.media.playIfPaused()
}
MediaState.State.NONE -> throw AssertionError(
@ -133,10 +140,6 @@ class TabTrayViewHolder(
}
}
itemView.setOnClickListener {
observable.notifyObservers { onTabSelected(tab) }
}
closeView.setOnClickListener {
observable.notifyObservers { onTabClosed(tab) }
}
@ -189,7 +192,7 @@ class TabTrayViewHolder(
}
internal fun updateAccessibilityRowIndex(item: View, newIndex: Int) {
item.setAccessibilityDelegate(object : View.AccessibilityDelegate() {
item.accessibilityDelegate = object : View.AccessibilityDelegate() {
override fun onInitializeAccessibilityNodeInfo(
host: View?,
info: AccessibilityNodeInfo?
@ -208,7 +211,7 @@ class TabTrayViewHolder(
}
}
}
})
}
}
companion object {

View File

@ -13,7 +13,6 @@ import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.widget.ImageView
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.marginTop
import kotlinx.android.synthetic.main.tracking_protection_onboarding_popup.*
@ -22,6 +21,7 @@ import mozilla.components.browser.session.Session
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.utils.Settings
@ -63,10 +63,10 @@ class TrackingProtectionOverlay(
val layout = LayoutInflater.from(context)
.inflate(R.layout.tracking_protection_onboarding_popup, null)
val isBottomToolbar = settings.shouldUseBottomToolbar
val toolbarPosition = settings.toolbarPosition
layout.drop_down_triangle.isGone = isBottomToolbar
layout.pop_up_triangle.isVisible = isBottomToolbar
layout.drop_down_triangle.isVisible = toolbarPosition == ToolbarPosition.TOP
layout.pop_up_triangle.isVisible = toolbarPosition == ToolbarPosition.BOTTOM
layout.onboarding_message.text =
context.getString(
@ -91,11 +91,7 @@ class TrackingProtectionOverlay(
val xOffset = triangleMarginStartPx + triangleWidthPx / 2
val gravity = if (isBottomToolbar) {
Gravity.START or Gravity.BOTTOM
} else {
Gravity.START or Gravity.TOP
}
val gravity = Gravity.START or toolbarPosition.androidGravity
trackingOnboardingDialog.apply {
setContentView(layout)

View File

@ -1,6 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.trackingprotection

View File

@ -1,6 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.trackingprotectionexceptions

View File

@ -1,6 +1,6 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.trackingprotectionexceptions

View File

@ -29,6 +29,7 @@ import org.mozilla.fenix.Config
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.metrics.MozillaProductDetector
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.settings.PhoneFeature
@ -463,6 +464,9 @@ class Settings(private val appContext: Context) : PreferencesHolder {
default = !touchExplorationIsEnabled && !switchServiceIsEnabled
)
val toolbarPosition: ToolbarPosition
get() = if (shouldUseBottomToolbar) ToolbarPosition.BOTTOM else ToolbarPosition.TOP
/**
* Check each active accessibility service to see if it can perform gestures, if any can,
* then it is *likely* a switch service is enabled. We are assuming this to be the case based on #7486
@ -827,12 +831,10 @@ class Settings(private val appContext: Context) : PreferencesHolder {
get() {
return when (savedLoginsSortingStrategyString) {
SavedLoginsFragment.SORTING_STRATEGY_ALPHABETICALLY -> SortingStrategy.Alphabetically(
appContext
appContext.components.publicSuffixList
)
SavedLoginsFragment.SORTING_STRATEGY_LAST_USED -> SortingStrategy.LastUsed(
appContext
)
else -> SortingStrategy.Alphabetically(appContext)
SavedLoginsFragment.SORTING_STRATEGY_LAST_USED -> SortingStrategy.LastUsed
else -> SortingStrategy.Alphabetically(appContext.components.publicSuffixList)
}
}
set(value) {

Some files were not shown because too many files have changed in this diff Show More