Close #18774: Migrate mutli-selection to store
Removes the recyclerview-selection library and replaces it with the SelectionHolder/SelectionInteractor with a Store. This is an implementation that's similar to what we have in other UI lists (library).
This commit is contained in:
parent
499aa858b2
commit
9078139e40
|
@ -533,7 +533,6 @@ dependencies {
|
|||
implementation Deps.androidx_navigation_fragment
|
||||
implementation Deps.androidx_navigation_ui
|
||||
implementation Deps.androidx_recyclerview
|
||||
implementation Deps.androidx_recyclerview_selection
|
||||
implementation Deps.androidx_lifecycle_livedata
|
||||
implementation Deps.androidx_lifecycle_runtime
|
||||
implementation Deps.androidx_lifecycle_viewmodel
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/* 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.tabstray
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
|
||||
|
||||
/**
|
||||
* Default tabs tray dialog implementation for overriding the default on back pressed.
|
||||
*/
|
||||
class TabsTrayDialog(
|
||||
context: Context,
|
||||
theme: Int,
|
||||
private val interactor: () -> BrowserTrayInteractor
|
||||
) : Dialog(context, theme) {
|
||||
override fun onBackPressed() {
|
||||
if (interactor.invoke().onBackPressed()) {
|
||||
return
|
||||
}
|
||||
|
||||
dismiss()
|
||||
}
|
||||
}
|
|
@ -19,7 +19,6 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|||
import com.google.android.material.tabs.TabLayout
|
||||
import kotlinx.android.synthetic.main.component_tabstray2.*
|
||||
import kotlinx.android.synthetic.main.component_tabstray2.view.*
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import mozilla.components.browser.state.selector.normalTabs
|
||||
import mozilla.components.lib.state.ext.consumeFrom
|
||||
|
@ -29,6 +28,8 @@ import org.mozilla.fenix.NavGraphDirections
|
|||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.components.StoreProvider
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.home.HomeScreenViewModel
|
||||
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
|
||||
|
@ -39,7 +40,6 @@ import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsInteractor
|
|||
|
||||
class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
|
||||
|
||||
private lateinit var behavior: BottomSheetBehavior<ConstraintLayout>
|
||||
private lateinit var navigationInteractor: NavigationInteractor
|
||||
|
||||
private val tabLayout: TabLayout? get() =
|
||||
|
@ -47,6 +47,9 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
|
|||
|
||||
private val isPrivateModeSelected: Boolean get() =
|
||||
tabLayout?.selectedTabPosition == TrayPagerAdapter.POSITION_PRIVATE_TABS
|
||||
private lateinit var tabsTrayStore: TabsTrayStore
|
||||
private lateinit var browserTrayInteractor: BrowserTrayInteractor
|
||||
private lateinit var behavior: BottomSheetBehavior<ConstraintLayout>
|
||||
|
||||
private val tabLayoutMediator = ViewBoundFeatureWrapper<TabLayoutMediator>()
|
||||
|
||||
|
@ -60,7 +63,8 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
|
|||
}
|
||||
|
||||
private val removeUseCases by lazy {
|
||||
RemoveTabUseCaseWrapper(requireComponents.analytics.metrics
|
||||
RemoveTabUseCaseWrapper(
|
||||
requireComponents.analytics.metrics
|
||||
) {
|
||||
tabRemoved(it)
|
||||
}
|
||||
|
@ -71,6 +75,9 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
|
|||
setStyle(STYLE_NO_TITLE, R.style.TabTrayDialogStyle)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?) =
|
||||
TabsTrayDialog(requireContext(), theme) { browserTrayInteractor }
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
@ -92,6 +99,8 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
|
|||
dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome
|
||||
)
|
||||
|
||||
tabsTrayStore = StoreProvider.get(this) { TabsTrayStore() }
|
||||
|
||||
return containerView
|
||||
}
|
||||
|
||||
|
@ -100,11 +109,12 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
setupMenu(view)
|
||||
|
||||
val browserTrayInteractor = DefaultBrowserTrayInteractor(
|
||||
this,
|
||||
browserTrayInteractor = DefaultBrowserTrayInteractor(
|
||||
tabsTrayStore,
|
||||
selectTabUseCase,
|
||||
removeUseCases,
|
||||
requireComponents.settings
|
||||
requireComponents.settings,
|
||||
this
|
||||
)
|
||||
|
||||
val syncedTabsTrayInteractor = SyncedTabsInteractor(
|
||||
|
@ -113,7 +123,13 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
|
|||
this
|
||||
)
|
||||
|
||||
setupPager(view.context, this, browserTrayInteractor, syncedTabsTrayInteractor)
|
||||
setupPager(
|
||||
view.context,
|
||||
tabsTrayStore,
|
||||
this,
|
||||
browserTrayInteractor,
|
||||
syncedTabsTrayInteractor
|
||||
)
|
||||
|
||||
tabLayoutMediator.set(
|
||||
feature = TabLayoutMediator(
|
||||
|
@ -160,6 +176,7 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
|
|||
|
||||
private fun setupPager(
|
||||
context: Context,
|
||||
store: TabsTrayStore,
|
||||
trayInteractor: TabsTrayInteractor,
|
||||
browserInteractor: BrowserTrayInteractor,
|
||||
syncedTabsTrayInteractor: SyncedTabsInteractor
|
||||
|
@ -167,9 +184,10 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
|
|||
tabsTray.apply {
|
||||
adapter = TrayPagerAdapter(
|
||||
context,
|
||||
trayInteractor,
|
||||
store,
|
||||
browserInteractor,
|
||||
syncedTabsTrayInteractor
|
||||
syncedTabsTrayInteractor,
|
||||
trayInteractor
|
||||
)
|
||||
isUserInputEnabled = false
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
package org.mozilla.fenix.tabstray
|
||||
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
|
@ -12,8 +11,8 @@ import android.widget.TextView
|
|||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.appcompat.widget.AppCompatImageButton
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.selection.ItemDetailsLookup
|
||||
import kotlinx.android.synthetic.main.checkbox_item.view.*
|
||||
import mozilla.components.browser.state.selector.findTabOrCustomTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
|
@ -36,6 +35,8 @@ import org.mozilla.fenix.ext.removeAndDisable
|
|||
import org.mozilla.fenix.ext.removeTouchDelegate
|
||||
import org.mozilla.fenix.ext.showAndEnable
|
||||
import org.mozilla.fenix.ext.toShortUrl
|
||||
import org.mozilla.fenix.selection.SelectionHolder
|
||||
import org.mozilla.fenix.selection.SelectionInteractor
|
||||
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
|
||||
|
||||
/**
|
||||
|
@ -45,7 +46,9 @@ abstract class TabsTrayViewHolder(
|
|||
itemView: View,
|
||||
private val imageLoader: ImageLoader,
|
||||
private val thumbnailSize: Int,
|
||||
private val browserTrayInteractor: BrowserTrayInteractor?,
|
||||
private val browserTrayInteractor: BrowserTrayInteractor,
|
||||
private val trayStore: TabsTrayStore,
|
||||
private val selectionHolder: SelectionHolder<Tab>?,
|
||||
private val store: BrowserStore = itemView.context.components.core.store,
|
||||
private val metrics: MetricController = itemView.context.components.analytics.metrics
|
||||
) : TabViewHolder(itemView) {
|
||||
|
@ -81,13 +84,63 @@ abstract class TabsTrayViewHolder(
|
|||
updateFavicon(tab)
|
||||
updateCloseButtonDescription(tab.title)
|
||||
updateSelectedTabIndicator(isSelected)
|
||||
updateMediaState(tab)
|
||||
|
||||
selectionHolder?.let {
|
||||
setSelectionInteractor(tab, it, browserTrayInteractor)
|
||||
}
|
||||
|
||||
if (tab.thumbnail != null) {
|
||||
thumbnailView.setImageBitmap(tab.thumbnail)
|
||||
} else {
|
||||
loadIntoThumbnailView(thumbnailView, tab.id)
|
||||
}
|
||||
}
|
||||
|
||||
fun showTabIsMultiSelectEnabled(isSelected: Boolean) {
|
||||
itemView.selected_mask.isVisible = isSelected
|
||||
closeView.isInvisible = trayStore.state.mode is TabsTrayState.Mode.Select
|
||||
}
|
||||
|
||||
private fun updateFavicon(tab: Tab) {
|
||||
if (tab.icon != null) {
|
||||
faviconView?.visibility = View.VISIBLE
|
||||
faviconView?.setImageBitmap(tab.icon)
|
||||
} else {
|
||||
faviconView?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTitle(tab: Tab) {
|
||||
val title = if (tab.title.isNotEmpty()) {
|
||||
tab.title
|
||||
} else {
|
||||
tab.url
|
||||
}
|
||||
titleView.text = title
|
||||
}
|
||||
|
||||
private fun updateUrl(tab: Tab) {
|
||||
// Truncate to MAX_URI_LENGTH to prevent the UI from locking up for
|
||||
// extremely large URLs such as data URIs or bookmarklets. The same
|
||||
// is done in the toolbar and awesomebar:
|
||||
// https://github.com/mozilla-mobile/fenix/issues/1824
|
||||
// https://github.com/mozilla-mobile/android-components/issues/6985
|
||||
urlView?.text = tab.url
|
||||
.toShortUrl(itemView.context.components.publicSuffixList)
|
||||
.take(MAX_URI_LENGTH)
|
||||
}
|
||||
|
||||
private fun updateCloseButtonDescription(title: String) {
|
||||
closeView.contentDescription =
|
||||
closeView.context.getString(R.string.close_tab_title, title)
|
||||
}
|
||||
|
||||
/**
|
||||
* NB: Why do we query for the media state from the store, when we have [Tab.playbackState] and
|
||||
* [Tab.controller] already mapped?
|
||||
*/
|
||||
private fun updateMediaState(tab: Tab) {
|
||||
// Media state
|
||||
playPauseButtonView.increaseTapArea(PLAY_PAUSE_BUTTON_EXTRA_DPS)
|
||||
|
||||
|
@ -136,65 +189,36 @@ abstract class TabsTrayViewHolder(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
closeView.setOnClickListener {
|
||||
observable.notifyObservers { onTabClosed(tab) }
|
||||
}
|
||||
}
|
||||
|
||||
fun getItemDetails() = object : ItemDetailsLookup.ItemDetails<Long>() {
|
||||
override fun getPosition(): Int = bindingAdapterPosition
|
||||
override fun getSelectionKey(): Long = itemId
|
||||
override fun inSelectionHotspot(e: MotionEvent): Boolean {
|
||||
return browserTrayInteractor?.isMultiSelectMode() == true
|
||||
}
|
||||
}
|
||||
|
||||
fun showTabIsMultiSelectEnabled(isSelected: Boolean) {
|
||||
itemView.selected_mask.isVisible = isSelected
|
||||
// TODO Enable this with https://github.com/mozilla-mobile/fenix/issues/18656
|
||||
// itemView.mozac_browser_tabstray_close.isVisible =
|
||||
// browserTrayInteractor?.isMultiSelectMode() == false
|
||||
}
|
||||
|
||||
private fun updateFavicon(tab: Tab) {
|
||||
if (tab.icon != null) {
|
||||
faviconView?.visibility = View.VISIBLE
|
||||
faviconView?.setImageBitmap(tab.icon)
|
||||
} else {
|
||||
faviconView?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTitle(tab: Tab) {
|
||||
val title = if (tab.title.isNotEmpty()) {
|
||||
tab.title
|
||||
} else {
|
||||
tab.url
|
||||
}
|
||||
titleView.text = title
|
||||
}
|
||||
|
||||
private fun updateUrl(tab: Tab) {
|
||||
// Truncate to MAX_URI_LENGTH to prevent the UI from locking up for
|
||||
// extremely large URLs such as data URIs or bookmarklets. The same
|
||||
// is done in the toolbar and awesomebar:
|
||||
// https://github.com/mozilla-mobile/fenix/issues/1824
|
||||
// https://github.com/mozilla-mobile/android-components/issues/6985
|
||||
urlView?.text = tab.url
|
||||
.toShortUrl(itemView.context.components.publicSuffixList)
|
||||
.take(MAX_URI_LENGTH)
|
||||
}
|
||||
|
||||
private fun updateCloseButtonDescription(title: String) {
|
||||
closeView.contentDescription =
|
||||
closeView.context.getString(R.string.close_tab_title, title)
|
||||
}
|
||||
|
||||
private fun loadIntoThumbnailView(thumbnailView: ImageView, id: String) {
|
||||
imageLoader.loadIntoView(thumbnailView, ImageLoadRequest(id, thumbnailSize))
|
||||
}
|
||||
|
||||
private fun setSelectionInteractor(
|
||||
item: Tab,
|
||||
holder: SelectionHolder<Tab>,
|
||||
interactor: SelectionInteractor<Tab>
|
||||
) {
|
||||
itemView.setOnClickListener {
|
||||
val selected = holder.selectedItems
|
||||
when {
|
||||
selected.isEmpty() -> interactor.open(item)
|
||||
item in selected -> interactor.deselect(item)
|
||||
else -> interactor.select(item)
|
||||
}
|
||||
}
|
||||
|
||||
itemView.setOnLongClickListener {
|
||||
if (holder.selectedItems.isEmpty()) {
|
||||
interactor.select(item)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal const val PLAY_PAUSE_BUTTON_EXTRA_DPS = 24
|
||||
internal const val GRID_ITEM_CLOSE_BUTTON_EXTRA_DPS = 24
|
||||
|
|
|
@ -18,14 +18,15 @@ import org.mozilla.fenix.sync.SyncedTabsAdapter
|
|||
import org.mozilla.fenix.tabstray.viewholders.SyncedTabViewHolder
|
||||
|
||||
class TrayPagerAdapter(
|
||||
val context: Context,
|
||||
val interactor: TabsTrayInteractor,
|
||||
val browserInteractor: BrowserTrayInteractor,
|
||||
val syncedTabsInteractor: SyncedTabsView.Listener
|
||||
private val context: Context,
|
||||
private val store: TabsTrayStore,
|
||||
private val browserInteractor: BrowserTrayInteractor,
|
||||
private val syncedTabsInteractor: SyncedTabsView.Listener,
|
||||
private val interactor: TabsTrayInteractor
|
||||
) : RecyclerView.Adapter<AbstractTrayViewHolder>() {
|
||||
|
||||
private val normalAdapter by lazy { BrowserTabsAdapter(context, browserInteractor) }
|
||||
private val privateAdapter by lazy { BrowserTabsAdapter(context, browserInteractor) }
|
||||
private val normalAdapter by lazy { BrowserTabsAdapter(context, browserInteractor, store) }
|
||||
private val privateAdapter by lazy { BrowserTabsAdapter(context, browserInteractor, store) }
|
||||
private val syncedTabsAdapter by lazy { SyncedTabsAdapter(syncedTabsInteractor) }
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractTrayViewHolder {
|
||||
|
@ -33,6 +34,7 @@ class TrayPagerAdapter(
|
|||
|
||||
return when (viewType) {
|
||||
NormalBrowserTabViewHolder.LAYOUT_ID -> NormalBrowserTabViewHolder(
|
||||
store,
|
||||
itemView,
|
||||
interactor
|
||||
)
|
||||
|
|
|
@ -6,16 +6,18 @@ package org.mozilla.fenix.tabstray.browser
|
|||
|
||||
import android.content.Context
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.selection.SelectionTracker
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import kotlinx.android.synthetic.main.tab_tray_item.view.*
|
||||
import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM
|
||||
import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_HIGHLIGHT_SELECTED_ITEM
|
||||
import mozilla.components.browser.thumbnails.loader.ThumbnailLoader
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import mozilla.components.concept.tabstray.TabsTray
|
||||
import mozilla.components.support.base.observer.Observable
|
||||
import mozilla.components.support.base.observer.ObserverRegistry
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.selection.SelectionHolder
|
||||
import org.mozilla.fenix.tabstray.TabsTrayStore
|
||||
import org.mozilla.fenix.tabstray.TabsTrayViewHolder
|
||||
|
||||
/**
|
||||
|
@ -24,7 +26,7 @@ import org.mozilla.fenix.tabstray.TabsTrayViewHolder
|
|||
class BrowserTabsAdapter(
|
||||
private val context: Context,
|
||||
private val interactor: BrowserTrayInteractor,
|
||||
private val layoutManager: (() -> GridLayoutManager)? = null,
|
||||
private val store: TabsTrayStore,
|
||||
delegate: Observable<TabsTray.Observer> = ObserverRegistry()
|
||||
) : TabsAdapter<TabsTrayViewHolder>(delegate) {
|
||||
|
||||
|
@ -39,12 +41,13 @@ class BrowserTabsAdapter(
|
|||
/**
|
||||
* Tracks the selected tabs in multi-select mode.
|
||||
*/
|
||||
var tracker: SelectionTracker<Long>? = null
|
||||
var selectionHolder: SelectionHolder<Tab>? = null
|
||||
|
||||
private val selectedItemAdapterBinding = SelectedItemAdapterBinding(store, this)
|
||||
private val imageLoader = ThumbnailLoader(context.components.core.thumbnailStorage)
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (context.settings().gridTabView) {
|
||||
return if (context.components.settings.gridTabView) {
|
||||
ViewType.GRID.ordinal
|
||||
} else {
|
||||
ViewType.LIST.ordinal
|
||||
|
@ -53,8 +56,20 @@ class BrowserTabsAdapter(
|
|||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabsTrayViewHolder {
|
||||
return when (viewType) {
|
||||
ViewType.GRID.ordinal -> TabsTrayGridViewHolder(parent, imageLoader, interactor)
|
||||
else -> TabsTrayListViewHolder(parent, imageLoader, interactor)
|
||||
ViewType.GRID.ordinal -> TabsTrayGridViewHolder(
|
||||
parent,
|
||||
imageLoader,
|
||||
interactor,
|
||||
store,
|
||||
selectionHolder
|
||||
)
|
||||
else -> TabsTrayListViewHolder(
|
||||
parent,
|
||||
imageLoader,
|
||||
interactor,
|
||||
store,
|
||||
selectionHolder
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,17 +77,48 @@ class BrowserTabsAdapter(
|
|||
super.onBindViewHolder(holder, position)
|
||||
|
||||
holder.tab?.let { tab ->
|
||||
holder.itemView.setOnClickListener {
|
||||
interactor.onOpenTab(tab)
|
||||
}
|
||||
|
||||
holder.itemView.mozac_browser_tabstray_close.setOnClickListener {
|
||||
interactor.onCloseTab(tab)
|
||||
interactor.close(tab)
|
||||
}
|
||||
|
||||
tracker?.let {
|
||||
holder.showTabIsMultiSelectEnabled(it.isSelected(getItemId(position)))
|
||||
selectionHolder?.let {
|
||||
holder.showTabIsMultiSelectEnabled(it.selectedItems.contains(tab))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Over-ridden [onBindViewHolder] that uses the payloads to notify the selected tab how to
|
||||
* display itself.
|
||||
*/
|
||||
override fun onBindViewHolder(holder: TabsTrayViewHolder, position: Int, payloads: List<Any>) {
|
||||
val tabs = tabs ?: return
|
||||
|
||||
if (tabs.list.isEmpty()) return
|
||||
|
||||
if (payloads.isEmpty()) {
|
||||
onBindViewHolder(holder, position)
|
||||
return
|
||||
}
|
||||
|
||||
if (position == tabs.selectedIndex) {
|
||||
if (payloads.contains(PAYLOAD_HIGHLIGHT_SELECTED_ITEM)) {
|
||||
holder.updateSelectedTabIndicator(true)
|
||||
} else if (payloads.contains(PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM)) {
|
||||
holder.updateSelectedTabIndicator(false)
|
||||
}
|
||||
}
|
||||
|
||||
selectionHolder?.let {
|
||||
holder.showTabIsMultiSelectEnabled(it.selectedItems.contains(holder.tab))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
selectedItemAdapterBinding.start()
|
||||
}
|
||||
|
||||
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
|
||||
selectedItemAdapterBinding.start()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,31 +9,26 @@ import androidx.recyclerview.widget.GridLayoutManager
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
import mozilla.components.support.base.feature.UserInteractionHandler
|
||||
import org.mozilla.fenix.selection.SelectionInteractor
|
||||
import org.mozilla.fenix.tabstray.TabsTrayAction
|
||||
import org.mozilla.fenix.tabstray.TabsTrayInteractor
|
||||
import org.mozilla.fenix.tabstray.TrayPagerAdapter
|
||||
import org.mozilla.fenix.tabstray.ext.numberOfGridColumns
|
||||
import org.mozilla.fenix.utils.Settings
|
||||
import org.mozilla.fenix.tabstray.TabsTrayState.Mode
|
||||
import org.mozilla.fenix.tabstray.TabsTrayStore
|
||||
|
||||
/**
|
||||
* For interacting with UI that is specifically for [BaseBrowserTrayList] and other browser
|
||||
* tab tray views.
|
||||
*/
|
||||
interface BrowserTrayInteractor {
|
||||
|
||||
/**
|
||||
* Select the tab.
|
||||
*/
|
||||
fun onOpenTab(tab: Tab)
|
||||
interface BrowserTrayInteractor : SelectionInteractor<Tab>, UserInteractionHandler {
|
||||
|
||||
/**
|
||||
* Close the tab.
|
||||
*/
|
||||
fun onCloseTab(tab: Tab)
|
||||
|
||||
/**
|
||||
* If multi-select mode is enabled or disabled.
|
||||
*/
|
||||
fun isMultiSelectMode(): Boolean
|
||||
fun close(tab: Tab)
|
||||
|
||||
/**
|
||||
* Returns the appropriate [RecyclerView.LayoutManager] to be used at [position].
|
||||
|
@ -45,32 +40,52 @@ interface BrowserTrayInteractor {
|
|||
* A default implementation of [BrowserTrayInteractor].
|
||||
*/
|
||||
class DefaultBrowserTrayInteractor(
|
||||
private val trayInteractor: TabsTrayInteractor,
|
||||
private val store: TabsTrayStore,
|
||||
private val selectTabUseCase: TabsUseCases.SelectTabUseCase,
|
||||
private val removeUseCases: TabsUseCases.RemoveTabUseCase,
|
||||
private val settings: Settings
|
||||
private val settings: Settings,
|
||||
private val trayInteractor: TabsTrayInteractor
|
||||
) : BrowserTrayInteractor {
|
||||
|
||||
/**
|
||||
* See [BrowserTrayInteractor.onOpenTab].
|
||||
* See [SelectionInteractor.open]
|
||||
*/
|
||||
override fun onOpenTab(tab: Tab) {
|
||||
selectTabUseCase.invoke(tab.id)
|
||||
override fun open(item: Tab) {
|
||||
selectTabUseCase.invoke(item.id)
|
||||
trayInteractor.navigateToBrowser()
|
||||
}
|
||||
|
||||
/**
|
||||
* See [BrowserTrayInteractor.onCloseTab].
|
||||
* See [BrowserTrayInteractor.close].
|
||||
*/
|
||||
override fun onCloseTab(tab: Tab) {
|
||||
override fun close(tab: Tab) {
|
||||
removeUseCases.invoke(tab.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* See [BrowserTrayInteractor.isMultiSelectMode].
|
||||
* See [SelectionInteractor.select]
|
||||
*/
|
||||
override fun isMultiSelectMode(): Boolean {
|
||||
// Needs https://github.com/mozilla-mobile/fenix/issues/18513 to change this value
|
||||
override fun select(item: Tab) {
|
||||
store.dispatch(TabsTrayAction.AddSelectTab(item))
|
||||
}
|
||||
|
||||
/**
|
||||
* See [SelectionInteractor.deselect]
|
||||
*/
|
||||
override fun deselect(item: Tab) {
|
||||
store.dispatch(TabsTrayAction.RemoveSelectTab(item))
|
||||
}
|
||||
|
||||
/**
|
||||
* See [UserInteractionHandler.onBackPressed]
|
||||
*
|
||||
* TODO move this to the navigation interactor when it lands.
|
||||
*/
|
||||
override fun onBackPressed(): Boolean {
|
||||
if (store.state.mode is Mode.Select) {
|
||||
store.dispatch(TabsTrayAction.ExitSelectMode)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/* 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.tabstray.browser
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.map
|
||||
import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM
|
||||
import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_HIGHLIGHT_SELECTED_ITEM
|
||||
import mozilla.components.lib.state.ext.flowScoped
|
||||
import mozilla.components.support.base.feature.LifecycleAwareFeature
|
||||
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
|
||||
import org.mozilla.fenix.tabstray.TabsTrayState.Mode
|
||||
import org.mozilla.fenix.tabstray.TabsTrayStore
|
||||
|
||||
/**
|
||||
* Notifies the adapter when the selection mode changes.
|
||||
*/
|
||||
class SelectedItemAdapterBinding(
|
||||
val store: TabsTrayStore,
|
||||
val adapter: BrowserTabsAdapter
|
||||
) : LifecycleAwareFeature {
|
||||
private var scope: CoroutineScope? = null
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun start() {
|
||||
scope = store.flowScoped { flow ->
|
||||
flow.map { it.mode }
|
||||
// ignore initial mode update; the adapter is already in an updated state.
|
||||
.drop(1)
|
||||
.ifChanged()
|
||||
.collect { mode ->
|
||||
notifyAdapter(mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
scope?.cancel()
|
||||
}
|
||||
|
||||
private fun notifyAdapter(mode: Mode) = with(adapter) {
|
||||
if (mode == Mode.Normal) {
|
||||
notifyItemRangeChanged(0, itemCount, PAYLOAD_HIGHLIGHT_SELECTED_ITEM)
|
||||
} else {
|
||||
notifyItemRangeChanged(0, itemCount, PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.tabstray.browser
|
||||
|
||||
import android.util.LruCache
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
|
||||
internal const val INITIAL_NUMBER_OF_TABS = 20
|
||||
internal const val CACHE_SIZE_MULTIPLIER = 1.5
|
||||
|
||||
/**
|
||||
* Storage for Browser tabs that need a stable ID for each item in a [RecyclerView.Adapter].
|
||||
* This ID is commonly needed by [RecyclerView.Adapter.getItemId] when
|
||||
* enabling [RecyclerView.Adapter.setHasStableIds].
|
||||
*/
|
||||
internal class TabAdapterIdStorage(initialSize: Int = INITIAL_NUMBER_OF_TABS) {
|
||||
private val uniqueTabIds = LruCache<String, Long>(initialSize)
|
||||
private var lastUsedSuggestionId = 0L
|
||||
|
||||
/**
|
||||
* Returns a unique tab ID for the given [Tab].
|
||||
*/
|
||||
fun getStableId(tab: Tab): Long {
|
||||
val key = tab.id
|
||||
return uniqueTabIds[key] ?: run {
|
||||
lastUsedSuggestionId += 1
|
||||
uniqueTabIds.put(key, lastUsedSuggestionId)
|
||||
lastUsedSuggestionId
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes the internal cache size if the [count] is larger than what is currently available.
|
||||
*/
|
||||
fun resizeCacheIfNeeded(count: Int) {
|
||||
val currentMaxSize = uniqueTabIds.maxSize()
|
||||
if (count > currentMaxSize) {
|
||||
val newMaxSize = (count * CACHE_SIZE_MULTIPLIER).toInt()
|
||||
uniqueTabIds.resize(newMaxSize)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -31,18 +31,10 @@ abstract class TabsAdapter<T : TabViewHolder>(
|
|||
protected var tabs: Tabs? = null
|
||||
protected var styling: TabsTrayStyling = TabsTrayStyling()
|
||||
|
||||
private val idStorage = TabAdapterIdStorage()
|
||||
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun updateTabs(tabs: Tabs) {
|
||||
this.tabs = tabs
|
||||
|
||||
idStorage.resizeCacheIfNeeded(tabs.list.size)
|
||||
|
||||
notifyObservers { onTabsUpdated() }
|
||||
}
|
||||
|
||||
|
@ -53,13 +45,6 @@ abstract class TabsAdapter<T : TabViewHolder>(
|
|||
holder.bind(tabs.list[position], isTabSelected(tabs, position), styling, this)
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
val key = tabs?.list?.get(position)
|
||||
?: throw IllegalStateException("Unknown tab for position $position")
|
||||
|
||||
return idStorage.getStableId(key)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = tabs?.list?.size ?: 0
|
||||
|
||||
final override fun isTabSelected(tabs: Tabs, position: Int): Boolean =
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.tabstray.browser
|
||||
|
||||
import android.view.MotionEvent
|
||||
import androidx.recyclerview.selection.ItemDetailsLookup
|
||||
import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.mozilla.fenix.tabstray.TabsTrayViewHolder
|
||||
|
||||
/**
|
||||
* An [ItemDetailsLookup] for retrieving the [ItemDetails] of a [TabsTrayViewHolder].
|
||||
*/
|
||||
class TabsDetailsLookup(
|
||||
private val recyclerView: RecyclerView
|
||||
) : ItemDetailsLookup<Long>() {
|
||||
|
||||
override fun getItemDetails(event: MotionEvent): ItemDetails<Long>? {
|
||||
val view = recyclerView.findChildViewUnder(event.x, event.y)
|
||||
if (view != null) {
|
||||
val viewHolder = recyclerView.getChildViewHolder(view) as TabsTrayViewHolder
|
||||
return viewHolder.getItemDetails()
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.tabstray.browser
|
||||
|
||||
import androidx.recyclerview.selection.ItemKeyProvider
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
/**
|
||||
* A key provider for the browser tabs.
|
||||
*/
|
||||
class TabsItemKeyProvider(private val recyclerView: RecyclerView) :
|
||||
ItemKeyProvider<Long>(SCOPE_MAPPED) {
|
||||
|
||||
override fun getKey(position: Int): Long? {
|
||||
return recyclerView.adapter?.getItemId(position)
|
||||
}
|
||||
|
||||
override fun getPosition(key: Long): Int {
|
||||
val viewHolder = recyclerView.findViewHolderForItemId(key)
|
||||
return viewHolder?.layoutPosition ?: RecyclerView.NO_POSITION
|
||||
}
|
||||
}
|
|
@ -19,6 +19,9 @@ import org.mozilla.fenix.ext.increaseTapArea
|
|||
import kotlin.math.max
|
||||
import kotlinx.android.synthetic.main.tab_tray_grid_item.view.tab_tray_grid_item
|
||||
import org.mozilla.fenix.tabstray.TabsTrayViewHolder
|
||||
import org.mozilla.fenix.selection.SelectionHolder
|
||||
import org.mozilla.fenix.tabstray.TabsTrayStore
|
||||
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
|
||||
|
||||
/**
|
||||
* A RecyclerView ViewHolder implementation for "tab" items with grid layout.
|
||||
|
@ -26,7 +29,9 @@ import org.mozilla.fenix.tabstray.TabsTrayViewHolder
|
|||
class TabsTrayGridViewHolder(
|
||||
parent: ViewGroup,
|
||||
imageLoader: ImageLoader,
|
||||
browserTrayInteractor: BrowserTrayInteractor? = null,
|
||||
browserTrayInteractor: BrowserTrayInteractor,
|
||||
store: TabsTrayStore,
|
||||
selectionHolder: SelectionHolder<Tab>? = null,
|
||||
itemView: View =
|
||||
LayoutInflater.from(parent.context).inflate(R.layout.tab_tray_grid_item, parent, false),
|
||||
thumbnailSize: Int =
|
||||
|
@ -34,7 +39,14 @@ class TabsTrayGridViewHolder(
|
|||
itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_grid_item_thumbnail_height),
|
||||
itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_grid_item_thumbnail_width)
|
||||
)
|
||||
) : TabsTrayViewHolder(itemView, imageLoader, thumbnailSize, browserTrayInteractor) {
|
||||
) : TabsTrayViewHolder(
|
||||
itemView,
|
||||
imageLoader,
|
||||
thumbnailSize,
|
||||
browserTrayInteractor,
|
||||
store,
|
||||
selectionHolder
|
||||
) {
|
||||
|
||||
private val closeButton: AppCompatImageButton = itemView.findViewById(R.id.mozac_browser_tabstray_close)
|
||||
|
||||
|
|
|
@ -9,8 +9,12 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import mozilla.components.concept.base.images.ImageLoader
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.tabstray.TabsTrayViewHolder
|
||||
import org.mozilla.fenix.selection.SelectionHolder
|
||||
import org.mozilla.fenix.tabstray.TabsTrayStore
|
||||
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
|
@ -19,7 +23,9 @@ import kotlin.math.max
|
|||
class TabsTrayListViewHolder(
|
||||
parent: ViewGroup,
|
||||
imageLoader: ImageLoader,
|
||||
browserTrayInteractor: BrowserTrayInteractor? = null,
|
||||
browserTrayInteractor: BrowserTrayInteractor,
|
||||
store: TabsTrayStore,
|
||||
selectionHolder: SelectionHolder<Tab>? = null,
|
||||
itemView: View =
|
||||
LayoutInflater.from(parent.context).inflate(R.layout.tab_tray_item, parent, false),
|
||||
thumbnailSize: Int =
|
||||
|
@ -27,7 +33,14 @@ class TabsTrayListViewHolder(
|
|||
itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_list_item_thumbnail_height),
|
||||
itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_list_item_thumbnail_width)
|
||||
)
|
||||
) : TabsTrayViewHolder(itemView, imageLoader, thumbnailSize, browserTrayInteractor) {
|
||||
) : TabsTrayViewHolder(
|
||||
itemView,
|
||||
imageLoader,
|
||||
thumbnailSize,
|
||||
browserTrayInteractor,
|
||||
store,
|
||||
selectionHolder
|
||||
) {
|
||||
|
||||
override fun updateSelectedTabIndicator(showAsSelected: Boolean) {
|
||||
val color = if (showAsSelected) {
|
||||
|
|
|
@ -5,49 +5,39 @@
|
|||
package org.mozilla.fenix.tabstray.viewholders
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.selection.SelectionPredicates
|
||||
import androidx.recyclerview.selection.SelectionTracker
|
||||
import androidx.recyclerview.selection.StorageStrategy
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.selection.SelectionHolder
|
||||
import org.mozilla.fenix.tabstray.TabsTrayInteractor
|
||||
import org.mozilla.fenix.tabstray.TabsTrayStore
|
||||
import org.mozilla.fenix.tabstray.browser.BrowserTabsAdapter
|
||||
import org.mozilla.fenix.tabstray.browser.TabsDetailsLookup
|
||||
import org.mozilla.fenix.tabstray.browser.TabsItemKeyProvider
|
||||
|
||||
/**
|
||||
* View holder for the normal tabs tray list.
|
||||
*/
|
||||
class NormalBrowserTabViewHolder(
|
||||
private val store: TabsTrayStore,
|
||||
containerView: View,
|
||||
interactor: TabsTrayInteractor
|
||||
) : BaseBrowserTabViewHolder(containerView, interactor) {
|
||||
) : BaseBrowserTabViewHolder(containerView, interactor), SelectionHolder<Tab> {
|
||||
|
||||
private lateinit var selectionTracker: SelectionTracker<Long>
|
||||
/**
|
||||
* Holds the list of selected tabs.
|
||||
*
|
||||
* Implementation notes: we do this here because we only want the normal tabs list to be able
|
||||
* to select tabs.
|
||||
*/
|
||||
override val selectedItems: Set<Tab>
|
||||
get() = store.state.mode.selectedTabs
|
||||
|
||||
override fun bind(
|
||||
adapter: RecyclerView.Adapter<out RecyclerView.ViewHolder>,
|
||||
layoutManager: RecyclerView.LayoutManager
|
||||
) {
|
||||
(adapter as BrowserTabsAdapter).selectionHolder = this
|
||||
|
||||
super.bind(adapter, layoutManager)
|
||||
|
||||
selectionTracker = SelectionTracker.Builder(
|
||||
"mySelection",
|
||||
trayList,
|
||||
TabsItemKeyProvider(trayList),
|
||||
TabsDetailsLookup(trayList),
|
||||
StorageStrategy.createLongStorage()
|
||||
).withSelectionPredicate(
|
||||
SelectionPredicates.createSelectAnything()
|
||||
).build()
|
||||
|
||||
(adapter as BrowserTabsAdapter).tracker = selectionTracker
|
||||
|
||||
selectionTracker.addObserver(object : SelectionTracker.SelectionObserver<Long>() {
|
||||
override fun onItemStateChanged(key: Long, selected: Boolean) {
|
||||
// TODO Do nothing for now; remove in a future patch if needed.
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
package org.mozilla.fenix.tabtray
|
||||
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
|
@ -13,12 +12,7 @@ import androidx.annotation.VisibleForTesting
|
|||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.appcompat.widget.AppCompatImageButton
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.selection.ItemDetailsLookup
|
||||
import kotlinx.android.synthetic.main.checkbox_item.view.*
|
||||
import kotlinx.android.synthetic.main.tab_tray_grid_item.view.*
|
||||
import kotlinx.android.synthetic.main.tab_tray_grid_item.view.mozac_browser_tabstray_close
|
||||
import kotlinx.android.synthetic.main.tab_tray_item.view.*
|
||||
import mozilla.components.browser.state.selector.findTabOrCustomTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.browser.tabstray.TabViewHolder
|
||||
|
@ -41,7 +35,6 @@ import org.mozilla.fenix.ext.removeTouchDelegate
|
|||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.ext.showAndEnable
|
||||
import org.mozilla.fenix.ext.toShortUrl
|
||||
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
|
@ -51,8 +44,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,
|
||||
private val browserTrayInteractor: BrowserTrayInteractor? = null
|
||||
private val metrics: MetricController = itemView.context.components.analytics.metrics
|
||||
) : TabViewHolder(itemView) {
|
||||
|
||||
private val faviconView: ImageView? =
|
||||
|
@ -204,20 +196,6 @@ class TabTrayViewHolder(
|
|||
)
|
||||
}
|
||||
|
||||
fun getItemDetails() = object : ItemDetailsLookup.ItemDetails<Long>() {
|
||||
override fun getPosition(): Int = bindingAdapterPosition
|
||||
override fun getSelectionKey(): Long = itemId
|
||||
override fun inSelectionHotspot(e: MotionEvent): Boolean {
|
||||
return browserTrayInteractor?.isMultiSelectMode() == true
|
||||
}
|
||||
}
|
||||
|
||||
fun showTabIsMultiSelectEnabled(isSelected: Boolean) {
|
||||
itemView.selected_mask.isVisible = isSelected
|
||||
itemView.mozac_browser_tabstray_close.isVisible =
|
||||
browserTrayInteractor?.isMultiSelectMode() == false
|
||||
}
|
||||
|
||||
private fun updateCloseButtonDescription(title: String) {
|
||||
closeView.contentDescription =
|
||||
closeView.context.getString(R.string.close_tab_title, title)
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/* 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.tabstray
|
||||
|
||||
import android.content.Context
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
|
||||
|
||||
class TabsTrayDialogTest {
|
||||
@Test
|
||||
fun `WHEN onBackPressed THEN invoke interactor`() {
|
||||
val context = mockk<Context>(relaxed = true)
|
||||
val interactor = mockk<BrowserTrayInteractor>(relaxed = true)
|
||||
val dialog = TabsTrayDialog(context, 0) { interactor }
|
||||
|
||||
dialog.onBackPressed()
|
||||
|
||||
verify { interactor.onBackPressed() }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
/* 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.tabstray.browser
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM
|
||||
import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_HIGHLIGHT_SELECTED_ITEM
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import mozilla.components.concept.tabstray.Tabs
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.selection.SelectionHolder
|
||||
import org.mozilla.fenix.tabstray.TabsTrayStore
|
||||
import org.mozilla.fenix.tabstray.TabsTrayViewHolder
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class BrowserTabsAdapterTest {
|
||||
|
||||
private val context = testContext
|
||||
private val interactor = mockk<BrowserTrayInteractor>(relaxed = true)
|
||||
private val store = TabsTrayStore()
|
||||
|
||||
@Test
|
||||
fun `WHEN bind with payloads is called THEN update the holder`() {
|
||||
val adapter = BrowserTabsAdapter(context, interactor, store)
|
||||
val holder = mockk<TabsTrayViewHolder>(relaxed = true)
|
||||
|
||||
adapter.updateTabs(Tabs(
|
||||
list = listOf(
|
||||
createTab("tab1")
|
||||
),
|
||||
selectedIndex = 0
|
||||
))
|
||||
|
||||
adapter.onBindViewHolder(holder, 0, listOf(PAYLOAD_HIGHLIGHT_SELECTED_ITEM))
|
||||
|
||||
verify { holder.updateSelectedTabIndicator(true) }
|
||||
|
||||
adapter.onBindViewHolder(holder, 0, listOf(PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM))
|
||||
|
||||
verify { holder.updateSelectedTabIndicator(false) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN the selection holder is set THEN update the selected tab`() {
|
||||
val adapter = BrowserTabsAdapter(context, interactor, store)
|
||||
val holder = mockk<TabsTrayViewHolder>(relaxed = true)
|
||||
val tab = createTab("tab1")
|
||||
|
||||
every { holder.tab }.answers { tab }
|
||||
testSelectionHolder.internalState.add(tab)
|
||||
adapter.selectionHolder = testSelectionHolder
|
||||
|
||||
adapter.updateTabs(Tabs(
|
||||
list = listOf(
|
||||
tab
|
||||
),
|
||||
selectedIndex = 0
|
||||
))
|
||||
|
||||
adapter.onBindViewHolder(holder, 0, listOf(PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM))
|
||||
|
||||
verify { holder.showTabIsMultiSelectEnabled(true) }
|
||||
}
|
||||
|
||||
private val testSelectionHolder = object : SelectionHolder<Tab> {
|
||||
override val selectedItems: Set<Tab>
|
||||
get() = internalState
|
||||
|
||||
val internalState = mutableSetOf<Tab>()
|
||||
}
|
||||
}
|
|
@ -32,7 +32,7 @@ class DefaultBrowserTrayInteractorTest {
|
|||
|
||||
@Test
|
||||
fun `WHEN pager position is synced tabs THEN return a list layout manager`() {
|
||||
val interactor = DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), mockk())
|
||||
val interactor = DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), mockk(), mockk())
|
||||
|
||||
val result = interactor.getLayoutManagerForPosition(
|
||||
mockk(),
|
||||
|
@ -46,7 +46,7 @@ class DefaultBrowserTrayInteractorTest {
|
|||
fun `WHEN setting is grid view THEN return grid layout manager`() {
|
||||
val context = mockk<Context>()
|
||||
val settings = mockk<Settings>()
|
||||
val interactor = DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), settings)
|
||||
val interactor = DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), settings, mockk())
|
||||
|
||||
every { context.numberOfGridColumns }.answers { 4 }
|
||||
every { settings.gridTabView }.answers { true }
|
||||
|
@ -63,7 +63,7 @@ class DefaultBrowserTrayInteractorTest {
|
|||
fun `WHEN setting is list view THEN return list layout manager`() {
|
||||
val context = mockk<Context>()
|
||||
val settings = mockk<Settings>()
|
||||
val interactor = DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), settings)
|
||||
val interactor = DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), settings, mockk())
|
||||
|
||||
every { context.numberOfGridColumns }.answers { 4 }
|
||||
every { settings.gridTabView }.answers { false }
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/* 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.tabstray.browser
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestCoroutineDispatcher
|
||||
import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM
|
||||
import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_HIGHLIGHT_SELECTED_ITEM
|
||||
import mozilla.components.support.test.libstate.ext.waitUntilIdle
|
||||
import mozilla.components.support.test.rule.MainCoroutineRule
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.tabstray.TabsTrayAction
|
||||
import org.mozilla.fenix.tabstray.TabsTrayStore
|
||||
|
||||
class SelectedItemAdapterBindingTest {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@get:Rule
|
||||
val coroutinesTestRule = MainCoroutineRule(TestCoroutineDispatcher())
|
||||
|
||||
private val adapter = mockk<BrowserTabsAdapter>(relaxed = true)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
every { adapter.itemCount }.answers { 1 }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN observing on start THEN ignore the initial state update`() {
|
||||
val store = TabsTrayStore()
|
||||
val binding = SelectedItemAdapterBinding(store, adapter)
|
||||
|
||||
binding.start()
|
||||
|
||||
verify(exactly = 0) {
|
||||
adapter.notifyItemRangeChanged(any(), any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN mode changes THEN notify the adapter`() {
|
||||
val store = TabsTrayStore()
|
||||
val binding = SelectedItemAdapterBinding(store, adapter)
|
||||
|
||||
binding.start()
|
||||
|
||||
store.dispatch(TabsTrayAction.EnterSelectMode)
|
||||
|
||||
store.waitUntilIdle()
|
||||
|
||||
verify {
|
||||
adapter.notifyItemRangeChanged(eq(0), eq(1), eq(PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM))
|
||||
}
|
||||
|
||||
store.dispatch(TabsTrayAction.ExitSelectMode)
|
||||
|
||||
store.waitUntilIdle()
|
||||
|
||||
verify {
|
||||
adapter.notifyItemRangeChanged(eq(0), eq(1), eq(PAYLOAD_HIGHLIGHT_SELECTED_ITEM))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.tabstray.browser
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class TabAdapterIdStorageTest {
|
||||
|
||||
@Test
|
||||
fun `the same ID is returned when queried multiple times`() {
|
||||
val storage = TabAdapterIdStorage()
|
||||
val tab = createTab()
|
||||
|
||||
val id1 = storage.getStableId(tab)
|
||||
val id2 = storage.getStableId(tab)
|
||||
|
||||
assertEquals(id1, id2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the same ID is returned when the cache is at max`() {
|
||||
val storage = TabAdapterIdStorage(2)
|
||||
val tab1 = createTab()
|
||||
val tab2 = createTab()
|
||||
|
||||
val id1 = storage.getStableId(tab1)
|
||||
val id2 = storage.getStableId(tab2)
|
||||
val id1Again = storage.getStableId(tab1)
|
||||
|
||||
assertEquals(id1, id1Again)
|
||||
assertNotEquals(id1, id2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the same ID is NOT returned if the cache is over max`() {
|
||||
val storage = TabAdapterIdStorage(2)
|
||||
val tab1 = createTab()
|
||||
val tab2 = createTab()
|
||||
val tab3 = createTab()
|
||||
|
||||
val id1 = storage.getStableId(tab1)
|
||||
val id2 = storage.getStableId(tab2)
|
||||
val id3 = storage.getStableId(tab3)
|
||||
val id1Again = storage.getStableId(tab1)
|
||||
|
||||
assertNotEquals(id1, id1Again)
|
||||
assertNotEquals(id1, id2)
|
||||
assertNotEquals(id1, id3)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the same ID is returned if the cache is resized when full`() {
|
||||
val storage = TabAdapterIdStorage(2)
|
||||
val tab1 = createTab()
|
||||
val tab2 = createTab()
|
||||
val tab3 = createTab()
|
||||
|
||||
val id1 = storage.getStableId(tab1)
|
||||
val id2 = storage.getStableId(tab2)
|
||||
|
||||
storage.resizeCacheIfNeeded(3)
|
||||
|
||||
val id3 = storage.getStableId(tab3)
|
||||
val id1Again = storage.getStableId(tab1)
|
||||
|
||||
assertEquals(id1, id1Again)
|
||||
assertNotEquals(id1, id2)
|
||||
assertNotEquals(id1, id3)
|
||||
assertNotEquals(id2, id3)
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.tabstray.browser
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import mozilla.components.browser.tabstray.TabViewHolder
|
||||
import mozilla.components.browser.tabstray.TabsTrayStyling
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import mozilla.components.concept.tabstray.Tabs
|
||||
import mozilla.components.concept.tabstray.TabsTray
|
||||
import mozilla.components.support.base.observer.Observable
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class TabsAdapterTest {
|
||||
|
||||
lateinit var adapter: TabsAdapter<TestTabsAdapter.ViewHolder>
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
adapter = TestTabsAdapter()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getItemId gives a new ID for each position`() {
|
||||
val (tab1, tab2, tab3) = Triple(createTab(), createTab(), createTab())
|
||||
val tabs = Tabs(
|
||||
list = listOf(tab1, tab2, tab3),
|
||||
selectedIndex = 0
|
||||
)
|
||||
|
||||
adapter.updateTabs(tabs)
|
||||
|
||||
val id1 = adapter.getItemId(0)
|
||||
val id2 = adapter.getItemId(1)
|
||||
val id3 = adapter.getItemId(2)
|
||||
val id1Again = adapter.getItemId(0)
|
||||
|
||||
assertEquals(id1, id1Again)
|
||||
assertNotEquals(id1, id2)
|
||||
assertNotEquals(id1, id3)
|
||||
assertNotEquals(id2, id3)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun `getItemId throws if a tab does not exist for the position`() {
|
||||
adapter.getItemId(4)
|
||||
}
|
||||
|
||||
class TestTabsAdapter : TabsAdapter<TestTabsAdapter.ViewHolder>() {
|
||||
|
||||
inner class ViewHolder(view: View) : TabViewHolder(view) {
|
||||
override var tab: Tab? = null
|
||||
|
||||
override fun bind(
|
||||
tab: Tab,
|
||||
isSelected: Boolean,
|
||||
styling: TabsTrayStyling,
|
||||
observable: Observable<TabsTray.Observer>
|
||||
) = throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): ViewHolder = throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
|
@ -29,7 +29,6 @@ object Versions {
|
|||
const val androidx_fragment = "1.2.5"
|
||||
const val androidx_navigation = "2.3.3"
|
||||
const val androidx_recyclerview = "1.2.0-beta01"
|
||||
const val androidx_recyclerview_selection = "1.0.0"
|
||||
const val androidx_core = "1.3.2"
|
||||
const val androidx_paging = "2.1.2"
|
||||
const val androidx_transition = "1.4.0"
|
||||
|
@ -186,7 +185,6 @@ object Deps {
|
|||
const val androidx_navigation_fragment = "androidx.navigation:navigation-fragment-ktx:${Versions.androidx_navigation}"
|
||||
const val androidx_navigation_ui = "androidx.navigation:navigation-ui:${Versions.androidx_navigation}"
|
||||
const val androidx_recyclerview = "androidx.recyclerview:recyclerview:${Versions.androidx_recyclerview}"
|
||||
const val androidx_recyclerview_selection = "androidx.recyclerview:recyclerview-selection:${Versions.androidx_recyclerview_selection}"
|
||||
const val androidx_core = "androidx.core:core:${Versions.androidx_core}"
|
||||
const val androidx_core_ktx = "androidx.core:core-ktx:${Versions.androidx_core}"
|
||||
const val androidx_transition = "androidx.transition:transition:${Versions.androidx_transition}"
|
||||
|
|
Loading…
Reference in New Issue
Block a user