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 .leanplum_token
.adjust_token .adjust_token
.sentry_token .sentry_token
.digital_asset_links_token
.mls_token .mls_token

View File

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

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 @Test
fun verifyContextCopyLink() { fun verifyContextCopyLink() {
val pageLinks = val pageLinks =

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

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

View File

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

View File

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

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 package org.mozilla.fenix
/** /**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 package org.mozilla.fenix.components
import android.view.View import android.view.View

View File

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

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 package org.mozilla.fenix.components
import android.content.Context import android.content.Context

View File

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

View File

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

View File

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

View File

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

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 lifecycleOwner View lifecycle owner used to determine when to cancel UI jobs.
* @param bookmarksStorage Used to check if a page is bookmarked. * @param bookmarksStorage Used to check if a page is bookmarked.
*/ */
@Suppress("LargeClass") @Suppress("LargeClass", "LongParameterList")
class DefaultToolbarMenu( class DefaultToolbarMenu(
private val context: Context, private val context: Context,
private val sessionManager: SessionManager, private val sessionManager: SessionManager,

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 package org.mozilla.fenix.components.toolbar
sealed class TabCounterMenuItem { sealed class TabCounterMenuItem {

View File

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

View File

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

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 * [DynamicDownloadDialog] is used to show a view in the current tab to the user, triggered when
* downloadFeature.onDownloadStopped gets invoked. It uses [DynamicDownloadDialogBehavior] to * downloadFeature.onDownloadStopped gets invoked. It uses [DynamicDownloadDialogBehavior] to
* hide when the users scrolls through a website as to not impede his activities. * hide when the users scrolls through a website as to not impede his activities.
* */ */
@Suppress("LongParameterList")
class DynamicDownloadDialog( class DynamicDownloadDialog(
private val container: ViewGroup, private val container: ViewGroup,
private val downloadState: DownloadState?, private val downloadState: DownloadState?,

View File

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

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

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) = fun SessionManager.sessionsOfType(private: Boolean) =
sessions.asSequence().filter { it.private == private } sessions.asSequence().filter { it.private == private }
/**
* @return the number of currently active sessions that are neither custom nor private
*/
fun SessionManager.normalSessionSize(): Int {
return this.sessions.filter { session ->
(!session.isCustomTabSession() && !session.private)
}.size
}

View File

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

View File

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

View File

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

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

View File

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

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

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

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 package org.mozilla.fenix.home.tips
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.button_tip_item.view.* import kotlinx.android.synthetic.main.button_tip_item.*
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.tips.Tip import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.components.tips.TipType import org.mozilla.fenix.components.tips.TipType
import org.mozilla.fenix.ext.addUnderline import org.mozilla.fenix.ext.addUnderline
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.utils.view.ViewHolder
class ButtonTipViewHolder( class ButtonTipViewHolder(
val view: View, view: View,
val interactor: SessionControlInteractor private val interactor: SessionControlInteractor,
) : RecyclerView.ViewHolder(view) { private val metrics: MetricController = view.context.components.analytics.metrics,
private val settings: Settings = view.context.components.settings
) : ViewHolder(view) {
var tip: Tip? = null var tip: Tip? = null
fun bind(tip: Tip) { fun bind(tip: Tip) {
@ -25,44 +34,39 @@ class ButtonTipViewHolder(
this.tip = tip this.tip = tip
view.apply { metrics.track(Event.TipDisplayed(tip.identifier))
context.components.analytics.metrics.track(Event.TipDisplayed(tip.identifier))
tip_header_text.text = tip.title tip_header_text.text = tip.title
tip_description_text.text = tip.description tip_description_text.text = tip.description
tip_button.text = tip.type.text tip_button.text = tip.type.text
if (tip.learnMoreURL == null) { tip_learn_more.isVisible = tip.learnMoreURL != null
tip_learn_more.visibility = View.GONE if (tip.learnMoreURL != null) {
} else { tip_learn_more.addUnderline()
tip_learn_more.addUnderline()
tip_learn_more.setOnClickListener { tip_learn_more.setOnClickListener {
(context as HomeActivity).openToBrowserAndLoad( (itemView.context as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = tip.learnMoreURL, searchTermOrURL = tip.learnMoreURL,
newTab = true, newTab = true,
from = BrowserDirection.FromHome from = BrowserDirection.FromHome
)
}
}
tip_button.setOnClickListener {
tip.type.action.invoke()
context.components.analytics.metrics.track(
Event.TipPressed(tip.identifier)
) )
} }
}
tip_close.setOnClickListener { tip_button.setOnClickListener {
context.components.analytics.metrics.track(Event.TipClosed(tip.identifier)) tip.type.action.invoke()
metrics.track(Event.TipPressed(tip.identifier))
}
context.settings().preferences tip_close.setOnClickListener {
.edit() metrics.track(Event.TipClosed(tip.identifier))
.putBoolean(tip.identifier, false)
.apply()
interactor.onCloseTip(tip) settings.preferences
} .edit()
.putBoolean(tip.identifier, false)
.apply()
interactor.onCloseTip(tip)
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 package org.mozilla.fenix.settings
import androidx.core.content.edit 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 * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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