For #10163 - Adds tab multiselect mode
parent
6c0be8db1d
commit
46511d6f8e
|
@ -107,7 +107,6 @@ import org.mozilla.fenix.ext.requireComponents
|
|||
import org.mozilla.fenix.ext.sessionsOfType
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.home.SharedViewModel
|
||||
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
|
||||
import org.mozilla.fenix.theme.ThemeManager
|
||||
import org.mozilla.fenix.utils.allowUndo
|
||||
import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration
|
||||
|
@ -231,7 +230,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
|
|||
tabCollectionStorage = requireComponents.core.tabCollectionStorage,
|
||||
topSiteStorage = requireComponents.core.topSiteStorage,
|
||||
onTabCounterClicked = {
|
||||
TabTrayDialogFragment.show(parentFragmentManager)
|
||||
findNavController().nav(
|
||||
R.id.browserFragment,
|
||||
BrowserFragmentDirections.actionGlobalTabTrayDialogFragment()
|
||||
)
|
||||
},
|
||||
onCloseTab = {
|
||||
val snapshot = sessionManager.createSessionSnapshot(it)
|
||||
|
|
|
@ -15,6 +15,8 @@ import mozilla.components.feature.tab.collections.TabCollection
|
|||
import org.mozilla.fenix.components.TabCollectionStorage
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.components.metrics.MetricController
|
||||
import org.mozilla.fenix.ext.getDefaultCollectionNumber
|
||||
import org.mozilla.fenix.ext.normalSessionSize
|
||||
import org.mozilla.fenix.home.Tab
|
||||
|
||||
interface CollectionCreationController {
|
||||
|
@ -92,7 +94,7 @@ class DefaultCollectionCreationController(
|
|||
}
|
||||
|
||||
metrics.track(
|
||||
Event.CollectionSaved(normalSessionSize(sessionManager), sessionBundle.size)
|
||||
Event.CollectionSaved(sessionManager.normalSessionSize(), sessionBundle.size)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -134,7 +136,7 @@ class DefaultCollectionCreationController(
|
|||
}
|
||||
|
||||
metrics.track(
|
||||
Event.CollectionTabsAdded(normalSessionSize(sessionManager), sessionBundle.size)
|
||||
Event.CollectionTabsAdded(sessionManager.normalSessionSize(), sessionBundle.size)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -146,7 +148,7 @@ class DefaultCollectionCreationController(
|
|||
} else {
|
||||
SaveCollectionStep.SelectCollection
|
||||
},
|
||||
defaultCollectionNumber = getDefaultCollectionNumber()
|
||||
defaultCollectionNumber = store.state.tabCollections.getDefaultCollectionNumber()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -155,26 +157,11 @@ class DefaultCollectionCreationController(
|
|||
store.dispatch(
|
||||
CollectionCreationAction.StepChanged(
|
||||
SaveCollectionStep.NameCollection,
|
||||
getDefaultCollectionNumber()
|
||||
store.state.tabCollections.getDefaultCollectionNumber()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the new default name recommendation for a collection
|
||||
*
|
||||
* Algorithm: Go through all collections, make a list of their names and keep only the default ones.
|
||||
* Then get the numbers from all these default names, compute the maximum number and add one.
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
fun getDefaultCollectionNumber(): Int {
|
||||
return (store.state.tabCollections
|
||||
.map { it.title }
|
||||
.filter { it.matches(Regex("Collection\\s\\d+")) }
|
||||
.map { Integer.valueOf(it.split(" ")[DEFAULT_COLLECTION_NUMBER_POSITION]) }
|
||||
.max() ?: 0) + DEFAULT_INCREMENT_VALUE
|
||||
}
|
||||
|
||||
override fun addTabToSelection(tab: Tab) {
|
||||
store.dispatch(CollectionCreationAction.TabAdded(tab))
|
||||
}
|
||||
|
@ -209,14 +196,4 @@ class DefaultCollectionCreationController(
|
|||
SaveCollectionStep.SelectTabs, SaveCollectionStep.RenameCollection -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the number of currently active sessions that are neither custom nor private
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
fun normalSessionSize(sessionManager: SessionManager): Int {
|
||||
return sessionManager.sessions.filter { session ->
|
||||
(!session.isCustomTabSession() && !session.private)
|
||||
}.size
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,3 +11,12 @@ import mozilla.components.browser.session.SessionManager
|
|||
*/
|
||||
fun SessionManager.sessionsOfType(private: Boolean) =
|
||||
sessions.asSequence().filter { it.private == private }
|
||||
|
||||
/**
|
||||
* @return the number of currently active sessions that are neither custom nor private
|
||||
*/
|
||||
fun SessionManager.normalSessionSize(): Int {
|
||||
return this.sessions.filter { session ->
|
||||
(!session.isCustomTabSession() && !session.private)
|
||||
}.size
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import androidx.annotation.ColorInt
|
|||
import androidx.core.content.ContextCompat
|
||||
import mozilla.components.feature.tab.collections.TabCollection
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.collections.DefaultCollectionCreationController
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
|
@ -22,3 +23,17 @@ fun TabCollection.getIconColor(context: Context): Int {
|
|||
iconColors.recycle()
|
||||
return color
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the new default name recommendation for a collection
|
||||
*
|
||||
* Algorithm: Go through all collections, make a list of their names and keep only the default ones.
|
||||
* Then get the numbers from all these default names, compute the maximum number and add one.
|
||||
*/
|
||||
fun List<TabCollection>.getDefaultCollectionNumber(): Int {
|
||||
return (this
|
||||
.map { it.title }
|
||||
.filter { it.matches(Regex("Collection\\s\\d+")) }
|
||||
.map { Integer.valueOf(it.split(" ")[DefaultCollectionCreationController.DEFAULT_COLLECTION_NUMBER_POSITION]) }
|
||||
.max() ?: 0) + DefaultCollectionCreationController.DEFAULT_INCREMENT_VALUE
|
||||
}
|
||||
|
|
|
@ -101,7 +101,6 @@ import org.mozilla.fenix.onboarding.FenixOnboarding
|
|||
import org.mozilla.fenix.settings.SupportUtils
|
||||
import org.mozilla.fenix.settings.SupportUtils.SumoTopic.HELP
|
||||
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
|
||||
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
|
||||
import org.mozilla.fenix.theme.ThemeManager
|
||||
import org.mozilla.fenix.utils.FragmentPreDrawManager
|
||||
import org.mozilla.fenix.utils.allowUndo
|
||||
|
@ -920,7 +919,10 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
|
||||
private fun openTabTray() {
|
||||
TabTrayDialogFragment.show(parentFragmentManager)
|
||||
findNavController().nav(
|
||||
R.id.homeFragment,
|
||||
HomeFragmentDirections.actionGlobalTabTrayDialogFragment()
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateTabCounter(browserState: BrowserState) {
|
||||
|
|
|
@ -192,8 +192,12 @@ class DefaultSessionControlController(
|
|||
metrics.track(Event.CollectionTabRemoved)
|
||||
|
||||
if (collection.tabs.size == 1) {
|
||||
val title = activity.resources.getString(R.string.delete_tab_and_collection_dialog_title, collection.title)
|
||||
val message = activity.resources.getString(R.string.delete_tab_and_collection_dialog_message)
|
||||
val title = activity.resources.getString(
|
||||
R.string.delete_tab_and_collection_dialog_title,
|
||||
collection.title
|
||||
)
|
||||
val message =
|
||||
activity.resources.getString(R.string.delete_tab_and_collection_dialog_message)
|
||||
showDeleteCollectionPrompt(collection, title, message)
|
||||
} else {
|
||||
viewLifecycleScope.launch(Dispatchers.IO) {
|
||||
|
@ -208,7 +212,8 @@ class DefaultSessionControlController(
|
|||
}
|
||||
|
||||
override fun handleDeleteCollectionTapped(collection: TabCollection) {
|
||||
val message = activity.resources.getString(R.string.tab_collection_dialog_message, collection.title)
|
||||
val message =
|
||||
activity.resources.getString(R.string.tab_collection_dialog_message, collection.title)
|
||||
showDeleteCollectionPrompt(collection, null, message)
|
||||
}
|
||||
|
||||
|
@ -254,8 +259,12 @@ class DefaultSessionControlController(
|
|||
|
||||
override fun handleSelectTopSite(url: String, isDefault: Boolean) {
|
||||
metrics.track(Event.TopSiteOpenInNewTab)
|
||||
if (isDefault) { metrics.track(Event.TopSiteOpenDefault) }
|
||||
if (url == SupportUtils.POCKET_TRENDING_URL) { metrics.track(Event.PocketTopSiteClicked) }
|
||||
if (isDefault) {
|
||||
metrics.track(Event.TopSiteOpenDefault)
|
||||
}
|
||||
if (url == SupportUtils.POCKET_TRENDING_URL) {
|
||||
metrics.track(Event.PocketTopSiteClicked)
|
||||
}
|
||||
addTabUseCase.invoke(
|
||||
url = url,
|
||||
selectTab = true,
|
||||
|
@ -297,6 +306,13 @@ class DefaultSessionControlController(
|
|||
fragmentStore.dispatch(HomeFragmentAction.RemoveTip(tip))
|
||||
}
|
||||
|
||||
private fun showTabTrayCollectionCreation() {
|
||||
val directions = HomeFragmentDirections.actionGlobalTabTrayDialogFragment(
|
||||
enterMultiselect = true
|
||||
)
|
||||
navController.nav(R.id.homeFragment, directions)
|
||||
}
|
||||
|
||||
private fun showCollectionCreationFragment(
|
||||
step: SaveCollectionStep,
|
||||
selectedTabIds: Array<String>? = null,
|
||||
|
@ -322,7 +338,7 @@ class DefaultSessionControlController(
|
|||
}
|
||||
|
||||
override fun handleCreateCollection() {
|
||||
showCollectionCreationFragment(step = SaveCollectionStep.SelectTabs)
|
||||
showTabTrayCollectionCreation()
|
||||
}
|
||||
|
||||
private fun showShareFragment(data: List<ShareData>) {
|
||||
|
|
|
@ -51,7 +51,6 @@ import org.mozilla.fenix.ext.nav
|
|||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.ext.toShortUrl
|
||||
import org.mozilla.fenix.library.LibraryPageFragment
|
||||
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
|
||||
import org.mozilla.fenix.utils.allowUndo
|
||||
|
||||
/**
|
||||
|
@ -240,7 +239,7 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
|
|||
|
||||
private fun showTabTray() {
|
||||
invokePendingDeletion()
|
||||
TabTrayDialogFragment.show(parentFragmentManager)
|
||||
navigate(BookmarkFragmentDirections.actionGlobalTabTrayDialogFragment())
|
||||
}
|
||||
|
||||
private fun navigate(directions: NavDirections) {
|
||||
|
|
|
@ -44,7 +44,6 @@ import org.mozilla.fenix.ext.requireComponents
|
|||
import org.mozilla.fenix.ext.showToolbar
|
||||
import org.mozilla.fenix.ext.toShortUrl
|
||||
import org.mozilla.fenix.library.LibraryPageFragment
|
||||
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
|
||||
import org.mozilla.fenix.utils.allowUndo
|
||||
|
||||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||
|
@ -207,7 +206,10 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
|||
|
||||
private fun showTabTray() {
|
||||
invokePendingDeletion()
|
||||
TabTrayDialogFragment.show(parentFragmentManager)
|
||||
findNavController().nav(
|
||||
R.id.historyFragment,
|
||||
HistoryFragmentDirections.actionGlobalTabTrayDialogFragment()
|
||||
)
|
||||
}
|
||||
|
||||
private fun getMultiSelectSnackBarMessage(historyItems: Set<HistoryItem>): String {
|
||||
|
@ -259,7 +261,10 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
|||
launch(Main) {
|
||||
viewModel.invalidate()
|
||||
historyStore.dispatch(HistoryFragmentAction.ExitDeletionMode)
|
||||
showSnackBar(requireView(), getString(R.string.preferences_delete_browsing_data_snackbar))
|
||||
showSnackBar(
|
||||
requireView(),
|
||||
getString(R.string.preferences_delete_browsing_data_snackbar)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.tabtray
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckedTextView
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import mozilla.components.support.ktx.android.util.dpToPx
|
||||
import org.mozilla.fenix.R
|
||||
|
||||
internal class CollectionsAdapter(
|
||||
private val collections: Array<String>,
|
||||
private val onNewCollectionClicked: () -> Unit
|
||||
) : RecyclerView.Adapter<CollectionsAdapter.CollectionItemViewHolder>() {
|
||||
|
||||
@VisibleForTesting
|
||||
internal var checkedPosition = 1
|
||||
|
||||
class CollectionItemViewHolder(val textView: CheckedTextView) :
|
||||
RecyclerView.ViewHolder(textView)
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): CollectionItemViewHolder {
|
||||
val textView = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.collection_dialog_list_item, parent, false) as CheckedTextView
|
||||
return CollectionItemViewHolder(textView)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: CollectionItemViewHolder, position: Int) {
|
||||
if (position == 0) {
|
||||
val displayMetrics = holder.textView.context.resources.displayMetrics
|
||||
holder.textView.setPadding(NEW_COLLECTION_PADDING_START.dpToPx(displayMetrics), 0, 0, 0)
|
||||
holder.textView.compoundDrawablePadding =
|
||||
NEW_COLLECTION_DRAWABLE_PADDING.dpToPx(displayMetrics)
|
||||
holder.textView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
ContextCompat.getDrawable(
|
||||
holder.textView.context,
|
||||
R.drawable.ic_new
|
||||
), null, null, null
|
||||
)
|
||||
} else {
|
||||
holder.textView.isChecked = checkedPosition == position
|
||||
}
|
||||
|
||||
holder.textView.setOnClickListener {
|
||||
if (position == 0) {
|
||||
onNewCollectionClicked()
|
||||
} else if (checkedPosition != position) {
|
||||
notifyItemChanged(position)
|
||||
notifyItemChanged(checkedPosition)
|
||||
checkedPosition = position
|
||||
}
|
||||
}
|
||||
holder.textView.text = collections[position]
|
||||
}
|
||||
|
||||
override fun getItemCount() = collections.size
|
||||
|
||||
fun getSelectedCollection() = checkedPosition - 1
|
||||
|
||||
companion object {
|
||||
private const val NEW_COLLECTION_PADDING_START = 24
|
||||
private const val NEW_COLLECTION_DRAWABLE_PADDING = 28
|
||||
}
|
||||
}
|
|
@ -6,14 +6,19 @@ package org.mozilla.fenix.tabtray
|
|||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.tab_tray_item.view.*
|
||||
import mozilla.components.browser.tabstray.TabViewHolder
|
||||
import mozilla.components.browser.tabstray.TabsAdapter
|
||||
import mozilla.components.concept.tabstray.Tabs
|
||||
import mozilla.components.support.images.loader.ImageLoader
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.metrics
|
||||
|
||||
class FenixTabsAdapter(
|
||||
context: Context,
|
||||
private val context: Context,
|
||||
imageLoader: ImageLoader
|
||||
) : TabsAdapter(
|
||||
viewHolderProvider = { parentView ->
|
||||
|
@ -21,11 +26,19 @@ class FenixTabsAdapter(
|
|||
LayoutInflater.from(context).inflate(
|
||||
R.layout.tab_tray_item,
|
||||
parentView,
|
||||
false),
|
||||
false
|
||||
),
|
||||
imageLoader
|
||||
)
|
||||
}
|
||||
) {
|
||||
var tabTrayInteractor: TabTrayInteractor? = null
|
||||
|
||||
private val mode: TabTrayDialogFragmentState.Mode?
|
||||
get() = tabTrayInteractor?.onModeRequested()
|
||||
|
||||
val selectedItems get() = mode?.selectedItems ?: setOf()
|
||||
|
||||
var onTabsUpdated: (() -> Unit)? = null
|
||||
var tabCount = 0
|
||||
|
||||
|
@ -35,9 +48,53 @@ class FenixTabsAdapter(
|
|||
tabCount = tabs.list.size
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: TabViewHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
if (payloads.isNullOrEmpty()) {
|
||||
onBindViewHolder(holder, position)
|
||||
return
|
||||
}
|
||||
// Otherwise, item needs to be checked or unchecked
|
||||
val shouldBeChecked =
|
||||
mode is TabTrayDialogFragmentState.Mode.MultiSelect && selectedItems.contains(holder.tab)
|
||||
holder.itemView.checkmark.isVisible = shouldBeChecked
|
||||
holder.itemView.selected_mask.isVisible = shouldBeChecked
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
|
||||
super.onBindViewHolder(holder, position)
|
||||
val newIndex = tabCount - position - 1
|
||||
(holder as TabTrayViewHolder).updateAccessibilityRowIndex(holder.itemView, newIndex)
|
||||
|
||||
holder.tab?.let { tab ->
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,10 +9,11 @@ import androidx.navigation.NavController
|
|||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import mozilla.components.browser.session.Session
|
||||
import mozilla.components.concept.engine.prompt.ShareData
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||
import org.mozilla.fenix.collections.SaveCollectionStep
|
||||
import org.mozilla.fenix.components.TabCollectionStorage
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.sessionsOfType
|
||||
import org.mozilla.fenix.home.HomeFragment
|
||||
|
@ -22,61 +23,81 @@ import org.mozilla.fenix.home.HomeFragment
|
|||
*
|
||||
* Delegated by View Interactors, handles container business logic and operates changes on it.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface TabTrayController {
|
||||
fun onNewTabTapped(private: Boolean)
|
||||
fun onTabTrayDismissed()
|
||||
fun onShareTabsClicked(private: Boolean)
|
||||
fun onSaveToCollectionClicked()
|
||||
fun onSaveToCollectionClicked(selectedTabs: Set<Tab>)
|
||||
fun onCloseAllTabsClicked(private: Boolean)
|
||||
fun handleBackPressed(): Boolean
|
||||
fun onModeRequested(): TabTrayDialogFragmentState.Mode
|
||||
fun handleAddSelectedTab(tab: Tab)
|
||||
fun handleRemoveSelectedTab(tab: Tab)
|
||||
fun handleOpenTab(tab: Tab)
|
||||
fun handleEnterMultiselect()
|
||||
}
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
/**
|
||||
* 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", "LongParameterList")
|
||||
class DefaultTabTrayController(
|
||||
private val activity: HomeActivity,
|
||||
private val navController: NavController,
|
||||
private val dismissTabTray: () -> 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 {
|
||||
private val tabCollectionStorage = activity.components.core.tabCollectionStorage
|
||||
|
||||
override fun onNewTabTapped(private: Boolean) {
|
||||
val startTime = activity.components.core.engine.profiler?.getProfilerTime()
|
||||
activity.browsingModeManager.mode = BrowsingMode.fromBoolean(private)
|
||||
navController.navigate(TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true))
|
||||
dismissTabTray()
|
||||
activity.components.core.engine.profiler?.addMarker("DefaultTabTrayController.onNewTabTapped", startTime)
|
||||
activity.components.core.engine.profiler?.addMarker(
|
||||
"DefaultTabTrayController.onNewTabTapped",
|
||||
startTime
|
||||
)
|
||||
}
|
||||
|
||||
override fun onTabTrayDismissed() {
|
||||
dismissTabTray()
|
||||
}
|
||||
|
||||
override fun onSaveToCollectionClicked() {
|
||||
val tabs = getListOfSessions(false)
|
||||
val tabIds = tabs.map { it.id }.toList().toTypedArray()
|
||||
val tabCollectionStorage = activity.components.core.tabCollectionStorage
|
||||
|
||||
val step = when {
|
||||
// Show the SelectTabs fragment if there are multiple opened tabs to select which tabs
|
||||
// you want to save to a collection.
|
||||
tabs.size > 1 -> SaveCollectionStep.SelectTabs
|
||||
// If there is an existing tab collection, show the SelectCollection fragment to save
|
||||
// the selected tab to a collection of your choice.
|
||||
tabCollectionStorage.cachedTabCollections.isNotEmpty() -> SaveCollectionStep.SelectCollection
|
||||
// Show the NameCollection fragment to create a new collection for the selected tab.
|
||||
else -> SaveCollectionStep.NameCollection
|
||||
override fun onSaveToCollectionClicked(selectedTabs: Set<Tab>) {
|
||||
val sessionList = selectedTabs.map {
|
||||
activity.components.core.sessionManager.findSessionById(it.id) ?: return
|
||||
}
|
||||
|
||||
if (navController.currentDestination?.id == R.id.collectionCreationFragment) return
|
||||
|
||||
// Only register the observer right before moving to collection creation
|
||||
registerCollectionStorageObserver()
|
||||
|
||||
val directions = TabTrayDialogFragmentDirections.actionGlobalCollectionCreationFragment(
|
||||
tabIds = tabIds,
|
||||
saveCollectionStep = step,
|
||||
selectedTabIds = tabIds
|
||||
)
|
||||
navController.navigate(directions)
|
||||
when {
|
||||
tabCollectionStorage.cachedTabCollections.isNotEmpty() -> {
|
||||
showChooseCollectionDialog(sessionList)
|
||||
}
|
||||
else -> {
|
||||
showAddNewCollectionDialog(sessionList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShareTabsClicked(private: Boolean) {
|
||||
|
@ -101,8 +122,37 @@ class DefaultTabTrayController(
|
|||
dismissTabTrayAndNavigateHome(sessionsToClose)
|
||||
}
|
||||
|
||||
override fun handleAddSelectedTab(tab: Tab) {
|
||||
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.AddItemForCollection(tab))
|
||||
}
|
||||
|
||||
override fun handleRemoveSelectedTab(tab: Tab) {
|
||||
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.RemoveItemForCollection(tab))
|
||||
}
|
||||
|
||||
override fun handleBackPressed(): Boolean {
|
||||
return if (tabTrayDialogFragmentStore.state.mode is TabTrayDialogFragmentState.Mode.MultiSelect) {
|
||||
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
private fun getListOfSessions(private: Boolean): List<Session> {
|
||||
return activity.components.core.sessionManager.sessionsOfType(private = private).toList()
|
||||
}
|
||||
|
||||
override fun onModeRequested(): TabTrayDialogFragmentState.Mode {
|
||||
return tabTrayDialogFragmentStore.state.mode
|
||||
}
|
||||
|
||||
override fun handleOpenTab(tab: Tab) {
|
||||
selectTabUseCase.invoke(tab.id)
|
||||
}
|
||||
|
||||
override fun handleEnterMultiselect() {
|
||||
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.EnterMultiSelectMode)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,23 +4,31 @@
|
|||
|
||||
package org.mozilla.fenix.tabtray
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.widget.EditText
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.component_tabstray.view.*
|
||||
import kotlinx.android.synthetic.main.component_tabstray_fab.view.*
|
||||
import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.*
|
||||
import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.view.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.browser.session.Session
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.browser.thumbnails.loader.ThumbnailLoader
|
||||
|
@ -28,24 +36,34 @@ import mozilla.components.feature.tab.collections.TabCollection
|
|||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
import mozilla.components.feature.tabs.tabstray.TabsFeature
|
||||
import mozilla.components.lib.state.ext.consumeFrom
|
||||
import mozilla.components.support.base.feature.UserInteractionHandler
|
||||
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
|
||||
import mozilla.components.support.ktx.android.view.showKeyboard
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.browser.BrowserFragmentDirections
|
||||
import org.mozilla.fenix.components.FenixSnackbar
|
||||
import org.mozilla.fenix.components.StoreProvider
|
||||
import org.mozilla.fenix.components.TabCollectionStorage
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.getDefaultCollectionNumber
|
||||
import org.mozilla.fenix.ext.metrics
|
||||
import org.mozilla.fenix.ext.normalSessionSize
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentState.Mode
|
||||
import org.mozilla.fenix.utils.allowUndo
|
||||
|
||||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||
class TabTrayDialogFragment : AppCompatDialogFragment() {
|
||||
class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
|
||||
private val args by navArgs<TabTrayDialogFragmentArgs>()
|
||||
|
||||
private val tabsFeature = ViewBoundFeatureWrapper<TabsFeature>()
|
||||
private var _tabTrayView: TabTrayView? = null
|
||||
private val tabTrayView: TabTrayView
|
||||
get() = _tabTrayView!!
|
||||
private lateinit var tabTrayDialogStore: TabTrayDialogFragmentStore
|
||||
|
||||
private val snackbarAnchor: View?
|
||||
get() = if (tabTrayView.fabView.new_tab_button.isVisible) tabTrayView.fabView.new_tab_button
|
||||
|
@ -75,6 +93,14 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return object : Dialog(requireContext(), this.theme) {
|
||||
override fun onBackPressed() {
|
||||
this@TabTrayDialogFragment.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val removeTabUseCase = object : TabsUseCases.RemoveTabUseCase {
|
||||
override fun invoke(sessionId: String) {
|
||||
requireContext().components.analytics.metrics.track(Event.ClosedExistingTab)
|
||||
|
@ -109,7 +135,18 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
|
|||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? = inflater.inflate(R.layout.fragment_tab_tray_dialog, container, false)
|
||||
): View? {
|
||||
tabTrayDialogStore = StoreProvider.get(this) {
|
||||
TabTrayDialogFragmentStore(
|
||||
TabTrayDialogFragmentState(
|
||||
requireComponents.core.store.state,
|
||||
if (args.enterMultiselect) Mode.MultiSelect(setOf()) else Mode.Normal
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return inflater.inflate(R.layout.fragment_tab_tray_dialog, container, false)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
|
@ -140,7 +177,11 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
|
|||
navController = findNavController(),
|
||||
dismissTabTray = ::dismissAllowingStateLoss,
|
||||
dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome,
|
||||
registerCollectionStorageObserver = ::registerCollectionStorageObserver
|
||||
registerCollectionStorageObserver = ::registerCollectionStorageObserver,
|
||||
tabTrayDialogFragmentStore = tabTrayDialogStore,
|
||||
selectTabUseCase = selectTabUseCase,
|
||||
showChooseCollectionDialog = ::showChooseCollectionDialog,
|
||||
showAddNewCollectionDialog = ::showAddNewCollectionDialog
|
||||
)
|
||||
),
|
||||
isPrivate = isPrivate,
|
||||
|
@ -188,6 +229,10 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
|
|||
}
|
||||
|
||||
consumeFrom(requireComponents.core.store) {
|
||||
tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.BrowserStateChanged(it))
|
||||
}
|
||||
|
||||
consumeFrom(tabTrayDialogStore) {
|
||||
tabTrayView.updateState(it)
|
||||
}
|
||||
}
|
||||
|
@ -209,7 +254,8 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
|
|||
} ?: return
|
||||
|
||||
// Check if this is the last tab of this session type
|
||||
val isLastOpenTab = sessionManager.sessions.filter { snapshot.session.private == it.private }.size == 1
|
||||
val isLastOpenTab =
|
||||
sessionManager.sessions.filter { snapshot.session.private == it.private }.size == 1
|
||||
|
||||
if (isLastOpenTab) {
|
||||
dismissTabTrayAndNavigateHome(sessionId)
|
||||
|
@ -295,21 +341,101 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ELEVATION = 80f
|
||||
private const val FRAGMENT_TAG = "tabTrayDialogFragment"
|
||||
override fun onBackPressed(): Boolean {
|
||||
if (!tabTrayView.onBackPressed()) {
|
||||
dismiss()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun show(fragmentManager: FragmentManager) {
|
||||
// If we've killed the fragmentManager. Let's not try to show the tabs tray.
|
||||
if (fragmentManager.isDestroyed) {
|
||||
return
|
||||
}
|
||||
private fun showChooseCollectionDialog(sessionList: List<Session>) {
|
||||
context?.let {
|
||||
val tabCollectionStorage = it.components.core.tabCollectionStorage
|
||||
val collections =
|
||||
tabCollectionStorage.cachedTabCollections.map { it.title }.toTypedArray()
|
||||
val customLayout =
|
||||
LayoutInflater.from(it).inflate(R.layout.add_new_collection_dialog, null)
|
||||
val list = customLayout.findViewById<RecyclerView>(R.id.recycler_view)
|
||||
list.layoutManager = LinearLayoutManager(it)
|
||||
|
||||
// We want to make sure we don't accidentally show the dialog twice if
|
||||
// a user somehow manages to trigger `show()` twice before we present the dialog.
|
||||
if (fragmentManager.findFragmentByTag(FRAGMENT_TAG) == null) {
|
||||
TabTrayDialogFragment().showNow(fragmentManager, FRAGMENT_TAG)
|
||||
}
|
||||
val builder = AlertDialog.Builder(it).setTitle(R.string.tab_tray_select_collection)
|
||||
.setView(customLayout)
|
||||
.setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
val selectedCollection =
|
||||
(list.adapter as CollectionsAdapter).getSelectedCollection()
|
||||
val collection = tabCollectionStorage.cachedTabCollections[selectedCollection]
|
||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||
tabCollectionStorage.addTabsToCollection(collection, sessionList)
|
||||
it.metrics.track(
|
||||
Event.CollectionTabsAdded(
|
||||
it.components.core.sessionManager.normalSessionSize(),
|
||||
sessionList.size
|
||||
)
|
||||
)
|
||||
launch(Main) {
|
||||
tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}.setNegativeButton(android.R.string.cancel) { dialog, _ ->
|
||||
tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
|
||||
dialog.cancel()
|
||||
}
|
||||
|
||||
val dialog = builder.create()
|
||||
val adapter =
|
||||
CollectionsAdapter(arrayOf(it.getString(R.string.tab_tray_add_new_collection)) + collections) {
|
||||
dialog.dismiss()
|
||||
showAddNewCollectionDialog(sessionList)
|
||||
}
|
||||
list.adapter = adapter
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAddNewCollectionDialog(sessionList: List<Session>) {
|
||||
context?.let {
|
||||
val tabCollectionStorage = it.components.core.tabCollectionStorage
|
||||
val customLayout =
|
||||
LayoutInflater.from(it).inflate(R.layout.name_collection_dialog, null)
|
||||
val collectionNameEditText: EditText =
|
||||
customLayout.findViewById(R.id.collection_name)
|
||||
collectionNameEditText.setText(
|
||||
it.getString(
|
||||
R.string.create_collection_default_name,
|
||||
tabCollectionStorage.cachedTabCollections.getDefaultCollectionNumber()
|
||||
)
|
||||
)
|
||||
|
||||
AlertDialog.Builder(it).setTitle(R.string.tab_tray_add_new_collection)
|
||||
.setView(customLayout).setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||
tabCollectionStorage.createCollection(
|
||||
collectionNameEditText.text.toString(),
|
||||
sessionList
|
||||
)
|
||||
it.metrics.track(
|
||||
Event.CollectionSaved(
|
||||
it.components.core.sessionManager.normalSessionSize(),
|
||||
sessionList.size
|
||||
)
|
||||
)
|
||||
launch(Main) {
|
||||
tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}.setNegativeButton(android.R.string.cancel) { dialog, _ ->
|
||||
tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
|
||||
dialog.cancel()
|
||||
}.create().show().also {
|
||||
collectionNameEditText.setSelection(0, collectionNameEditText.text.length)
|
||||
collectionNameEditText.showKeyboard()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ELEVATION = 80f
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.tabtray
|
||||
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import mozilla.components.lib.state.Action
|
||||
import mozilla.components.lib.state.State
|
||||
import mozilla.components.lib.state.Store
|
||||
|
||||
/**
|
||||
* The [Store] for holding the [TabTrayDialogFragmentState] and
|
||||
* applying [TabTrayDialogFragmentAction]s.
|
||||
*/
|
||||
class TabTrayDialogFragmentStore(initialState: TabTrayDialogFragmentState) :
|
||||
Store<TabTrayDialogFragmentState, TabTrayDialogFragmentAction>(
|
||||
initialState,
|
||||
::tabTrayStateReducer
|
||||
)
|
||||
|
||||
/**
|
||||
* Actions to dispatch through the `TabTrayDialogFragmentStore` to modify
|
||||
* `TabTrayDialogFragmentState` through the reducer.
|
||||
*/
|
||||
sealed class TabTrayDialogFragmentAction : Action {
|
||||
data class BrowserStateChanged(val browserState: BrowserState) : TabTrayDialogFragmentAction()
|
||||
object EnterMultiSelectMode : TabTrayDialogFragmentAction()
|
||||
object ExitMultiSelectMode : TabTrayDialogFragmentAction()
|
||||
data class AddItemForCollection(val item: Tab) : TabTrayDialogFragmentAction()
|
||||
data class RemoveItemForCollection(val item: Tab) : TabTrayDialogFragmentAction()
|
||||
}
|
||||
|
||||
/**
|
||||
* The state for the Tab Tray Dialog Screen
|
||||
* @property mode Current Mode of Multiselection
|
||||
*/
|
||||
data class TabTrayDialogFragmentState(val browserState: BrowserState, val mode: Mode) : State {
|
||||
sealed class Mode {
|
||||
open val selectedItems = emptySet<Tab>()
|
||||
|
||||
object Normal : Mode()
|
||||
data class MultiSelect(override val selectedItems: Set<Tab>) : Mode()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The TabTrayDialogFragmentState Reducer.
|
||||
*/
|
||||
private fun tabTrayStateReducer(
|
||||
state: TabTrayDialogFragmentState,
|
||||
action: TabTrayDialogFragmentAction
|
||||
): TabTrayDialogFragmentState {
|
||||
return when (action) {
|
||||
is TabTrayDialogFragmentAction.BrowserStateChanged -> state.copy(browserState = action.browserState)
|
||||
is TabTrayDialogFragmentAction.AddItemForCollection ->
|
||||
state.copy(mode = TabTrayDialogFragmentState.Mode.MultiSelect(state.mode.selectedItems + action.item))
|
||||
is TabTrayDialogFragmentAction.RemoveItemForCollection -> {
|
||||
val selected = state.mode.selectedItems - action.item
|
||||
state.copy(
|
||||
mode = if (selected.isEmpty()) {
|
||||
TabTrayDialogFragmentState.Mode.Normal
|
||||
} else {
|
||||
TabTrayDialogFragmentState.Mode.MultiSelect(selected)
|
||||
}
|
||||
)
|
||||
}
|
||||
is TabTrayDialogFragmentAction.ExitMultiSelectMode -> state.copy(mode = TabTrayDialogFragmentState.Mode.Normal)
|
||||
is TabTrayDialogFragmentAction.EnterMultiSelectMode -> state.copy(
|
||||
mode = TabTrayDialogFragmentState.Mode.MultiSelect(
|
||||
setOf()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -4,17 +4,70 @@
|
|||
|
||||
package org.mozilla.fenix.tabtray
|
||||
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
interface TabTrayInteractor {
|
||||
/**
|
||||
* Called when user clicks the new tab button.
|
||||
*/
|
||||
fun onNewTabTapped(private: Boolean)
|
||||
|
||||
/**
|
||||
* Called when tab tray should be dismissed.
|
||||
*/
|
||||
fun onTabTrayDismissed()
|
||||
|
||||
/**
|
||||
* Called when user clicks the share tabs button.
|
||||
*/
|
||||
fun onShareTabsClicked(private: Boolean)
|
||||
fun onSaveToCollectionClicked()
|
||||
|
||||
/**
|
||||
* Called when user clicks button to save selected tabs to a collection.
|
||||
*/
|
||||
fun onSaveToCollectionClicked(selectedTabs: Set<Tab>)
|
||||
|
||||
/**
|
||||
* Called when user clicks the close all tabs button.
|
||||
*/
|
||||
fun onCloseAllTabsClicked(private: Boolean)
|
||||
|
||||
/**
|
||||
* Called when the physical back button is clicked.
|
||||
*/
|
||||
fun onBackPressed(): Boolean
|
||||
|
||||
/**
|
||||
* Called when a requester needs to know the current mode of the tab tray.
|
||||
*/
|
||||
fun onModeRequested(): TabTrayDialogFragmentState.Mode
|
||||
|
||||
/**
|
||||
* Called when a tab should be opened in the browser.
|
||||
*/
|
||||
fun onOpenTab(tab: Tab)
|
||||
|
||||
/**
|
||||
* Called when a tab should be selected in multiselect mode.
|
||||
*/
|
||||
fun onAddSelectedTab(tab: Tab)
|
||||
|
||||
/**
|
||||
* Called when a tab should be unselected in multiselect mode.
|
||||
*/
|
||||
fun onRemoveSelectedTab(tab: Tab)
|
||||
|
||||
/**
|
||||
* Called when multiselect mode should be entered with no tabs selected.
|
||||
*/
|
||||
fun onEnterMultiselect()
|
||||
}
|
||||
|
||||
/**
|
||||
* Interactor for the tab tray fragment.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class TabTrayFragmentInteractor(private val controller: TabTrayController) : TabTrayInteractor {
|
||||
override fun onNewTabTapped(private: Boolean) {
|
||||
controller.onNewTabTapped(private)
|
||||
|
@ -28,11 +81,35 @@ class TabTrayFragmentInteractor(private val controller: TabTrayController) : Tab
|
|||
controller.onShareTabsClicked(private)
|
||||
}
|
||||
|
||||
override fun onSaveToCollectionClicked() {
|
||||
controller.onSaveToCollectionClicked()
|
||||
override fun onSaveToCollectionClicked(selectedTabs: Set<Tab>) {
|
||||
controller.onSaveToCollectionClicked(selectedTabs)
|
||||
}
|
||||
|
||||
override fun onCloseAllTabsClicked(private: Boolean) {
|
||||
controller.onCloseAllTabsClicked(private)
|
||||
}
|
||||
|
||||
override fun onBackPressed(): Boolean {
|
||||
return controller.handleBackPressed()
|
||||
}
|
||||
|
||||
override fun onModeRequested(): TabTrayDialogFragmentState.Mode {
|
||||
return controller.onModeRequested()
|
||||
}
|
||||
|
||||
override fun onAddSelectedTab(tab: Tab) {
|
||||
controller.handleAddSelectedTab(tab)
|
||||
}
|
||||
|
||||
override fun onRemoveSelectedTab(tab: Tab) {
|
||||
controller.handleRemoveSelectedTab(tab)
|
||||
}
|
||||
|
||||
override fun onOpenTab(tab: Tab) {
|
||||
controller.handleOpenTab(tab)
|
||||
}
|
||||
|
||||
override fun onEnterMultiselect() {
|
||||
controller.handleEnterMultiselect()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,15 +9,18 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import kotlinx.android.synthetic.main.component_tabstray.*
|
||||
import kotlinx.android.synthetic.main.component_tabstray.view.*
|
||||
import kotlinx.android.synthetic.main.component_tabstray_fab.view.*
|
||||
import kotlinx.android.synthetic.main.tabs_tray_tab_counter.*
|
||||
|
@ -30,6 +33,7 @@ import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
|
|||
import mozilla.components.browser.state.selector.normalTabs
|
||||
import mozilla.components.browser.state.selector.privateTabs
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.support.ktx.android.util.dpToPx
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.components
|
||||
|
@ -51,16 +55,20 @@ class TabTrayView(
|
|||
val fabView = LayoutInflater.from(container.context)
|
||||
.inflate(R.layout.component_tabstray_fab, container, true)
|
||||
|
||||
private val hasAccessibilityEnabled = container.context.settings().accessibilityServicesEnabled
|
||||
|
||||
val view = LayoutInflater.from(container.context)
|
||||
.inflate(R.layout.component_tabstray, container, true)
|
||||
|
||||
val isPrivateModeSelected: Boolean get() = view.tab_layout.selectedTabPosition == PRIVATE_TAB_ID
|
||||
private val isPrivateModeSelected: Boolean get() = view.tab_layout.selectedTabPosition == PRIVATE_TAB_ID
|
||||
|
||||
private val behavior = BottomSheetBehavior.from(view.tab_wrapper)
|
||||
|
||||
private val tabTrayItemMenu: TabTrayItemMenu
|
||||
private var menu: BrowserMenu? = null
|
||||
|
||||
private var tabsTouchHelper: TabsTouchHelper
|
||||
|
||||
private var hasLoaded = false
|
||||
|
||||
override val containerView: View?
|
||||
|
@ -69,8 +77,6 @@ class TabTrayView(
|
|||
init {
|
||||
container.context.components.analytics.metrics.track(Event.TabsTrayOpened)
|
||||
|
||||
val hasAccessibilityEnabled = view.context.settings().accessibilityServicesEnabled
|
||||
|
||||
toggleFabText(isPrivate)
|
||||
|
||||
behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
|
@ -126,8 +132,10 @@ class TabTrayView(
|
|||
}
|
||||
adapter = tabsAdapter
|
||||
|
||||
TabsTouchHelper(tabsAdapter).attachToRecyclerView(this)
|
||||
tabsTouchHelper = TabsTouchHelper(tabsAdapter)
|
||||
tabsTouchHelper.attachToRecyclerView(this)
|
||||
|
||||
tabsAdapter.tabTrayInteractor = interactor
|
||||
tabsAdapter.onTabsUpdated = {
|
||||
if (hasAccessibilityEnabled) {
|
||||
tabsAdapter.notifyDataSetChanged()
|
||||
|
@ -158,7 +166,7 @@ class TabTrayView(
|
|||
is TabTrayItemMenu.Item.ShareAllTabs -> interactor.onShareTabsClicked(
|
||||
isPrivateModeSelected
|
||||
)
|
||||
is TabTrayItemMenu.Item.SaveToCollection -> interactor.onSaveToCollectionClicked()
|
||||
is TabTrayItemMenu.Item.SaveToCollection -> interactor.onEnterMultiselect()
|
||||
is TabTrayItemMenu.Item.CloseAllTabs -> interactor.onCloseAllTabsClicked(
|
||||
isPrivateModeSelected
|
||||
)
|
||||
|
@ -179,6 +187,10 @@ class TabTrayView(
|
|||
}
|
||||
}
|
||||
|
||||
adjustNewTabButtonsForNormalMode()
|
||||
}
|
||||
|
||||
private fun adjustNewTabButtonsForNormalMode() {
|
||||
view.tab_tray_new_tab.apply {
|
||||
isVisible = hasAccessibilityEnabled
|
||||
setOnClickListener {
|
||||
|
@ -214,7 +226,7 @@ class TabTrayView(
|
|||
toggleFabText(isPrivateModeSelected)
|
||||
filterTabs.invoke(isPrivateModeSelected)
|
||||
|
||||
updateState(view.context.components.core.store.state)
|
||||
updateUINormalMode(view.context.components.core.store.state)
|
||||
scrollToTab(view.context.components.core.store.state.selectedTabId)
|
||||
|
||||
if (isPrivateModeSelected) {
|
||||
|
@ -230,32 +242,168 @@ class TabTrayView(
|
|||
override fun onTabUnselected(tab: TabLayout.Tab?) { /*noop*/
|
||||
}
|
||||
|
||||
fun updateState(state: BrowserState) {
|
||||
view.let {
|
||||
val hasNoTabs = if (isPrivateModeSelected) {
|
||||
state.privateTabs.isEmpty()
|
||||
} else {
|
||||
state.normalTabs.isEmpty()
|
||||
}
|
||||
var mode: TabTrayDialogFragmentState.Mode = TabTrayDialogFragmentState.Mode.Normal
|
||||
private set
|
||||
|
||||
view.tab_tray_empty_view.isVisible = hasNoTabs
|
||||
if (hasNoTabs) {
|
||||
view.tab_tray_empty_view.text = if (isPrivateModeSelected) {
|
||||
view.context.getString(R.string.no_private_tabs_description)
|
||||
} else {
|
||||
view.context?.getString(R.string.no_open_tabs_description)
|
||||
fun updateState(state: TabTrayDialogFragmentState) {
|
||||
val oldMode = mode
|
||||
|
||||
if (oldMode::class != state.mode::class && view.context.settings().accessibilityServicesEnabled) {
|
||||
view.announceForAccessibility(
|
||||
if (state.mode == TabTrayDialogFragmentState.Mode.Normal) view.context.getString(
|
||||
R.string.tab_tray_exit_multiselect_content_description
|
||||
) else view.context.getString(R.string.tab_tray_enter_multiselect_content_description)
|
||||
)
|
||||
}
|
||||
|
||||
mode = state.mode
|
||||
when (state.mode) {
|
||||
TabTrayDialogFragmentState.Mode.Normal -> {
|
||||
view.tabsTray.apply {
|
||||
tabsTouchHelper.attachToRecyclerView(this)
|
||||
}
|
||||
|
||||
toggleUIMultiselect(multiselect = false)
|
||||
|
||||
updateUINormalMode(state.browserState)
|
||||
}
|
||||
is TabTrayDialogFragmentState.Mode.MultiSelect -> {
|
||||
// Disable swipe to delete while in multiselect
|
||||
tabsTouchHelper.attachToRecyclerView(null)
|
||||
|
||||
toggleUIMultiselect(multiselect = true)
|
||||
|
||||
fabView.new_tab_button.isVisible = false
|
||||
view.tab_tray_new_tab.isVisible = false
|
||||
view.collect_multi_select.isVisible = state.mode.selectedItems.size > 0
|
||||
|
||||
view.multiselect_title.text = view.context.getString(
|
||||
R.string.tab_tray_multi_select_title,
|
||||
state.mode.selectedItems.size
|
||||
)
|
||||
view.collect_multi_select.setOnClickListener {
|
||||
interactor.onSaveToCollectionClicked(state.mode.selectedItems)
|
||||
}
|
||||
view.exit_multi_select.setOnClickListener {
|
||||
interactor.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view.tabsTray.visibility = if (hasNoTabs) {
|
||||
View.INVISIBLE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
if (oldMode.selectedItems != state.mode.selectedItems) {
|
||||
val unselectedItems = oldMode.selectedItems - state.mode.selectedItems
|
||||
|
||||
state.mode.selectedItems.union(unselectedItems).forEach { item ->
|
||||
if (view.context.settings().accessibilityServicesEnabled) {
|
||||
view.announceForAccessibility(
|
||||
if (unselectedItems.contains(item)) view.context.getString(
|
||||
R.string.tab_tray_item_unselected_multiselect_content_description,
|
||||
item.title
|
||||
) else view.context.getString(
|
||||
R.string.tab_tray_item_selected_multiselect_content_description,
|
||||
item.title
|
||||
)
|
||||
)
|
||||
}
|
||||
updateTabsForSelectionChanged(item.id)
|
||||
}
|
||||
view.tab_tray_overflow.isVisible = !hasNoTabs
|
||||
}
|
||||
}
|
||||
|
||||
counter_text.text = "${state.normalTabs.size}"
|
||||
updateTabCounterContentDescription(state.normalTabs.size)
|
||||
private fun ConstraintLayout.setChildWPercent(percentage: Float, @IdRes childId: Int) {
|
||||
this.findViewById<View>(childId)?.let {
|
||||
val constraintSet = ConstraintSet()
|
||||
constraintSet.clone(this)
|
||||
constraintSet.constrainPercentWidth(it.id, percentage)
|
||||
constraintSet.applyTo(this)
|
||||
it.requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUINormalMode(browserState: BrowserState) {
|
||||
val hasNoTabs = if (isPrivateModeSelected) {
|
||||
browserState.privateTabs.isEmpty()
|
||||
} else {
|
||||
browserState.normalTabs.isEmpty()
|
||||
}
|
||||
|
||||
view.tab_tray_empty_view.isVisible = hasNoTabs
|
||||
if (hasNoTabs) {
|
||||
view.tab_tray_empty_view.text = if (isPrivateModeSelected) {
|
||||
view.context.getString(R.string.no_private_tabs_description)
|
||||
} else {
|
||||
view.context?.getString(R.string.no_open_tabs_description)
|
||||
}
|
||||
}
|
||||
|
||||
view.tabsTray.visibility = if (hasNoTabs) {
|
||||
View.INVISIBLE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
view.tab_tray_overflow.isVisible = !hasNoTabs
|
||||
|
||||
counter_text.text = "${browserState.normalTabs.size}"
|
||||
updateTabCounterContentDescription(browserState.normalTabs.size)
|
||||
|
||||
adjustNewTabButtonsForNormalMode()
|
||||
}
|
||||
|
||||
private fun toggleUIMultiselect(multiselect: Boolean) {
|
||||
view.multiselect_title.isVisible = multiselect
|
||||
view.collect_multi_select.isVisible = multiselect
|
||||
view.exit_multi_select.isVisible = multiselect
|
||||
|
||||
view.topBar.setBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
view.context,
|
||||
if (multiselect) R.color.accent_normal_theme else R.color.foundation_normal_theme
|
||||
)
|
||||
)
|
||||
|
||||
val displayMetrics = view.context.resources.displayMetrics
|
||||
|
||||
view.handle.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
height =
|
||||
if (multiselect) MULTISELECT_HANDLE_HEIGHT.dpToPx(displayMetrics) else NORMAL_HANDLE_HEIGHT.dpToPx(
|
||||
displayMetrics
|
||||
)
|
||||
topMargin = if (multiselect) 0.dpToPx(displayMetrics) else NORMAL_TOP_MARGIN.dpToPx(
|
||||
displayMetrics
|
||||
)
|
||||
}
|
||||
|
||||
view.tab_wrapper.setChildWPercent(
|
||||
if (multiselect) 1F else NORMAL_HANDLE_PERCENT_WIDTH,
|
||||
view.handle.id
|
||||
)
|
||||
|
||||
view.handle.setBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
view.context,
|
||||
if (multiselect) R.color.accent_normal_theme else R.color.secondary_text_normal_theme
|
||||
)
|
||||
)
|
||||
|
||||
view.tab_layout.isVisible = !multiselect
|
||||
view.tab_tray_empty_view.isVisible = !multiselect
|
||||
view.tab_tray_overflow.isVisible = !multiselect
|
||||
view.tab_layout.isVisible = !multiselect
|
||||
}
|
||||
|
||||
private fun updateTabsForSelectionChanged(itemId: String) {
|
||||
view.tabsTray.apply {
|
||||
val tabs = if (isPrivateModeSelected) {
|
||||
view.context.components.core.store.state.privateTabs
|
||||
} else {
|
||||
view.context.components.core.store.state.normalTabs
|
||||
}
|
||||
|
||||
val selectedBrowserTabIndex = tabs.indexOfFirst { it.id == itemId }
|
||||
|
||||
this.adapter?.notifyItemChanged(
|
||||
selectedBrowserTabIndex, true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -293,6 +441,10 @@ class TabTrayView(
|
|||
}
|
||||
}
|
||||
|
||||
fun onBackPressed(): Boolean {
|
||||
return interactor.onBackPressed()
|
||||
}
|
||||
|
||||
fun scrollToTab(sessionId: String?) {
|
||||
view.tabsTray.apply {
|
||||
val tabs = if (isPrivateModeSelected) {
|
||||
|
@ -314,6 +466,10 @@ class TabTrayView(
|
|||
private const val EXPAND_AT_SIZE = 3
|
||||
private const val SLIDE_OFFSET = 0
|
||||
private const val SELECTION_DELAY = 500
|
||||
private const val MULTISELECT_HANDLE_HEIGHT = 11
|
||||
private const val NORMAL_HANDLE_HEIGHT = 3
|
||||
private const val NORMAL_TOP_MARGIN = 8
|
||||
private const val NORMAL_HANDLE_PERCENT_WIDTH = 0.1F
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -46,8 +46,7 @@ class TabTrayViewHolder(
|
|||
itemView: View,
|
||||
private val imageLoader: ImageLoader,
|
||||
private val store: BrowserStore = itemView.context.components.core.store,
|
||||
private val metrics: MetricController = itemView.context.components.analytics.metrics,
|
||||
val getSelectedTabId: () -> String? = { store.state.selectedTabId }
|
||||
private val metrics: MetricController = itemView.context.components.analytics.metrics
|
||||
) : TabViewHolder(itemView) {
|
||||
|
||||
private val titleView: TextView = itemView.findViewById(R.id.mozac_browser_tabstray_title)
|
||||
|
@ -71,9 +70,6 @@ class TabTrayViewHolder(
|
|||
styling: TabsTrayStyling,
|
||||
observable: Observable<TabsTray.Observer>
|
||||
) {
|
||||
// This is a hack to workaround a bug in a-c.
|
||||
// https://github.com/mozilla-mobile/android-components/issues/7186
|
||||
val isSelected2 = tab.id == getSelectedTabId()
|
||||
this.tab = tab
|
||||
|
||||
// Basic text
|
||||
|
@ -82,7 +78,7 @@ class TabTrayViewHolder(
|
|||
updateCloseButtonDescription(tab.title)
|
||||
|
||||
// Drawables and theme
|
||||
updateBackgroundColor(isSelected2)
|
||||
updateBackgroundColor(isSelected)
|
||||
|
||||
if (tab.thumbnail != null) {
|
||||
thumbnailView.setImageBitmap(tab.thumbnail)
|
||||
|
@ -144,10 +140,6 @@ class TabTrayViewHolder(
|
|||
}
|
||||
}
|
||||
|
||||
itemView.setOnClickListener {
|
||||
observable.notifyObservers { onTabSelected(tab) }
|
||||
}
|
||||
|
||||
closeView.setOnClickListener {
|
||||
observable.notifyObservers { onTabClosed(tab) }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
<?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/. -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/linear_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<View
|
||||
android:id="@+id/top_divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="?neutralFaded" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scroll_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:listitem="@layout/collection_dialog_list_item" />
|
||||
</ScrollView>
|
||||
|
||||
<View
|
||||
android:id="@+id/bottom_divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="?neutralFaded" />
|
||||
</LinearLayout>
|
|
@ -0,0 +1,20 @@
|
|||
<?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/. -->
|
||||
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/add_new_collection"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:drawablePadding="24dp"
|
||||
android:ellipsize="marquee"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="?attr/listPreferredItemHeightSmall"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="?attr/dialogPreferredPadding"
|
||||
android:text="@string/tab_tray_add_new_collection"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="?primaryText"
|
||||
app:drawableStartCompat="?android:attr/listChoiceIndicatorSingle" />
|
|
@ -2,15 +2,14 @@
|
|||
<!-- 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/. -->
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:mozac="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/tab_wrapper"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
style="@style/BottomSheetModal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:backgroundTint="@color/foundation_normal_theme"
|
||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
||||
|
||||
|
@ -20,81 +19,141 @@
|
|||
android:layout_height="3dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@color/secondary_text_normal_theme"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.1"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_percent="0.1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tab_tray_empty_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:gravity="center_horizontal"
|
||||
android:paddingTop="80dp"
|
||||
android:text="@string/no_open_tabs_description"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:textColor="?secondaryText"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tab_layout" />
|
||||
app:layout_constraintTop_toBottomOf="@id/topBar" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tab_layout"
|
||||
android:layout_width="0dp"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/topBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:background="@color/foundation_normal_theme"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/handle"
|
||||
app:layout_constraintWidth_percent="0.5"
|
||||
app:tabGravity="fill"
|
||||
app:tabIconTint="@color/tab_icon"
|
||||
app:tabIndicatorColor="@color/accent_normal_theme"
|
||||
app:tabRippleColor="@android:color/transparent">
|
||||
app:layout_constraintTop_toBottomOf="@+id/handle">
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:id="@+id/default_tab_item"
|
||||
<ImageButton
|
||||
android:id="@+id/exit_multi_select"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="0dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/tab_tray_close_multiselect_content_description"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/multiselect_title"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/multiselect_title"
|
||||
app:srcCompat="@drawable/ic_close"
|
||||
app:tint="@color/contrast_text_normal_theme" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/multiselect_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/tab_header_label"
|
||||
android:layout="@layout/tabs_tray_tab_counter"
|
||||
app:tabIconTint="@color/tab_icon" />
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:textColor="@color/contrast_text_normal_theme"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/collect_multi_select"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toEndOf="@+id/exit_multi_select"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="3 selected" />
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:id="@+id/private_tab_item"
|
||||
<TextView
|
||||
android:id="@+id/collect_multi_select"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
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="@id/multiselect_title"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/multiselect_title" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tab_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/tabs_header_private_tabs_title"
|
||||
android:icon="@drawable/ic_private_browsing" />
|
||||
android:layout_height="80dp"
|
||||
android:background="@color/foundation_normal_theme"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_percent="0.5"
|
||||
app:tabGravity="fill"
|
||||
app:tabIconTint="@color/tab_icon"
|
||||
app:tabIndicatorColor="@color/accent_normal_theme"
|
||||
app:tabRippleColor="@android:color/transparent">
|
||||
|
||||
</com.google.android.material.tabs.TabLayout>
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:id="@+id/default_tab_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/tab_header_label"
|
||||
android:layout="@layout/tabs_tray_tab_counter"
|
||||
app:tabIconTint="@color/tab_icon" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/tab_tray_new_tab"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:visibility="gone"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/add_tab"
|
||||
app:srcCompat="@drawable/ic_new"
|
||||
app:layout_constraintTop_toTopOf="@id/tab_layout"
|
||||
app:layout_constraintEnd_toStartOf="@id/tab_tray_overflow"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tab_layout" />
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:id="@+id/private_tab_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/tabs_header_private_tabs_title"
|
||||
android:icon="@drawable/ic_private_browsing" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/tab_tray_overflow"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/open_tabs_menu"
|
||||
app:srcCompat="@drawable/ic_menu_tab_tray"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/tab_layout"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tab_layout" />
|
||||
</com.google.android.material.tabs.TabLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/tab_tray_new_tab"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/add_tab"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tab_layout"
|
||||
app:layout_constraintEnd_toStartOf="@id/tab_tray_overflow"
|
||||
app:layout_constraintTop_toTopOf="@id/tab_layout"
|
||||
app:srcCompat="@drawable/ic_new" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/tab_tray_overflow"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/open_tabs_menu"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tab_layout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/tab_layout"
|
||||
app:srcCompat="@drawable/ic_menu_tab_tray" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider"
|
||||
|
@ -103,14 +162,14 @@
|
|||
android:background="@color/tab_tray_item_divider_normal_theme"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tab_layout" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/topBar" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/tabsTray"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:paddingBottom="140dp"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="140dp"
|
||||
android:scrollbars="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<?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/. -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/name_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="26dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:text="@string/tab_tray_add_new_collection_name"
|
||||
android:textAllCaps="true"
|
||||
android:textAppearance="@style/Body16TextStyle"
|
||||
android:textColor="?secondaryText" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/collection_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:backgroundTint="?neutral"
|
||||
android:inputType="text"
|
||||
android:singleLine="true"
|
||||
android:textAlignment="viewStart" />
|
||||
</LinearLayout>
|
|
@ -7,7 +7,9 @@
|
|||
android:id="@+id/tab_item"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="88dp"
|
||||
android:focusable="true">
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:foreground="?android:selectableItemBackground">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/play_pause_button"
|
||||
|
@ -36,12 +38,12 @@
|
|||
|
||||
<ImageView
|
||||
android:id="@+id/default_tab_thumbnail"
|
||||
android:src="@drawable/mozac_ic_globe"
|
||||
android:tint="?tabTrayThumbnailIcon"
|
||||
android:padding="22dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
android:layout_height="match_parent"
|
||||
android:importantForAccessibility="no"
|
||||
android:padding="22dp"
|
||||
app:srcCompat="@drawable/mozac_ic_globe"
|
||||
app:tint="?tabTrayThumbnailIcon" />
|
||||
|
||||
<mozilla.components.browser.tabstray.thumbnail.TabThumbnailView
|
||||
android:id="@+id/mozac_browser_tabstray_thumbnail"
|
||||
|
@ -49,6 +51,26 @@
|
|||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/mozac_browser_tabstray_open_tab" />
|
||||
|
||||
<View
|
||||
android:id="@+id/selected_mask"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/tab_tray_selected_mask_normal_theme"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/checkmark"
|
||||
android:contentDescription="@string/tab_tray_multiselect_selected_content_description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal|center_vertical"
|
||||
android:background="@drawable/favicon_background"
|
||||
android:backgroundTint="@color/accent_normal_theme"
|
||||
android:elevation="1dp"
|
||||
android:padding="10dp"
|
||||
android:visibility="gone"
|
||||
app:srcCompat="@drawable/mozac_ic_check" />
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<TextView
|
||||
|
@ -88,10 +110,10 @@
|
|||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/close_tab"
|
||||
android:tint="@color/tab_tray_item_text_normal_theme"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/mozac_ic_close" />
|
||||
app:srcCompat="@drawable/mozac_ic_close"
|
||||
app:tint="@color/tab_tray_item_text_normal_theme" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -109,7 +109,12 @@
|
|||
<dialog
|
||||
android:id="@+id/tabTrayDialogFragment"
|
||||
android:name="org.mozilla.fenix.tabtray.TabTrayDialogFragment"
|
||||
tools:layout="@layout/fragment_tab_tray_dialog" />
|
||||
tools:layout="@layout/fragment_tab_tray_dialog">
|
||||
<argument
|
||||
android:name="enterMultiselect"
|
||||
android:defaultValue="false"
|
||||
app:argType="boolean" />
|
||||
</dialog>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/homeFragment"
|
||||
|
@ -374,13 +379,13 @@
|
|||
android:id="@+id/syncedTabsFragment"
|
||||
android:name="org.mozilla.fenix.sync.SyncedTabsFragment"
|
||||
android:label="@string/synced_tabs"
|
||||
tools:layout="@layout/fragment_synced_tabs"/>
|
||||
tools:layout="@layout/fragment_synced_tabs" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/loginExceptionsFragment"
|
||||
android:name="org.mozilla.fenix.loginexceptions.LoginExceptionsFragment"
|
||||
android:label="@string/preferences_passwords_exceptions"
|
||||
tools:layout="@layout/fragment_exceptions"/>
|
||||
tools:layout="@layout/fragment_exceptions" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/loginDetailFragment"
|
||||
|
|
|
@ -60,6 +60,7 @@
|
|||
<color name="tab_tray_heading_icon_inactive_normal_theme">@color/tab_tray_heading_icon_inactive_dark_theme</color>
|
||||
<color name="tab_tray_item_thumbnail_background_normal_theme">@color/tab_tray_item_thumbnail_background_dark_theme</color>
|
||||
<color name="tab_tray_item_thumbnail_icon_normal_theme">@color/tab_tray_item_thumbnail_icon_dark_theme</color>
|
||||
<color name="tab_tray_selected_mask_normal_theme">@color/tab_tray_selected_mask_dark_theme</color>
|
||||
|
||||
<!--Top site colors -->
|
||||
<color name="top_site_background">@color/top_site_background_dark_theme</color>
|
||||
|
|
|
@ -85,6 +85,7 @@
|
|||
<color name="tab_tray_heading_icon_inactive_light_theme">@color/ink_20_48a</color>
|
||||
<color name="tab_tray_item_thumbnail_background_light_theme">@color/light_grey_10</color>
|
||||
<color name="tab_tray_item_thumbnail_icon_light_theme">@color/light_grey_60</color>
|
||||
<color name="tab_tray_selected_mask_light_theme">@color/violet_70_12a</color>
|
||||
|
||||
<!-- Dark theme color palette -->
|
||||
<color name="primary_text_dark_theme">#FBFBFE</color>
|
||||
|
@ -144,6 +145,7 @@
|
|||
<color name="tab_tray_heading_icon_inactive_dark_theme">@color/violet_50_48a</color>
|
||||
<color name="tab_tray_item_thumbnail_background_dark_theme">@color/dark_grey_50</color>
|
||||
<color name="tab_tray_item_thumbnail_icon_dark_theme">@color/dark_grey_05</color>
|
||||
<color name="tab_tray_selected_mask_dark_theme">@color/violet_50_32a</color>
|
||||
|
||||
<!-- Private theme color palette -->
|
||||
<color name="primary_text_private_theme">#FBFBFE</color>
|
||||
|
@ -249,6 +251,7 @@
|
|||
<color name="tab_tray_heading_icon_inactive_normal_theme">@color/tab_tray_heading_icon_inactive_light_theme</color>
|
||||
<color name="tab_tray_item_thumbnail_background_normal_theme">@color/tab_tray_item_thumbnail_background_light_theme</color>
|
||||
<color name="tab_tray_item_thumbnail_icon_normal_theme">@color/tab_tray_item_thumbnail_icon_light_theme</color>
|
||||
<color name="tab_tray_selected_mask_normal_theme">@color/tab_tray_selected_mask_light_theme</color>
|
||||
|
||||
<!-- Bookmark buttons -->
|
||||
<color name="bookmark_favicon_background">#DFDFE3</color>
|
||||
|
|
|
@ -23,6 +23,28 @@
|
|||
<string name="open_tab_tray_single">1 open tab. Tap to switch tabs.</string>
|
||||
<!-- Message announced to the user when tab tray is selected with 0 or 2+ tabs -->
|
||||
<string name="open_tab_tray_plural">%1$s open tabs. Tap to switch tabs.</string>
|
||||
<!-- Tab tray multi select title in app bar. The first parameter is the number of tabs selected -->
|
||||
<string name="tab_tray_multi_select_title">%1$d selected</string>
|
||||
<!-- Label of button in create collection dialog for creating a new collection -->
|
||||
<string name="tab_tray_add_new_collection">Add new collection</string>
|
||||
<!-- Label of editable text in create collection dialog for naming a new collection -->
|
||||
<string name="tab_tray_add_new_collection_name">Name</string>
|
||||
<!-- Label of button in save to collection dialog for selecting a current collection -->
|
||||
<string name="tab_tray_select_collection">Select collection</string>
|
||||
<!-- Content description for close button while in multiselect mode in tab tray -->
|
||||
<string name="tab_tray_close_multiselect_content_description">Exit multiselect mode</string>
|
||||
<!-- Content description for save to collection button while in multiselect mode in tab tray -->
|
||||
<string name="tab_tray_collection_button_multiselect_content_description">Save selected tabs to collection</string>
|
||||
<!-- Content description for checkmark while tab is selected while in multiselect mode in tab tray. The first parameter is the title of the tab selected -->
|
||||
<string name="tab_tray_item_selected_multiselect_content_description">Selected %1$s</string>
|
||||
<!-- Content description when tab is unselected while in multiselect mode in tab tray. The first parameter is the title of the tab unselected -->
|
||||
<string name="tab_tray_item_unselected_multiselect_content_description">Unselected %1$s</string>
|
||||
<!-- Content description announcement when exiting multiselect mode in tab tray -->
|
||||
<string name="tab_tray_exit_multiselect_content_description">Exited multiselect mode</string>
|
||||
<!-- Content description announcement when entering multiselect mode in tab tray -->
|
||||
<string name="tab_tray_enter_multiselect_content_description">Entered multiselect mode, select tabs to save to a collection</string>
|
||||
<!-- Content description on checkmark while tab is selected in multiselect mode in tab tray -->
|
||||
<string name="tab_tray_multiselect_selected_content_description">Selected</string>
|
||||
|
||||
<!-- About content. The first parameter is the name of the application. (For example: Fenix) -->
|
||||
<string name="about_content">%1$s is produced by Mozilla.</string>
|
||||
|
@ -498,6 +520,8 @@
|
|||
<string name="remove_top_site">Remove</string>
|
||||
<!-- Postfix for private WebApp titles, placeholder is replaced with app name -->
|
||||
<string name="pwa_site_controls_title_private">%1$s (Private Mode)</string>
|
||||
<!-- Button in the current tab tray header in multiselect mode. Saved the selected tabs to a collection when pressed. -->
|
||||
<string name="tab_tray_save_to_collection">Save</string>
|
||||
|
||||
<!-- History -->
|
||||
<!-- Text for the button to clear all history -->
|
||||
|
|
|
@ -100,6 +100,10 @@
|
|||
<style name="NormalTheme" parent="NormalThemeBase" />
|
||||
|
||||
<style name="BaseDialogStyle" parent="Theme.MaterialComponents.Dialog.Alert">
|
||||
<item name="colorControlNormal">?primaryText</item>
|
||||
<item name="textColorAlertDialogListItem">?primaryText</item>
|
||||
<item name="android:titleTextStyle">@style/HeaderTextStyle</item>
|
||||
<item name="android:windowTitleStyle">@style/HeaderTextStyle</item>
|
||||
<item name="dialogCornerRadius">@dimen/tab_corner_radius</item>
|
||||
<item name="android:colorBackground">?above</item>
|
||||
<item name="colorAccent">?accent</item>
|
||||
|
|
|
@ -210,41 +210,6 @@ class DefaultCollectionCreationControllerTest {
|
|||
assertEquals(SaveCollectionStep.SelectCollection, controller.stepBack(SaveCollectionStep.NameCollection))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN list of collections WHEN default collection number is required THEN return next default number`() {
|
||||
val collections = mutableListOf<TabCollection>(
|
||||
mockk {
|
||||
every { title } returns "Collection 1"
|
||||
},
|
||||
mockk {
|
||||
every { title } returns "Collection 2"
|
||||
},
|
||||
mockk {
|
||||
every { title } returns "Collection 3"
|
||||
}
|
||||
)
|
||||
state = state.copy(tabCollections = collections)
|
||||
assertEquals(4, controller.getDefaultCollectionNumber())
|
||||
|
||||
collections.add(mockk {
|
||||
every { title } returns "Collection 5"
|
||||
})
|
||||
state = state.copy(tabCollections = collections)
|
||||
assertEquals(6, controller.getDefaultCollectionNumber())
|
||||
|
||||
collections.add(mockk {
|
||||
every { title } returns "Random name"
|
||||
})
|
||||
state = state.copy(tabCollections = collections)
|
||||
assertEquals(6, controller.getDefaultCollectionNumber())
|
||||
|
||||
collections.add(mockk {
|
||||
every { title } returns "Collection 10 10"
|
||||
})
|
||||
state = state.copy(tabCollections = collections)
|
||||
assertEquals(6, controller.getDefaultCollectionNumber())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN adding a new collection THEN dispatch NameCollection step changed`() {
|
||||
controller.addNewCollection()
|
||||
|
@ -275,27 +240,6 @@ class DefaultCollectionCreationControllerTest {
|
|||
verify { store.dispatch(CollectionCreationAction.StepChanged(SaveCollectionStep.SelectCollection, 2)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `normalSessionSize only counts non-private non-custom sessions`() {
|
||||
val normal1 = mockSession()
|
||||
val normal2 = mockSession()
|
||||
val normal3 = mockSession()
|
||||
|
||||
val private1 = mockSession(isPrivate = true)
|
||||
val private2 = mockSession(isPrivate = true)
|
||||
|
||||
val custom1 = mockSession(isCustom = true)
|
||||
val custom2 = mockSession(isCustom = true)
|
||||
val custom3 = mockSession(isCustom = true)
|
||||
|
||||
val privateCustom = mockSession(isPrivate = true, isCustom = true)
|
||||
|
||||
every { sessionManager.sessions } returns listOf(normal1, private1, private2, custom1,
|
||||
normal2, normal3, custom2, custom3, privateCustom)
|
||||
|
||||
assertEquals(3, controller.normalSessionSize(sessionManager))
|
||||
}
|
||||
|
||||
private fun mockSession(
|
||||
sessionId: String? = null,
|
||||
isPrivate: Boolean = false,
|
||||
|
|
|
@ -43,9 +43,44 @@ class SessionManagerTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `normalSessionSize only counts non-private non-custom sessions`() {
|
||||
val normal1 = mockSession()
|
||||
val normal2 = mockSession()
|
||||
val normal3 = mockSession()
|
||||
|
||||
val private1 = mockSession(isPrivate = true)
|
||||
val private2 = mockSession(isPrivate = true)
|
||||
|
||||
val custom1 = mockSession(isCustom = true)
|
||||
val custom2 = mockSession(isCustom = true)
|
||||
val custom3 = mockSession(isCustom = true)
|
||||
|
||||
val privateCustom = mockSession(isPrivate = true, isCustom = true)
|
||||
|
||||
val sessionManager = mockSessionManager(
|
||||
listOf(
|
||||
normal1, private1, private2, custom1,
|
||||
normal2, normal3, custom2, custom3, privateCustom
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(3, sessionManager.normalSessionSize())
|
||||
}
|
||||
|
||||
private fun mockSessionManager(sessions: List<Session>): SessionManager {
|
||||
val sessionManager: SessionManager = mockk()
|
||||
every { sessionManager.sessions } returns sessions
|
||||
return sessionManager
|
||||
}
|
||||
|
||||
private fun mockSession(
|
||||
sessionId: String? = null,
|
||||
isPrivate: Boolean = false,
|
||||
isCustom: Boolean = false
|
||||
) = mockk<Session> {
|
||||
sessionId?.let { every { id } returns it }
|
||||
every { private } returns isPrivate
|
||||
every { isCustomTabSession() } returns isCustom
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,37 @@ class TabCollectionTest {
|
|||
assertNotEquals(defaultColor, mockTabCollection(-123L).getIconColor(testContext))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN list of collections WHEN default collection number is required THEN return next default number`() {
|
||||
val collections = mutableListOf<TabCollection>(
|
||||
mockk {
|
||||
every { title } returns "Collection 1"
|
||||
},
|
||||
mockk {
|
||||
every { title } returns "Collection 2"
|
||||
},
|
||||
mockk {
|
||||
every { title } returns "Collection 3"
|
||||
}
|
||||
)
|
||||
assertEquals(4, collections.getDefaultCollectionNumber())
|
||||
|
||||
collections.add(mockk {
|
||||
every { title } returns "Collection 5"
|
||||
})
|
||||
assertEquals(6, collections.getDefaultCollectionNumber())
|
||||
|
||||
collections.add(mockk {
|
||||
every { title } returns "Random name"
|
||||
})
|
||||
assertEquals(6, collections.getDefaultCollectionNumber())
|
||||
|
||||
collections.add(mockk {
|
||||
every { title } returns "Collection 10 10"
|
||||
})
|
||||
assertEquals(6, collections.getDefaultCollectionNumber())
|
||||
}
|
||||
|
||||
private fun mockTabCollection(id: Long): TabCollection {
|
||||
val collection: TabCollection = mockk()
|
||||
every { collection.id } returns id
|
||||
|
|
|
@ -336,7 +336,7 @@ class DefaultSessionControlControllerTest {
|
|||
|
||||
verify {
|
||||
navController.navigate(
|
||||
match<NavDirections> { it.actionId == R.id.action_global_collectionCreationFragment },
|
||||
match<NavDirections> { it.actionId == R.id.action_global_tabTrayDialogFragment },
|
||||
null
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
/* 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.widget.FrameLayout
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class CollectionsAdapterTest {
|
||||
private val collectionList: Array<String> =
|
||||
arrayOf(
|
||||
"Add new collection",
|
||||
"Collection 1",
|
||||
"Collection 2"
|
||||
)
|
||||
private val onNewCollectionClicked: () -> Unit = mockk(relaxed = true)
|
||||
|
||||
@Test
|
||||
fun `getItemCount should return the correct list size`() {
|
||||
val adapter = CollectionsAdapter(collectionList, onNewCollectionClicked)
|
||||
|
||||
assertEquals(3, adapter.itemCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getSelectedCollection should account for add new collection when returning right item`() {
|
||||
val adapter = CollectionsAdapter(collectionList, onNewCollectionClicked)
|
||||
|
||||
// first collection by default
|
||||
assertEquals(1, adapter.checkedPosition)
|
||||
assertEquals(0, adapter.getSelectedCollection())
|
||||
|
||||
adapter.checkedPosition = 3
|
||||
assertEquals(2, adapter.getSelectedCollection())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `creates and binds viewholder`() {
|
||||
val adapter = CollectionsAdapter(collectionList, onNewCollectionClicked)
|
||||
|
||||
val holder1 = adapter.createViewHolder(FrameLayout(testContext), 0)
|
||||
val holder2 = adapter.createViewHolder(FrameLayout(testContext), 0)
|
||||
val holder3 = adapter.createViewHolder(FrameLayout(testContext), 0)
|
||||
|
||||
adapter.bindViewHolder(holder1, 0)
|
||||
adapter.bindViewHolder(holder2, 1)
|
||||
adapter.bindViewHolder(holder3, 2)
|
||||
|
||||
assertEquals("Add new collection", holder1.textView.text)
|
||||
holder1.textView.callOnClick()
|
||||
verify {
|
||||
onNewCollectionClicked()
|
||||
}
|
||||
|
||||
assertEquals(true, holder2.textView.isChecked)
|
||||
assertEquals("Collection 1", holder2.textView.text)
|
||||
holder2.textView.callOnClick()
|
||||
assertEquals(true, holder2.textView.isChecked)
|
||||
|
||||
assertEquals(false, holder3.textView.isChecked)
|
||||
assertEquals("Collection 2", holder3.textView.text)
|
||||
holder3.textView.callOnClick()
|
||||
adapter.bindViewHolder(holder3, 2)
|
||||
assertEquals(true, holder3.textView.isChecked)
|
||||
}
|
||||
}
|
|
@ -15,9 +15,12 @@ import io.mockk.mockkStatic
|
|||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
import io.mockk.verifyOrder
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import mozilla.components.browser.session.Session
|
||||
import mozilla.components.browser.session.SessionManager
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import mozilla.components.feature.tab.collections.TabCollection
|
||||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
|
@ -29,20 +32,22 @@ import org.mozilla.fenix.components.TabCollectionStorage
|
|||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.sessionsOfType
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class DefaultTabTrayControllerTest {
|
||||
|
||||
private val activity: HomeActivity = mockk(relaxed = true)
|
||||
private val navController: NavController = mockk()
|
||||
private val sessionManager: SessionManager = mockk(relaxed = true)
|
||||
private val dismissTabTray: (() -> Unit) = mockk(relaxed = true)
|
||||
private val dismissTabTrayAndNavigateHome: ((String) -> Unit) = mockk(relaxed = true)
|
||||
private val showUndoSnackbar: ((String, SessionManager.Snapshot) -> Unit) =
|
||||
mockk(relaxed = true)
|
||||
private val registerCollectionStorageObserver: (() -> Unit) = mockk(relaxed = true)
|
||||
private val showChooseCollectionDialog: ((List<Session>) -> Unit) = mockk(relaxed = true)
|
||||
private val showAddNewCollectionDialog: ((List<Session>) -> Unit) = mockk(relaxed = true)
|
||||
private val tabCollectionStorage: TabCollectionStorage = mockk(relaxed = true)
|
||||
private val tabCollection: TabCollection = mockk()
|
||||
private val cachedTabCollections: List<TabCollection> = listOf(tabCollection)
|
||||
private val currentDestination: NavDestination = mockk(relaxed = true)
|
||||
private val tabTrayFragmentStore: TabTrayDialogFragmentStore = mockk(relaxed = true)
|
||||
private val selectTabUseCase: TabsUseCases.SelectTabUseCase = mockk(relaxed = true)
|
||||
|
||||
private lateinit var controller: DefaultTabTrayController
|
||||
|
||||
|
@ -59,28 +64,34 @@ class DefaultTabTrayControllerTest {
|
|||
@Before
|
||||
fun setUp() {
|
||||
mockkStatic("org.mozilla.fenix.ext.SessionManagerKt")
|
||||
|
||||
every { activity.components.core.sessionManager } returns sessionManager
|
||||
every { activity.components.core.tabCollectionStorage } returns tabCollectionStorage
|
||||
every { activity.components.core.engine.profiler } returns mockk(relaxed = true)
|
||||
|
||||
every { sessionManager.sessionsOfType(private = true) } returns listOf(session).asSequence()
|
||||
every { sessionManager.sessionsOfType(private = false) } returns listOf(nonPrivateSession).asSequence()
|
||||
every { sessionManager.createSessionSnapshot(any()) } returns SessionManager.Snapshot.Item(
|
||||
session
|
||||
)
|
||||
every { sessionManager.findSessionById("1234") } returns session
|
||||
every { sessionManager.remove(any()) } just Runs
|
||||
every { tabCollectionStorage.cachedTabCollections } returns cachedTabCollections
|
||||
every { sessionManager.selectedSession } returns nonPrivateSession
|
||||
every { navController.navigate(any<NavDirections>()) } just Runs
|
||||
every { navController.currentDestination } returns currentDestination
|
||||
every { currentDestination.id } returns R.id.browserFragment
|
||||
every { tabCollection.title } returns "Collection title"
|
||||
|
||||
controller = DefaultTabTrayController(
|
||||
activity = activity,
|
||||
navController = navController,
|
||||
dismissTabTray = dismissTabTray,
|
||||
dismissTabTrayAndNavigateHome = dismissTabTrayAndNavigateHome,
|
||||
registerCollectionStorageObserver = registerCollectionStorageObserver
|
||||
registerCollectionStorageObserver = registerCollectionStorageObserver,
|
||||
tabTrayDialogFragmentStore = tabTrayFragmentStore,
|
||||
selectTabUseCase = selectTabUseCase,
|
||||
showChooseCollectionDialog = showChooseCollectionDialog,
|
||||
showAddNewCollectionDialog = showAddNewCollectionDialog
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -120,24 +131,6 @@ class DefaultTabTrayControllerTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onSaveToCollectionClicked() {
|
||||
val navDirectionsSlot = slot<NavDirections>()
|
||||
every { navController.navigate(capture(navDirectionsSlot)) } just Runs
|
||||
|
||||
controller.onSaveToCollectionClicked()
|
||||
verify {
|
||||
registerCollectionStorageObserver()
|
||||
navController.navigate(capture(navDirectionsSlot))
|
||||
}
|
||||
|
||||
assertTrue(navDirectionsSlot.isCaptured)
|
||||
assertEquals(
|
||||
R.id.action_global_collectionCreationFragment,
|
||||
navDirectionsSlot.captured.actionId
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onShareTabsClicked() {
|
||||
val navDirectionsSlot = slot<NavDirections>()
|
||||
|
@ -161,4 +154,73 @@ class DefaultTabTrayControllerTest {
|
|||
dismissTabTrayAndNavigateHome(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleBackPressed() {
|
||||
every { tabTrayFragmentStore.state.mode } returns TabTrayDialogFragmentState.Mode.MultiSelect(
|
||||
setOf()
|
||||
)
|
||||
controller.handleBackPressed()
|
||||
verify {
|
||||
tabTrayFragmentStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onModeRequested() {
|
||||
val mode = TabTrayDialogFragmentState.Mode.MultiSelect(
|
||||
setOf()
|
||||
)
|
||||
every { tabTrayFragmentStore.state.mode } returns mode
|
||||
controller.onModeRequested()
|
||||
verify {
|
||||
tabTrayFragmentStore.state.mode
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleAddSelectedTab() {
|
||||
val tab = Tab("1234", "mozilla.org")
|
||||
controller.handleAddSelectedTab(tab)
|
||||
verify {
|
||||
tabTrayFragmentStore.dispatch(TabTrayDialogFragmentAction.AddItemForCollection(tab))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleRemoveSelectedTab() {
|
||||
val tab = Tab("1234", "mozilla.org")
|
||||
controller.handleRemoveSelectedTab(tab)
|
||||
verify {
|
||||
tabTrayFragmentStore.dispatch(TabTrayDialogFragmentAction.RemoveItemForCollection(tab))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleOpenTab() {
|
||||
val tab = Tab("1234", "mozilla.org")
|
||||
controller.handleOpenTab(tab)
|
||||
verify {
|
||||
selectTabUseCase.invoke(tab.id)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleEnterMultiselect() {
|
||||
controller.handleEnterMultiselect()
|
||||
verify {
|
||||
tabTrayFragmentStore.dispatch(TabTrayDialogFragmentAction.EnterMultiSelectMode)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onSaveToCollectionClicked() {
|
||||
val tab = Tab("1234", "mozilla.org")
|
||||
|
||||
controller.onSaveToCollectionClicked(setOf(tab))
|
||||
verify {
|
||||
registerCollectionStorageObserver()
|
||||
showChooseCollectionDialog(listOf(session))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
/* 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 kotlinx.coroutines.runBlocking
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.createTab
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotSame
|
||||
import org.junit.Test
|
||||
|
||||
class TabTrayDialogFragmentStoreTest {
|
||||
|
||||
@Test
|
||||
fun browserStateChange() = runBlocking {
|
||||
val initialState = emptyDefaultState()
|
||||
val store = TabTrayDialogFragmentStore(initialState)
|
||||
|
||||
val newBrowserState = BrowserState(
|
||||
listOf(
|
||||
createTab("https://www.mozilla.org", id = "13256")
|
||||
)
|
||||
)
|
||||
|
||||
store.dispatch(
|
||||
TabTrayDialogFragmentAction.BrowserStateChanged(
|
||||
newBrowserState
|
||||
)
|
||||
).join()
|
||||
|
||||
assertNotSame(initialState, store.state)
|
||||
assertEquals(
|
||||
store.state.browserState,
|
||||
newBrowserState
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun enterMultiselectMode() = runBlocking {
|
||||
val initialState = emptyDefaultState()
|
||||
val store = TabTrayDialogFragmentStore(initialState)
|
||||
|
||||
store.dispatch(
|
||||
TabTrayDialogFragmentAction.EnterMultiSelectMode
|
||||
).join()
|
||||
|
||||
assertNotSame(initialState, store.state)
|
||||
assertEquals(
|
||||
store.state.mode,
|
||||
TabTrayDialogFragmentState.Mode.MultiSelect(setOf())
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun exitMultiselectMode() = runBlocking {
|
||||
val initialState = TabTrayDialogFragmentState(
|
||||
browserState = BrowserState(),
|
||||
mode = TabTrayDialogFragmentState.Mode.MultiSelect(setOf())
|
||||
)
|
||||
val store = TabTrayDialogFragmentStore(initialState)
|
||||
|
||||
store.dispatch(
|
||||
TabTrayDialogFragmentAction.ExitMultiSelectMode
|
||||
).join()
|
||||
|
||||
assertNotSame(initialState, store.state)
|
||||
assertEquals(
|
||||
store.state.mode,
|
||||
TabTrayDialogFragmentState.Mode.Normal
|
||||
)
|
||||
assertEquals(
|
||||
store.state.mode.selectedItems,
|
||||
setOf<Tab>()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addItemForCollection() = runBlocking {
|
||||
val initialState = emptyDefaultState()
|
||||
val store = TabTrayDialogFragmentStore(initialState)
|
||||
|
||||
val tab = Tab(id = "1234", url = "mozilla.org")
|
||||
store.dispatch(
|
||||
TabTrayDialogFragmentAction.AddItemForCollection(tab)
|
||||
).join()
|
||||
|
||||
assertNotSame(initialState, store.state)
|
||||
assertEquals(
|
||||
store.state.mode,
|
||||
TabTrayDialogFragmentState.Mode.MultiSelect(setOf(tab))
|
||||
)
|
||||
assertEquals(
|
||||
store.state.mode.selectedItems,
|
||||
setOf(tab)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeItemForCollection() = runBlocking {
|
||||
val tab = Tab(id = "1234", url = "mozilla.org")
|
||||
val secondTab = Tab(id = "12345", url = "pocket.com")
|
||||
|
||||
val initialState = TabTrayDialogFragmentState(
|
||||
browserState = BrowserState(),
|
||||
mode = TabTrayDialogFragmentState.Mode.MultiSelect(setOf(tab, secondTab))
|
||||
)
|
||||
|
||||
val store = TabTrayDialogFragmentStore(initialState)
|
||||
|
||||
store.dispatch(
|
||||
TabTrayDialogFragmentAction.RemoveItemForCollection(tab)
|
||||
).join()
|
||||
|
||||
assertNotSame(initialState, store.state)
|
||||
assertEquals(
|
||||
store.state.mode,
|
||||
TabTrayDialogFragmentState.Mode.MultiSelect(setOf(secondTab))
|
||||
)
|
||||
assertEquals(
|
||||
store.state.mode.selectedItems,
|
||||
setOf(secondTab)
|
||||
)
|
||||
|
||||
store.dispatch(
|
||||
TabTrayDialogFragmentAction.RemoveItemForCollection(secondTab)
|
||||
).join()
|
||||
|
||||
assertEquals(
|
||||
store.state.mode,
|
||||
TabTrayDialogFragmentState.Mode.Normal
|
||||
)
|
||||
assertEquals(
|
||||
store.state.mode.selectedItems,
|
||||
setOf<Tab>()
|
||||
)
|
||||
}
|
||||
|
||||
private fun emptyDefaultState(): TabTrayDialogFragmentState = TabTrayDialogFragmentState(
|
||||
browserState = BrowserState(),
|
||||
mode = TabTrayDialogFragmentState.Mode.Normal
|
||||
)
|
||||
}
|
|
@ -6,6 +6,7 @@ package org.mozilla.fenix.tabtray
|
|||
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import org.junit.Test
|
||||
|
||||
class TabTrayFragmentInteractorTest {
|
||||
|
@ -38,8 +39,9 @@ class TabTrayFragmentInteractorTest {
|
|||
|
||||
@Test
|
||||
fun onSaveToCollectionClicked() {
|
||||
interactor.onSaveToCollectionClicked()
|
||||
verify { controller.onSaveToCollectionClicked() }
|
||||
val tab = Tab("1234", "mozilla.org")
|
||||
interactor.onSaveToCollectionClicked(setOf(tab))
|
||||
verify { controller.onSaveToCollectionClicked(setOf(tab)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -50,4 +52,43 @@ class TabTrayFragmentInteractorTest {
|
|||
interactor.onCloseAllTabsClicked(private = true)
|
||||
verify { controller.onCloseAllTabsClicked(true) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onBackPressed() {
|
||||
interactor.onBackPressed()
|
||||
verify { controller.handleBackPressed() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onModeRequested() {
|
||||
interactor.onModeRequested()
|
||||
verify { controller.onModeRequested() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onOpenTab() {
|
||||
val tab = Tab("1234", "mozilla.org")
|
||||
interactor.onOpenTab(tab)
|
||||
verify { controller.handleOpenTab(tab) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onAddSelectedTab() {
|
||||
val tab = Tab("1234", "mozilla.org")
|
||||
interactor.onAddSelectedTab(tab)
|
||||
verify { controller.handleAddSelectedTab(tab) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onRemoveSelectedTab() {
|
||||
val tab = Tab("1234", "mozilla.org")
|
||||
interactor.onRemoveSelectedTab(tab)
|
||||
verify { controller.handleRemoveSelectedTab(tab) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onEnterMultiselect() {
|
||||
interactor.onEnterMultiselect()
|
||||
verify { controller.handleEnterMultiselect() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,11 +109,10 @@ class TabTrayViewHolderTest {
|
|||
assertEquals("Pause", playPauseButtonView.contentDescription)
|
||||
}
|
||||
|
||||
private fun createViewHolder(getSelectedTabId: () -> String? = { null }) = TabTrayViewHolder(
|
||||
private fun createViewHolder() = TabTrayViewHolder(
|
||||
view,
|
||||
imageLoader = imageLoader,
|
||||
store = store,
|
||||
metrics = metrics,
|
||||
getSelectedTabId = getSelectedTabId
|
||||
metrics = metrics
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue