For #16132 - Revise multiselect mode UI

upstream-sync
ekager 4 years ago
parent 1f6f29ea7d
commit a8db85fc22

@ -141,7 +141,7 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
private fun hasExistingAddonInstallationDialogFragment(): Boolean { private fun hasExistingAddonInstallationDialogFragment(): Boolean {
return parentFragmentManager.findFragmentByTag(INSTALLATION_DIALOG_FRAGMENT_TAG) return parentFragmentManager.findFragmentByTag(INSTALLATION_DIALOG_FRAGMENT_TAG)
as? AddonInstallationDialogFragment != null as? AddonInstallationDialogFragment != null
} }
private fun showPermissionDialog(addon: Addon) { private fun showPermissionDialog(addon: Addon) {

@ -0,0 +1,44 @@
/* 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.content.Context
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
class MultiselectSelectionMenu(
private val context: Context,
private val onItemTapped: (Item) -> Unit = {}
) {
sealed class Item {
object BookmarkTabs : Item()
object DeleteTabs : Item()
}
val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
private val menuItems by lazy {
listOf(
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_multiselect_menu_item_bookmark),
textColorResource = R.color.primary_text_normal_theme
) {
context.components.analytics.metrics.track(Event.TabsTraySaveToCollectionPressed)
onItemTapped.invoke(Item.BookmarkTabs)
},
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_multiselect_menu_item_close),
textColorResource = R.color.primary_text_normal_theme
) {
context.components.analytics.metrics.track(Event.TabsTrayShareAllTabsPressed)
onItemTapped.invoke(Item.DeleteTabs)
}
)
}
}

@ -4,13 +4,20 @@
package org.mozilla.fenix.tabtray package org.mozilla.fenix.tabtray
import androidx.annotation.VisibleForTesting
import androidx.navigation.NavController import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot
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.store.BrowserStore
import mozilla.components.concept.base.profiler.Profiler import mozilla.components.concept.base.profiler.Profiler
import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.concept.storage.BookmarksStorage
import mozilla.components.concept.tabstray.Tab import mozilla.components.concept.tabstray.Tab
import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.feature.tabs.TabsUseCases
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
@ -18,7 +25,6 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.home.HomeFragment import org.mozilla.fenix.home.HomeFragment
import mozilla.components.browser.storage.sync.Tab as SyncTab import mozilla.components.browser.storage.sync.Tab as SyncTab
@ -29,13 +35,16 @@ import mozilla.components.browser.storage.sync.Tab as SyncTab
*/ */
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
interface TabTrayController { interface TabTrayController {
fun onNewTabTapped(private: Boolean) fun handleNewTabTapped(private: Boolean)
fun onTabTrayDismissed() fun handleTabTrayDismissed()
fun handleTabSettingsClicked() fun handleTabSettingsClicked()
fun onShareTabsClicked(private: Boolean) fun handleShareTabsOfTypeClicked(private: Boolean)
fun onSyncedTabClicked(syncTab: SyncTab) fun handleShareSelectedTabsClicked(selectedTabs: Set<Tab>)
fun onSaveToCollectionClicked(selectedTabs: Set<Tab>) fun handleSyncedTabClicked(syncTab: SyncTab)
fun onCloseAllTabsClicked(private: Boolean) fun handleSaveToCollectionClicked(selectedTabs: Set<Tab>)
fun handleBookmarkSelectedTabs(selectedTabs: Set<Tab>)
fun handleDeleteSelectedTabs(selectedTabs: Set<Tab>)
fun handleCloseAllTabsClicked(private: Boolean)
fun handleBackPressed(): Boolean fun handleBackPressed(): Boolean
fun onModeRequested(): TabTrayDialogFragmentState.Mode fun onModeRequested(): TabTrayDialogFragmentState.Mode
fun handleAddSelectedTab(tab: Tab) fun handleAddSelectedTab(tab: Tab)
@ -68,8 +77,12 @@ class DefaultTabTrayController(
private val activity: HomeActivity, private val activity: HomeActivity,
private val profiler: Profiler?, private val profiler: Profiler?,
private val sessionManager: SessionManager, private val sessionManager: SessionManager,
private val browserStore: BrowserStore,
private val browsingModeManager: BrowsingModeManager, private val browsingModeManager: BrowsingModeManager,
private val tabCollectionStorage: TabCollectionStorage, private val tabCollectionStorage: TabCollectionStorage,
private val bookmarksStorage: BookmarksStorage,
private val scope: CoroutineScope,
private val tabsUseCases: TabsUseCases,
private val navController: NavController, private val navController: NavController,
private val dismissTabTray: () -> Unit, private val dismissTabTray: () -> Unit,
private val dismissTabTrayAndNavigateHome: (String) -> Unit, private val dismissTabTrayAndNavigateHome: (String) -> Unit,
@ -77,10 +90,12 @@ class DefaultTabTrayController(
private val tabTrayDialogFragmentStore: TabTrayDialogFragmentStore, private val tabTrayDialogFragmentStore: TabTrayDialogFragmentStore,
private val selectTabUseCase: TabsUseCases.SelectTabUseCase, private val selectTabUseCase: TabsUseCases.SelectTabUseCase,
private val showChooseCollectionDialog: (List<Session>) -> Unit, private val showChooseCollectionDialog: (List<Session>) -> Unit,
private val showAddNewCollectionDialog: (List<Session>) -> Unit private val showAddNewCollectionDialog: (List<Session>) -> Unit,
private val showUndoSnackbarForTabs: () -> Unit,
private val showBookmarksSnackbar: () -> Unit
) : TabTrayController { ) : TabTrayController {
override fun onNewTabTapped(private: Boolean) { override fun handleNewTabTapped(private: Boolean) {
val startTime = profiler?.getProfilerTime() val startTime = profiler?.getProfilerTime()
browsingModeManager.mode = BrowsingMode.fromBoolean(private) browsingModeManager.mode = BrowsingMode.fromBoolean(private)
navController.navigate(TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true)) navController.navigate(TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true))
@ -95,11 +110,11 @@ class DefaultTabTrayController(
navController.navigate(TabTrayDialogFragmentDirections.actionGlobalTabSettingsFragment()) navController.navigate(TabTrayDialogFragmentDirections.actionGlobalTabSettingsFragment())
} }
override fun onTabTrayDismissed() { override fun handleTabTrayDismissed() {
dismissTabTray() dismissTabTray()
} }
override fun onSaveToCollectionClicked(selectedTabs: Set<Tab>) { override fun handleSaveToCollectionClicked(selectedTabs: Set<Tab>) {
val sessionList = selectedTabs.map { val sessionList = selectedTabs.map {
sessionManager.findSessionById(it.id) ?: return sessionManager.findSessionById(it.id) ?: return
} }
@ -117,9 +132,19 @@ class DefaultTabTrayController(
} }
} }
override fun onShareTabsClicked(private: Boolean) { override fun handleShareTabsOfTypeClicked(private: Boolean) {
val tabs = getListOfSessions(private) val tabs = browserStore.state.getNormalOrPrivateTabs(private)
val data = tabs.map { val data = tabs.map {
ShareData(url = it.content.url, title = it.content.title)
}
val directions = TabTrayDialogFragmentDirections.actionGlobalShareFragment(
data = data.toTypedArray()
)
navController.navigate(directions)
}
override fun handleShareSelectedTabsClicked(selectedTabs: Set<Tab>) {
val data = selectedTabs.map {
ShareData(url = it.url, title = it.title) ShareData(url = it.url, title = it.title)
} }
val directions = TabTrayDialogFragmentDirections.actionGlobalShareFragment( val directions = TabTrayDialogFragmentDirections.actionGlobalShareFragment(
@ -128,7 +153,40 @@ class DefaultTabTrayController(
navController.navigate(directions) navController.navigate(directions)
} }
override fun onSyncedTabClicked(syncTab: SyncTab) { override fun handleBookmarkSelectedTabs(selectedTabs: Set<Tab>) {
selectedTabs.forEach {
scope.launch(IO) {
val shouldAddBookmark = bookmarksStorage.getBookmarksWithUrl(it.url)
.firstOrNull { it.url == it.url } == null
if (shouldAddBookmark) {
bookmarksStorage.addItem(
BookmarkRoot.Mobile.id,
url = it.url,
title = it.title,
position = null
)
}
}
}
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
showBookmarksSnackbar()
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun handleDeleteSelectedTabs(selectedTabs: Set<Tab>) {
if (browserStore.state.normalTabs.size == selectedTabs.size) {
dismissTabTrayAndNavigateHome(HomeFragment.ALL_NORMAL_TABS)
} else {
selectedTabs.map { it.id }.let {
tabsUseCases.removeTabs(it)
}
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
showUndoSnackbarForTabs()
}
}
override fun handleSyncedTabClicked(syncTab: SyncTab) {
activity.openToBrowserAndLoad( activity.openToBrowserAndLoad(
searchTermOrURL = syncTab.active().url, searchTermOrURL = syncTab.active().url,
newTab = true, newTab = true,
@ -137,7 +195,7 @@ class DefaultTabTrayController(
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override fun onCloseAllTabsClicked(private: Boolean) { override fun handleCloseAllTabsClicked(private: Boolean) {
val sessionsToClose = if (private) { val sessionsToClose = if (private) {
HomeFragment.ALL_PRIVATE_TABS HomeFragment.ALL_PRIVATE_TABS
} else { } else {
@ -164,11 +222,6 @@ class DefaultTabTrayController(
} }
} }
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
private fun getListOfSessions(private: Boolean): List<Session> {
return sessionManager.sessionsOfType(private = private).toList()
}
override fun onModeRequested(): TabTrayDialogFragmentState.Mode { override fun onModeRequested(): TabTrayDialogFragmentState.Mode {
return tabTrayDialogFragmentStore.state.mode return tabTrayDialogFragmentStore.state.mode
} }

@ -30,6 +30,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.TabSessionState
@ -71,7 +72,8 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
private val snackbarAnchor: View? private val snackbarAnchor: View?
get() = if (tabTrayView.fabView.new_tab_button.isVisible || get() = if (tabTrayView.fabView.new_tab_button.isVisible ||
tabTrayView.mode != Mode.Normal) tabTrayView.fabView.new_tab_button tabTrayView.mode != Mode.Normal
) tabTrayView.fabView.new_tab_button
/* During selection of the tabs to the collection, the FAB is not visible, /* During selection of the tabs to the collection, the FAB is not visible,
which leads to not attaching a needed AnchorView. That's why, we're not only checking, if it's not visible, which leads to not attaching a needed AnchorView. That's why, we're not only checking, if it's not visible,
but also if we're not in a "Normal" mode, so after selecting tabs for a collection, we're pushing snackbar but also if we're not in a "Normal" mode, so after selecting tabs for a collection, we're pushing snackbar
@ -177,6 +179,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@Suppress("LongMethod")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val activity = activity as HomeActivity val activity = activity as HomeActivity
@ -194,8 +197,12 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
activity = activity, activity = activity,
profiler = activity.components.core.engine.profiler, profiler = activity.components.core.engine.profiler,
sessionManager = activity.components.core.sessionManager, sessionManager = activity.components.core.sessionManager,
browserStore = activity.components.core.store,
tabsUseCases = activity.components.useCases.tabsUseCases,
scope = lifecycleScope,
browsingModeManager = activity.browsingModeManager, browsingModeManager = activity.browsingModeManager,
tabCollectionStorage = activity.components.core.tabCollectionStorage, tabCollectionStorage = activity.components.core.tabCollectionStorage,
bookmarksStorage = activity.components.core.bookmarksStorage,
navController = findNavController(), navController = findNavController(),
dismissTabTray = ::dismissAllowingStateLoss, dismissTabTray = ::dismissAllowingStateLoss,
dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome, dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome,
@ -203,7 +210,9 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
tabTrayDialogFragmentStore = tabTrayDialogStore, tabTrayDialogFragmentStore = tabTrayDialogStore,
selectTabUseCase = selectTabUseCase, selectTabUseCase = selectTabUseCase,
showChooseCollectionDialog = ::showChooseCollectionDialog, showChooseCollectionDialog = ::showChooseCollectionDialog,
showAddNewCollectionDialog = ::showAddNewCollectionDialog showAddNewCollectionDialog = ::showAddNewCollectionDialog,
showUndoSnackbarForTabs = ::showUndoSnackbarForTabs,
showBookmarksSnackbar = ::showBookmarksSnackbar
) )
), ),
store = tabTrayDialogStore, store = tabTrayDialogStore,
@ -267,6 +276,20 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
} }
} }
private fun showUndoSnackbarForTabs() {
lifecycleScope.allowUndo(
requireView().tabLayout,
getString(R.string.snackbar_message_tabs_closed),
getString(R.string.snackbar_deleted_undo),
{
requireComponents.useCases.tabsUseCases.undo.invoke()
},
operation = { },
elevation = ELEVATION,
anchorView = snackbarAnchor
)
}
private fun showUndoSnackbarForTab(sessionId: String) { private fun showUndoSnackbarForTab(sessionId: String) {
val store = requireComponents.core.store val store = requireComponents.core.store
val tab = requireComponents.core.store.state.findTab(sessionId) ?: return val tab = requireComponents.core.store.state.findTab(sessionId) ?: return
@ -358,6 +381,26 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
} }
} }
private fun showBookmarksSnackbar() {
val snackbar = FenixSnackbar
.make(
duration = FenixSnackbar.LENGTH_LONG,
isDisplayedWithBrowserToolbar = false,
view = (view as View)
)
.setAnchorView(snackbarAnchor)
.setText(requireContext().getString(R.string.snackbar_message_bookmarks_saved))
.setAction(requireContext().getString(R.string.snackbar_message_bookmarks_view)) {
dismissAllowingStateLoss()
findNavController().navigate(
TabTrayDialogFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id)
)
}
snackbar.view.elevation = ELEVATION
snackbar.show()
}
override fun onBackPressed(): Boolean { override fun onBackPressed(): Boolean {
if (!tabTrayView.onBackPressed()) { if (!tabTrayView.onBackPressed()) {
dismiss() dismiss()

@ -22,7 +22,22 @@ interface TabTrayInteractor {
/** /**
* Called when user clicks the share tabs button. * Called when user clicks the share tabs button.
*/ */
fun onShareTabsClicked(private: Boolean) fun onShareTabsOfTypeClicked(private: Boolean)
/**
* Called when user clicks button to share selected tabs in multiselect.
*/
fun onShareSelectedTabsClicked(selectedTabs: Set<Tab>)
/**
* Called when user clicks bookmark button in menu to bookmark selected tabs in multiselect.
*/
fun onBookmarkSelectedTabs(selectedTabs: Set<Tab>)
/**
* Called when user clicks delete button in menu to delete selected tabs in multiselect.
*/
fun onDeleteSelectedTabs(selectedTabs: Set<Tab>)
/** /**
* Called when user clicks the tab settings button. * Called when user clicks the tab settings button.
@ -91,11 +106,11 @@ interface TabTrayInteractor {
@Suppress("TooManyFunctions") @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.handleNewTabTapped(private)
} }
override fun onTabTrayDismissed() { override fun onTabTrayDismissed() {
controller.onTabTrayDismissed() controller.handleTabTrayDismissed()
} }
override fun onTabSettingsClicked() { override fun onTabSettingsClicked() {
@ -106,20 +121,32 @@ class TabTrayFragmentInteractor(private val controller: TabTrayController) : Tab
controller.handleRecentlyClosedClicked() controller.handleRecentlyClosedClicked()
} }
override fun onShareTabsClicked(private: Boolean) { override fun onShareTabsOfTypeClicked(private: Boolean) {
controller.onShareTabsClicked(private) controller.handleShareTabsOfTypeClicked(private)
}
override fun onShareSelectedTabsClicked(selectedTabs: Set<Tab>) {
controller.handleShareSelectedTabsClicked(selectedTabs)
}
override fun onBookmarkSelectedTabs(selectedTabs: Set<Tab>) {
controller.handleBookmarkSelectedTabs(selectedTabs)
}
override fun onDeleteSelectedTabs(selectedTabs: Set<Tab>) {
controller.handleDeleteSelectedTabs(selectedTabs)
} }
override fun onSaveToCollectionClicked(selectedTabs: Set<Tab>) { override fun onSaveToCollectionClicked(selectedTabs: Set<Tab>) {
controller.onSaveToCollectionClicked(selectedTabs) controller.handleSaveToCollectionClicked(selectedTabs)
} }
override fun onCloseAllTabsClicked(private: Boolean) { override fun onCloseAllTabsClicked(private: Boolean) {
controller.onCloseAllTabsClicked(private) controller.handleCloseAllTabsClicked(private)
} }
override fun onSyncedTabClicked(syncTab: SyncTab) { override fun onSyncedTabClicked(syncTab: SyncTab) {
controller.onSyncedTabClicked(syncTab) controller.handleSyncedTabClicked(syncTab)
} }
override fun onBackPressed(): Boolean { override fun onBackPressed(): Boolean {

@ -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.content.Context
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
class TabTrayItemMenu(
private val context: Context,
private val shouldShowSaveToCollection: () -> Boolean,
private val hasOpenTabs: () -> Boolean,
private val onItemTapped: (Item) -> Unit = {}
) {
sealed class Item {
object ShareAllTabs : Item()
object OpenTabSettings : Item()
object SaveToCollection : Item()
object CloseAllTabs : Item()
object OpenRecentlyClosed : Item()
}
val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
private val menuItems by lazy {
listOf(
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_item_save),
textColorResource = R.color.primary_text_normal_theme
) {
context.components.analytics.metrics.track(Event.TabsTraySaveToCollectionPressed)
onItemTapped.invoke(Item.SaveToCollection)
}.apply { visible = shouldShowSaveToCollection },
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_item_share),
textColorResource = R.color.primary_text_normal_theme
) {
context.components.analytics.metrics.track(Event.TabsTrayShareAllTabsPressed)
onItemTapped.invoke(Item.ShareAllTabs)
}.apply { visible = hasOpenTabs },
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_tab_settings),
textColorResource = R.color.primary_text_normal_theme
) {
onItemTapped.invoke(Item.OpenTabSettings)
},
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_recently_closed),
textColorResource = R.color.primary_text_normal_theme
) {
onItemTapped.invoke(Item.OpenRecentlyClosed)
},
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_item_close),
textColorResource = R.color.primary_text_normal_theme
) {
context.components.analytics.metrics.track(Event.TabsTrayCloseAllTabsPressed)
onItemTapped.invoke(Item.CloseAllTabs)
}.apply { visible = hasOpenTabs }
)
}
}

@ -30,12 +30,11 @@ import kotlinx.android.extensions.LayoutContainer
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.*
import kotlinx.android.synthetic.main.tabstray_multiselect_items.view.*
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.menu.BrowserMenu import mozilla.components.browser.menu.BrowserMenu
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs 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
@ -90,6 +89,9 @@ class TabTrayView(
private val tabTrayItemMenu: TabTrayItemMenu private val tabTrayItemMenu: TabTrayItemMenu
private var menu: BrowserMenu? = null private var menu: BrowserMenu? = null
private val multiselectSelectionMenu: MultiselectSelectionMenu
private var multiselectMenu: BrowserMenu? = null
private var tabsTouchHelper: TabsTouchHelper private var tabsTouchHelper: TabsTouchHelper
private val collectionsButtonAdapter = SaveToCollectionsButtonAdapter(interactor, isPrivate) private val collectionsButtonAdapter = SaveToCollectionsButtonAdapter(interactor, isPrivate)
@ -230,7 +232,7 @@ class TabTrayView(
hasOpenTabs = checkOpenTabs hasOpenTabs = checkOpenTabs
) { ) {
when (it) { when (it) {
is TabTrayItemMenu.Item.ShareAllTabs -> interactor.onShareTabsClicked( is TabTrayItemMenu.Item.ShareAllTabs -> interactor.onShareTabsOfTypeClicked(
isPrivateModeSelected isPrivateModeSelected
) )
is TabTrayItemMenu.Item.OpenTabSettings -> interactor.onTabSettingsClicked() is TabTrayItemMenu.Item.OpenTabSettings -> interactor.onTabSettingsClicked()
@ -242,18 +244,30 @@ class TabTrayView(
} }
} }
multiselectSelectionMenu = MultiselectSelectionMenu(
context = view.context
) {
when (it) {
is MultiselectSelectionMenu.Item.BookmarkTabs -> interactor.onBookmarkSelectedTabs(
mode.selectedItems
)
is MultiselectSelectionMenu.Item.DeleteTabs -> interactor.onDeleteSelectedTabs(
mode.selectedItems
)
}
}
view.tab_tray_overflow.setOnClickListener { view.tab_tray_overflow.setOnClickListener {
components.analytics.metrics.track(Event.TabsTrayMenuOpened) components.analytics.metrics.track(Event.TabsTrayMenuOpened)
menu = tabTrayItemMenu.menuBuilder.build(container.context) menu = tabTrayItemMenu.menuBuilder.build(container.context)
menu?.show(it) menu?.show(it)?.also { popupMenu ->
?.also { pu -> (popupMenu.contentView as? CardView)?.setCardBackgroundColor(
(pu.contentView as? CardView)?.setCardBackgroundColor( ContextCompat.getColor(
ContextCompat.getColor( view.context,
view.context, R.color.foundation_normal_theme
R.color.foundation_normal_theme
)
) )
} )
}
} }
adjustNewTabButtonsForNormalMode() adjustNewTabButtonsForNormalMode()
@ -469,6 +483,8 @@ class TabTrayView(
fabView.new_tab_button.isVisible = false fabView.new_tab_button.isVisible = false
view.tab_tray_new_tab.isVisible = false view.tab_tray_new_tab.isVisible = false
view.collect_multi_select.isVisible = state.mode.selectedItems.isNotEmpty() view.collect_multi_select.isVisible = state.mode.selectedItems.isNotEmpty()
view.share_multi_select.isVisible = state.mode.selectedItems.isNotEmpty()
view.menu_multi_select.isVisible = state.mode.selectedItems.isNotEmpty()
view.multiselect_title.text = view.context.getString( view.multiselect_title.text = view.context.getString(
R.string.tab_tray_multi_select_title, R.string.tab_tray_multi_select_title,
@ -477,6 +493,20 @@ class TabTrayView(
view.collect_multi_select.setOnClickListener { view.collect_multi_select.setOnClickListener {
interactor.onSaveToCollectionClicked(state.mode.selectedItems) interactor.onSaveToCollectionClicked(state.mode.selectedItems)
} }
view.share_multi_select.setOnClickListener {
interactor.onShareSelectedTabsClicked(state.mode.selectedItems)
}
view.menu_multi_select.setOnClickListener {
multiselectMenu = multiselectSelectionMenu.menuBuilder.build(container.context)
multiselectMenu?.show(it)?.also { popupMenu ->
(popupMenu.contentView as? CardView)?.setCardBackgroundColor(
ContextCompat.getColor(
view.context,
R.color.foundation_normal_theme
)
)
}
}
view.exit_multi_select.setOnClickListener { view.exit_multi_select.setOnClickListener {
interactor.onBackPressed() interactor.onBackPressed()
} }
@ -544,6 +574,8 @@ class TabTrayView(
private fun toggleUIMultiselect(multiselect: Boolean) { private fun toggleUIMultiselect(multiselect: Boolean) {
view.multiselect_title.isVisible = multiselect view.multiselect_title.isVisible = multiselect
view.collect_multi_select.isVisible = multiselect view.collect_multi_select.isVisible = multiselect
view.share_multi_select.isVisible = multiselect
view.menu_multi_select.isVisible = multiselect
view.exit_multi_select.isVisible = multiselect view.exit_multi_select.isVisible = multiselect
view.topBar.setBackgroundColor( view.topBar.setBackgroundColor(
@ -707,9 +739,7 @@ class TabTrayView(
// We offset the tab index by the number of items in the other adapters. // We offset the tab index by the number of items in the other adapters.
// We add the offset, because the layoutManager is initialized with `reverseLayout`. // We add the offset, because the layoutManager is initialized with `reverseLayout`.
return if (view.context.settings().listTabView) { return if (view.context.settings().listTabView) {
selectedBrowserTabIndex + selectedBrowserTabIndex + collectionsButtonAdapter.itemCount + syncedTabsController.adapter.itemCount
collectionsButtonAdapter.itemCount +
syncedTabsController.adapter.itemCount
} else { } else {
selectedBrowserTabIndex selectedBrowserTabIndex
} }
@ -719,75 +749,18 @@ class TabTrayView(
private const val TAB_COUNT_SHOW_CFR = 6 private const val TAB_COUNT_SHOW_CFR = 6
private const val DEFAULT_TAB_ID = 0 private const val DEFAULT_TAB_ID = 0
private const val PRIVATE_TAB_ID = 1 private const val PRIVATE_TAB_ID = 1
// Minimum number of list items for which to show the tabs tray as expanded. // Minimum number of list items for which to show the tabs tray as expanded.
private const val EXPAND_AT_LIST_SIZE = 4 private const val EXPAND_AT_LIST_SIZE = 4
// Minimum number of grid items for which to show the tabs tray as expanded. // Minimum number of grid items for which to show the tabs tray as expanded.
private const val EXPAND_AT_GRID_SIZE = 3 private const val EXPAND_AT_GRID_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 NORMAL_HANDLE_PERCENT_WIDTH = 0.1F private const val NORMAL_HANDLE_PERCENT_WIDTH = 0.1F
private const val COLUMN_WIDTH_DP = 180 private const val COLUMN_WIDTH_DP = 180
// The remaining padding offset needed to provide a 16dp column spacing between the grid items. // The remaining padding offset needed to provide a 16dp column spacing between the grid items.
const val GRID_ITEM_PARENT_PADDING = 8 const val GRID_ITEM_PARENT_PADDING = 8
} }
} }
class TabTrayItemMenu(
private val context: Context,
private val shouldShowSaveToCollection: () -> Boolean,
private val hasOpenTabs: () -> Boolean,
private val onItemTapped: (Item) -> Unit = {}
) {
sealed class Item {
object ShareAllTabs : Item()
object OpenTabSettings : Item()
object SaveToCollection : Item()
object CloseAllTabs : Item()
object OpenRecentlyClosed : Item()
}
val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
private val menuItems by lazy {
listOf(
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_item_save),
textColorResource = R.color.primary_text_normal_theme
) {
context.components.analytics.metrics.track(Event.TabsTraySaveToCollectionPressed)
onItemTapped.invoke(Item.SaveToCollection)
}.apply { visible = shouldShowSaveToCollection },
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_item_share),
textColorResource = R.color.primary_text_normal_theme
) {
context.components.analytics.metrics.track(Event.TabsTrayShareAllTabsPressed)
onItemTapped.invoke(Item.ShareAllTabs)
}.apply { visible = hasOpenTabs },
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_tab_settings),
textColorResource = R.color.primary_text_normal_theme
) {
onItemTapped.invoke(Item.OpenTabSettings)
},
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_recently_closed),
textColorResource = R.color.primary_text_normal_theme
) {
onItemTapped.invoke(Item.OpenRecentlyClosed)
},
SimpleBrowserMenuItem(
context.getString(R.string.tab_tray_menu_item_close),
textColorResource = R.color.primary_text_normal_theme
) {
context.components.analytics.metrics.track(Event.TabsTrayCloseAllTabsPressed)
onItemTapped.invoke(Item.CloseAllTabs)
}.apply { visible = hasOpenTabs }
)
}
}

@ -23,14 +23,13 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.1" /> app:layout_constraintWidth_percent="0.1" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/infoBanner" android:id="@+id/infoBanner"
android:visibility="gone"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/foundation_normal_theme" android:background="@color/foundation_normal_theme"
app:layout_constraintTop_toBottomOf="@+id/topBar"/> android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/topBar" />
<TextView <TextView
android:id="@+id/tab_tray_empty_view" android:id="@+id/tab_tray_empty_view"
@ -76,7 +75,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="12dp" android:layout_marginStart="12dp"
android:textColor="@color/contrast_text_normal_theme" android:textColor="@color/contrast_text_normal_theme"
android:textSize="18sp" android:textSize="20sp"
app:fontFamily="@font/metropolis_semibold"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/collect_multi_select" app:layout_constraintEnd_toStartOf="@id/collect_multi_select"
@ -86,34 +86,12 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="3 selected" /> tools:text="3 selected" />
<TextView <include layout="@layout/tabstray_multiselect_items" />
android:id="@+id/collect_multi_select"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_collection_button_multiselect_content_description"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:text="@string/tab_tray_save_to_collection"
android:textAllCaps="true"
android:textColor="@color/contrast_text_normal_theme"
android:textSize="14sp"
android:textStyle="bold"
android:visibility="gone"
app:drawableStartCompat="@drawable/ic_tab_collection"
app:drawableTint="@color/contrast_text_normal_theme"
app:fontFamily="@font/metropolis_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout" android:id="@+id/tab_layout"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="80dp" android:layout_height="80dp"
app:tabMaxWidth="0dp"
android:background="@color/foundation_normal_theme" android:background="@color/foundation_normal_theme"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
@ -121,6 +99,7 @@
app:tabGravity="fill" app:tabGravity="fill"
app:tabIconTint="@color/tab_icon" app:tabIconTint="@color/tab_icon"
app:tabIndicatorColor="@color/accent_normal_theme" app:tabIndicatorColor="@color/accent_normal_theme"
app:tabMaxWidth="0dp"
app:tabRippleColor="@android:color/transparent"> app:tabRippleColor="@android:color/transparent">
<com.google.android.material.tabs.TabItem <com.google.android.material.tabs.TabItem
@ -150,8 +129,8 @@
app:layout_constraintBottom_toBottomOf="@id/tab_layout" app:layout_constraintBottom_toBottomOf="@id/tab_layout"
app:layout_constraintEnd_toStartOf="@id/tab_tray_overflow" app:layout_constraintEnd_toStartOf="@id/tab_tray_overflow"
app:layout_constraintTop_toTopOf="@id/tab_layout" app:layout_constraintTop_toTopOf="@id/tab_layout"
app:tint="@color/primary_text_normal_theme" app:srcCompat="@drawable/ic_new"
app:srcCompat="@drawable/ic_new" /> app:tint="@color/primary_text_normal_theme" />
<ImageButton <ImageButton
android:id="@+id/tab_tray_overflow" android:id="@+id/tab_tray_overflow"
@ -161,11 +140,11 @@
android:background="?android:attr/selectableItemBackgroundBorderless" android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/open_tabs_menu" android:contentDescription="@string/open_tabs_menu"
android:visibility="visible" android:visibility="visible"
app:tint="@color/tab_tray_heading_icon_menu_normal_theme"
app:layout_constraintBottom_toBottomOf="@id/tab_layout" app:layout_constraintBottom_toBottomOf="@id/tab_layout"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tab_layout" app:layout_constraintTop_toTopOf="@id/tab_layout"
app:srcCompat="@drawable/ic_menu" /> app:srcCompat="@drawable/ic_menu"
app:tint="@color/tab_tray_heading_icon_menu_normal_theme" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<View <View

@ -2,10 +2,7 @@
<!-- 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/. -->
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android" style="@style/NeutralButton"
xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_margin="8dp"
style="@style/NeutralButton" android:text="@string/tabs_tray_select_tabs" />
android:layout_margin="8dp"
android:text="@string/save_to_collection"
app:icon="@drawable/ic_tab_collection" />

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<ImageButton
android:id="@+id/collect_multi_select"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_collection_button_multiselect_content_description"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/share_multi_select"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_tab_collection"
app:tint="@color/contrast_text_normal_theme" />
<ImageButton
android:id="@+id/share_multi_select"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_multiselect_share_content_description"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/menu_multi_select"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_hollow_share"
app:tint="@color/contrast_text_normal_theme" />
<ImageButton
android:id="@+id/menu_multi_select"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_multiselect_menu_content_description"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_menu"
app:tint="@color/contrast_text_normal_theme" />
</merge>

@ -584,8 +584,18 @@
<string name="tab_tray_menu_home">Go home</string> <string name="tab_tray_menu_home">Go home</string>
<!-- Shortcut action to toggle private mode --> <!-- Shortcut action to toggle private mode -->
<string name="tab_tray_menu_toggle">Toggle tab mode</string> <string name="tab_tray_menu_toggle">Toggle tab mode</string>
<!-- Text shown in the multiselect menu for bookmarking selected tabs. -->
<string name="tab_tray_multiselect_menu_item_bookmark">Bookmark</string>
<!-- Text shown in the multiselect menu for closing selected tabs. -->
<string name="tab_tray_multiselect_menu_item_close">Close</string>
<!-- Content description for tabs tray multiselect share button -->
<string name="tab_tray_multiselect_share_content_description">Share selected tabs</string>
<!-- Content description for tabs tray multiselect menu -->
<string name="tab_tray_multiselect_menu_content_description">Selected tabs menu</string>
<!-- Content description (not visible, for screen readers etc.): Removes tab from collection button. Removes the selected tab from collection when pressed --> <!-- Content description (not visible, for screen readers etc.): Removes tab from collection button. Removes the selected tab from collection when pressed -->
<string name="remove_tab_from_collection">Remove tab from collection</string> <string name="remove_tab_from_collection">Remove tab from collection</string>
<!-- Text for button to enter multiselect mode in tabs tray -->
<string name="tabs_tray_select_tabs">Select tabs</string>
<!-- Content description (not visible, for screen readers etc.): Close tab button. Closes the current session when pressed --> <!-- Content description (not visible, for screen readers etc.): Close tab button. Closes the current session when pressed -->
<string name="close_tab">Close tab</string> <string name="close_tab">Close tab</string>
<!-- Content description (not visible, for screen readers etc.): Close tab <title> button. First parameter is tab title --> <!-- Content description (not visible, for screen readers etc.): Close tab <title> button. First parameter is tab title -->
@ -950,6 +960,12 @@
<string name="snackbar_tab_closed">Tab closed</string> <string name="snackbar_tab_closed">Tab closed</string>
<!-- Text shown in snackbar when user closes all tabs --> <!-- Text shown in snackbar when user closes all tabs -->
<string name="snackbar_tabs_closed">Tabs closed</string> <string name="snackbar_tabs_closed">Tabs closed</string>
<!-- Text shown in snackbar when user closes tabs -->
<string name="snackbar_message_tabs_closed">Tabs closed!</string>
<!-- Text shown in snackbar when user bookmarks a list of tabs -->
<string name="snackbar_message_bookmarks_saved">Bookmarks saved!</string>
<!-- Text shown in snackbar action for viewing bookmarks -->
<string name="snackbar_message_bookmarks_view">View</string>
<!-- Text shown in snackbar when user adds a site to top sites --> <!-- Text shown in snackbar when user adds a site to top sites -->
<string name="snackbar_added_to_top_sites">Added to top sites!</string> <string name="snackbar_added_to_top_sites">Added to top sites!</string>
<!-- Text shown in snackbar when user closes a private tab --> <!-- Text shown in snackbar when user closes a private tab -->

@ -8,6 +8,7 @@ import androidx.navigation.NavController
import androidx.navigation.NavDestination import androidx.navigation.NavDestination
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import io.mockk.Runs import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
@ -16,9 +17,14 @@ import io.mockk.slot
import io.mockk.verify import io.mockk.verify
import io.mockk.verifyOrder import io.mockk.verifyOrder
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScope
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.state.BrowserState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.base.profiler.Profiler import mozilla.components.concept.base.profiler.Profiler
import mozilla.components.concept.storage.BookmarksStorage
import mozilla.components.concept.tabstray.Tab import mozilla.components.concept.tabstray.Tab
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
@ -40,6 +46,14 @@ class DefaultTabTrayControllerTest {
private val profiler: Profiler? = mockk(relaxed = true) private val profiler: Profiler? = mockk(relaxed = true)
private val navController: NavController = mockk() private val navController: NavController = mockk()
private val sessionManager: SessionManager = mockk(relaxed = true) private val sessionManager: SessionManager = mockk(relaxed = true)
var store = BrowserStore(
BrowserState(
tabs = listOf(
createTab(url = "http://firefox.com", id = "5678"),
createTab(url = "http://mozilla.org", id = "1234")
), selectedTabId = "1234"
)
)
private val browsingModeManager: BrowsingModeManager = mockk(relaxed = true) private val browsingModeManager: BrowsingModeManager = mockk(relaxed = true)
private val dismissTabTray: (() -> Unit) = mockk(relaxed = true) private val dismissTabTray: (() -> Unit) = mockk(relaxed = true)
private val dismissTabTrayAndNavigateHome: ((String) -> Unit) = mockk(relaxed = true) private val dismissTabTrayAndNavigateHome: ((String) -> Unit) = mockk(relaxed = true)
@ -47,11 +61,15 @@ class DefaultTabTrayControllerTest {
private val showChooseCollectionDialog: ((List<Session>) -> Unit) = mockk(relaxed = true) private val showChooseCollectionDialog: ((List<Session>) -> Unit) = mockk(relaxed = true)
private val showAddNewCollectionDialog: ((List<Session>) -> Unit) = mockk(relaxed = true) private val showAddNewCollectionDialog: ((List<Session>) -> Unit) = mockk(relaxed = true)
private val tabCollectionStorage: TabCollectionStorage = mockk(relaxed = true) private val tabCollectionStorage: TabCollectionStorage = mockk(relaxed = true)
private val bookmarksStorage: BookmarksStorage = mockk(relaxed = true)
private val tabCollection: TabCollection = mockk() private val tabCollection: TabCollection = mockk()
private val cachedTabCollections: List<TabCollection> = listOf(tabCollection) private val cachedTabCollections: List<TabCollection> = listOf(tabCollection)
private val currentDestination: NavDestination = mockk(relaxed = true) private val currentDestination: NavDestination = mockk(relaxed = true)
private val tabTrayFragmentStore: TabTrayDialogFragmentStore = mockk(relaxed = true) private val tabTrayFragmentStore: TabTrayDialogFragmentStore = mockk(relaxed = true)
private val selectTabUseCase: TabsUseCases.SelectTabUseCase = mockk(relaxed = true) private val selectTabUseCase: TabsUseCases.SelectTabUseCase = mockk(relaxed = true)
private val tabsUseCases: TabsUseCases = mockk(relaxed = true)
private val showUndoSnackbarForTabs: (() -> Unit) = mockk(relaxed = true)
private val showBookmarksSavedSnackbar: (() -> Unit) = mockk(relaxed = true)
private lateinit var controller: DefaultTabTrayController private lateinit var controller: DefaultTabTrayController
@ -87,16 +105,22 @@ class DefaultTabTrayControllerTest {
activity = activity, activity = activity,
profiler = profiler, profiler = profiler,
sessionManager = sessionManager, sessionManager = sessionManager,
browserStore = store,
browsingModeManager = browsingModeManager, browsingModeManager = browsingModeManager,
tabCollectionStorage = tabCollectionStorage, tabCollectionStorage = tabCollectionStorage,
bookmarksStorage = bookmarksStorage,
scope = TestCoroutineScope(),
navController = navController, navController = navController,
tabsUseCases = tabsUseCases,
dismissTabTray = dismissTabTray, dismissTabTray = dismissTabTray,
dismissTabTrayAndNavigateHome = dismissTabTrayAndNavigateHome, dismissTabTrayAndNavigateHome = dismissTabTrayAndNavigateHome,
registerCollectionStorageObserver = registerCollectionStorageObserver, registerCollectionStorageObserver = registerCollectionStorageObserver,
tabTrayDialogFragmentStore = tabTrayFragmentStore, tabTrayDialogFragmentStore = tabTrayFragmentStore,
selectTabUseCase = selectTabUseCase, selectTabUseCase = selectTabUseCase,
showChooseCollectionDialog = showChooseCollectionDialog, showChooseCollectionDialog = showChooseCollectionDialog,
showAddNewCollectionDialog = showAddNewCollectionDialog showAddNewCollectionDialog = showAddNewCollectionDialog,
showUndoSnackbarForTabs = showUndoSnackbarForTabs,
showBookmarksSnackbar = showBookmarksSavedSnackbar
) )
} }
@ -113,7 +137,7 @@ class DefaultTabTrayControllerTest {
@Test @Test
fun onNewTabTapped() { fun onNewTabTapped() {
controller.onNewTabTapped(private = false) controller.handleNewTabTapped(private = false)
verifyOrder { verifyOrder {
browsingModeManager.mode = BrowsingMode.fromBoolean(false) browsingModeManager.mode = BrowsingMode.fromBoolean(false)
@ -125,7 +149,7 @@ class DefaultTabTrayControllerTest {
dismissTabTray() dismissTabTray()
} }
controller.onNewTabTapped(private = true) controller.handleNewTabTapped(private = true)
verifyOrder { verifyOrder {
browsingModeManager.mode = BrowsingMode.fromBoolean(true) browsingModeManager.mode = BrowsingMode.fromBoolean(true)
@ -140,7 +164,7 @@ class DefaultTabTrayControllerTest {
@Test @Test
fun onTabTrayDismissed() { fun onTabTrayDismissed() {
controller.onTabTrayDismissed() controller.handleTabTrayDismissed()
verify { verify {
dismissTabTray() dismissTabTray()
@ -152,7 +176,7 @@ class DefaultTabTrayControllerTest {
val navDirectionsSlot = slot<NavDirections>() val navDirectionsSlot = slot<NavDirections>()
every { navController.navigate(capture(navDirectionsSlot)) } just Runs every { navController.navigate(capture(navDirectionsSlot)) } just Runs
controller.onShareTabsClicked(private = false) controller.handleShareTabsOfTypeClicked(private = false)
verify { verify {
navController.navigate(capture(navDirectionsSlot)) navController.navigate(capture(navDirectionsSlot))
@ -164,7 +188,7 @@ class DefaultTabTrayControllerTest {
@Test @Test
fun onCloseAllTabsClicked() { fun onCloseAllTabsClicked() {
controller.onCloseAllTabsClicked(private = false) controller.handleCloseAllTabsClicked(private = false)
verify { verify {
dismissTabTrayAndNavigateHome(any()) dismissTabTrayAndNavigateHome(any())
@ -173,7 +197,7 @@ class DefaultTabTrayControllerTest {
@Test @Test
fun onSyncedTabClicked() { fun onSyncedTabClicked() {
controller.onSyncedTabClicked(mockk(relaxed = true)) controller.handleSyncedTabClicked(mockk(relaxed = true))
verify { verify {
activity.openToBrowserAndLoad(any(), true, BrowserDirection.FromTabTray) activity.openToBrowserAndLoad(any(), true, BrowserDirection.FromTabTray)
@ -242,13 +266,53 @@ class DefaultTabTrayControllerTest {
fun onSaveToCollectionClicked() { fun onSaveToCollectionClicked() {
val tab = Tab("1234", "mozilla.org") val tab = Tab("1234", "mozilla.org")
controller.onSaveToCollectionClicked(setOf(tab)) controller.handleSaveToCollectionClicked(setOf(tab))
verify { verify {
registerCollectionStorageObserver() registerCollectionStorageObserver()
showChooseCollectionDialog(listOf(session)) showChooseCollectionDialog(listOf(session))
} }
} }
@Test
fun handleShareSelectedTabs() {
val tab = Tab("1234", "mozilla.org")
val navDirectionsSlot = slot<NavDirections>()
every { navController.navigate(capture(navDirectionsSlot)) } just Runs
controller.handleShareSelectedTabsClicked(setOf(tab))
verify {
navController.navigate(capture(navDirectionsSlot))
}
assertTrue(navDirectionsSlot.isCaptured)
assertEquals(R.id.action_global_shareFragment, navDirectionsSlot.captured.actionId)
}
@Test
fun handleDeleteSelectedTabs() {
val tab = Tab("1234", "mozilla.org")
controller.handleDeleteSelectedTabs(setOf(tab))
verify {
tabsUseCases.removeTabs(listOf(tab.id))
tabTrayFragmentStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
showUndoSnackbarForTabs()
}
}
@Test
fun handleBookmarkSelectedTabs() {
val tab = Tab("1234", "mozilla.org")
coEvery { bookmarksStorage.getBookmarksWithUrl("mozilla.org") } returns listOf()
controller.handleBookmarkSelectedTabs(setOf(tab))
verify {
tabTrayFragmentStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
showBookmarksSavedSnackbar()
}
}
@Test @Test
fun handleSetUpAutoCloseTabsClicked() { fun handleSetUpAutoCloseTabsClicked() {
controller.handleSetUpAutoCloseTabsClicked() controller.handleSetUpAutoCloseTabsClicked()

@ -13,13 +13,40 @@ class TabTrayFragmentInteractorTest {
private val controller = mockk<TabTrayController>(relaxed = true) private val controller = mockk<TabTrayController>(relaxed = true)
private val interactor = TabTrayFragmentInteractor(controller) private val interactor = TabTrayFragmentInteractor(controller)
@Test
fun onShareSelectedTabsClicked() {
val tab = Tab("1234", "mozilla.org")
val tab2 = Tab("5678", "pocket.com")
val selectedTabs = setOf(tab, tab2)
interactor.onShareSelectedTabsClicked(selectedTabs)
verify { controller.handleShareSelectedTabsClicked(selectedTabs) }
}
@Test
fun onBookmarkSelectedTabs() {
val tab = Tab("1234", "mozilla.org")
val tab2 = Tab("5678", "pocket.com")
val selectedTabs = setOf(tab, tab2)
interactor.onBookmarkSelectedTabs(selectedTabs)
verify { controller.handleBookmarkSelectedTabs(selectedTabs) }
}
@Test
fun onDeleteSelectedTabs() {
val tab = Tab("1234", "mozilla.org")
val tab2 = Tab("5678", "pocket.com")
val selectedTabs = setOf(tab, tab2)
interactor.onDeleteSelectedTabs(selectedTabs)
verify { controller.handleDeleteSelectedTabs(selectedTabs) }
}
@Test @Test
fun onNewTabTapped() { fun onNewTabTapped() {
interactor.onNewTabTapped(private = true) interactor.onNewTabTapped(private = true)
verify { controller.onNewTabTapped(true) } verify { controller.handleNewTabTapped(true) }
interactor.onNewTabTapped(private = false) interactor.onNewTabTapped(private = false)
verify { controller.onNewTabTapped(false) } verify { controller.handleNewTabTapped(false) }
} }
@Test @Test
@ -34,38 +61,38 @@ class TabTrayFragmentInteractorTest {
@Test @Test
fun onTabTrayDismissed() { fun onTabTrayDismissed() {
interactor.onTabTrayDismissed() interactor.onTabTrayDismissed()
verify { controller.onTabTrayDismissed() } verify { controller.handleTabTrayDismissed() }
} }
@Test @Test
fun onShareTabsClicked() { fun onShareTabsClicked() {
interactor.onShareTabsClicked(private = true) interactor.onShareTabsOfTypeClicked(private = true)
verify { controller.onShareTabsClicked(true) } verify { controller.handleShareTabsOfTypeClicked(true) }
interactor.onShareTabsClicked(private = false) interactor.onShareTabsOfTypeClicked(private = false)
verify { controller.onShareTabsClicked(false) } verify { controller.handleShareTabsOfTypeClicked(false) }
} }
@Test @Test
fun onSaveToCollectionClicked() { fun onSaveToCollectionClicked() {
val tab = Tab("1234", "mozilla.org") val tab = Tab("1234", "mozilla.org")
interactor.onSaveToCollectionClicked(setOf(tab)) interactor.onSaveToCollectionClicked(setOf(tab))
verify { controller.onSaveToCollectionClicked(setOf(tab)) } verify { controller.handleSaveToCollectionClicked(setOf(tab)) }
} }
@Test @Test
fun onCloseAllTabsClicked() { fun onCloseAllTabsClicked() {
interactor.onCloseAllTabsClicked(private = false) interactor.onCloseAllTabsClicked(private = false)
verify { controller.onCloseAllTabsClicked(false) } verify { controller.handleCloseAllTabsClicked(false) }
interactor.onCloseAllTabsClicked(private = true) interactor.onCloseAllTabsClicked(private = true)
verify { controller.onCloseAllTabsClicked(true) } verify { controller.handleCloseAllTabsClicked(true) }
} }
@Test @Test
fun onSyncedTabClicked() { fun onSyncedTabClicked() {
interactor.onSyncedTabClicked(mockk(relaxed = true)) interactor.onSyncedTabClicked(mockk(relaxed = true))
verify { controller.onSyncedTabClicked(any()) } verify { controller.handleSyncedTabClicked(any()) }
} }
@Test @Test

Loading…
Cancel
Save