diff --git a/app/build.gradle b/app/build.gradle index 1c8c4be88..da5d7bfdf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayDialog.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayDialog.kt new file mode 100644 index 000000000..18e9af8f3 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayDialog.kt @@ -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() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt index e619aab58..ad942ace9 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt @@ -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 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 private val tabLayoutMediator = ViewBoundFeatureWrapper() @@ -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 } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayViewHolder.kt index f8186a38e..38a0b2c98 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayViewHolder.kt @@ -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?, 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,63 +189,34 @@ abstract class TabsTrayViewHolder( } } } - - closeView.setOnClickListener { - observable.notifyObservers { onTabClosed(tab) } - } - } - - fun getItemDetails() = object : ItemDetailsLookup.ItemDetails() { - 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 loadIntoThumbnailView(thumbnailView: ImageView, id: String) { + imageLoader.loadIntoView(thumbnailView, ImageLoadRequest(id, thumbnailSize)) } - private fun updateFavicon(tab: Tab) { - if (tab.icon != null) { - faviconView?.visibility = View.VISIBLE - faviconView?.setImageBitmap(tab.icon) - } else { - faviconView?.visibility = View.GONE + private fun setSelectionInteractor( + item: Tab, + holder: SelectionHolder, + interactor: SelectionInteractor + ) { + itemView.setOnClickListener { + val selected = holder.selectedItems + when { + selected.isEmpty() -> interactor.open(item) + item in selected -> interactor.deselect(item) + else -> interactor.select(item) + } } - } - private fun updateTitle(tab: Tab) { - val title = if (tab.title.isNotEmpty()) { - tab.title - } else { - tab.url + itemView.setOnLongClickListener { + if (holder.selectedItems.isEmpty()) { + interactor.select(item) + true + } else { + false + } } - 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)) } companion object { diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt index 0249ceb25..f106475a7 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt @@ -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() { - 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 ) diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapter.kt index 3eb31fba6..f78ae3688 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapter.kt @@ -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 = ObserverRegistry() ) : TabsAdapter(delegate) { @@ -39,12 +41,13 @@ class BrowserTabsAdapter( /** * Tracks the selected tabs in multi-select mode. */ - var tracker: SelectionTracker? = null + var selectionHolder: SelectionHolder? = 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.close(tab) } - holder.itemView.mozac_browser_tabstray_close.setOnClickListener { - interactor.onCloseTab(tab) + 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) { + val tabs = tabs ?: return + + if (tabs.list.isEmpty()) return - tracker?.let { - holder.showTabIsMultiSelectEnabled(it.isSelected(getItemId(position))) + 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() } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTrayInteractor.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTrayInteractor.kt index 20b65e223..8596347c9 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTrayInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTrayInteractor.kt @@ -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, 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 } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectedItemAdapterBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectedItemAdapterBinding.kt new file mode 100644 index 000000000..a725364dc --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectedItemAdapterBinding.kt @@ -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) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabAdapterIdStorage.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabAdapterIdStorage.kt deleted file mode 100644 index e1760debe..000000000 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabAdapterIdStorage.kt +++ /dev/null @@ -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(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) - } - } -} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsAdapter.kt index b0f1a1430..8e4820cd1 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsAdapter.kt @@ -31,18 +31,10 @@ abstract class TabsAdapter( 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( 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 = diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsDetailsLookup.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsDetailsLookup.kt deleted file mode 100644 index 514546a8d..000000000 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsDetailsLookup.kt +++ /dev/null @@ -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() { - - override fun getItemDetails(event: MotionEvent): ItemDetails? { - val view = recyclerView.findChildViewUnder(event.x, event.y) - if (view != null) { - val viewHolder = recyclerView.getChildViewHolder(view) as TabsTrayViewHolder - return viewHolder.getItemDetails() - } - return null - } -} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsItemKeyProvider.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsItemKeyProvider.kt deleted file mode 100644 index d138d4232..000000000 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsItemKeyProvider.kt +++ /dev/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(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 - } -} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTrayGridViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTrayGridViewHolder.kt index 8a6d4b953..48e123a06 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTrayGridViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTrayGridViewHolder.kt @@ -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? = 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) diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTrayListViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTrayListViewHolder.kt index 2332a02ff..584cdb7a1 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTrayListViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTrayListViewHolder.kt @@ -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? = 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) { diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/NormalBrowserTabViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/NormalBrowserTabViewHolder.kt index 2c647177e..f7ac2b551 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/NormalBrowserTabViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/NormalBrowserTabViewHolder.kt @@ -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 { - private lateinit var selectionTracker: SelectionTracker + /** + * 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 + get() = store.state.mode.selectedTabs override fun bind( adapter: RecyclerView.Adapter, layoutManager: RecyclerView.LayoutManager ) { - super.bind(adapter, layoutManager) - - selectionTracker = SelectionTracker.Builder( - "mySelection", - trayList, - TabsItemKeyProvider(trayList), - TabsDetailsLookup(trayList), - StorageStrategy.createLongStorage() - ).withSelectionPredicate( - SelectionPredicates.createSelectAnything() - ).build() + (adapter as BrowserTabsAdapter).selectionHolder = this - (adapter as BrowserTabsAdapter).tracker = selectionTracker - - selectionTracker.addObserver(object : SelectionTracker.SelectionObserver() { - override fun onItemStateChanged(key: Long, selected: Boolean) { - // TODO Do nothing for now; remove in a future patch if needed. - } - }) + super.bind(adapter, layoutManager) } companion object { diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt index 63ff46b7c..5d0832433 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt @@ -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() { - 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) diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayDialogTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayDialogTest.kt new file mode 100644 index 000000000..27263cce2 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayDialogTest.kt @@ -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(relaxed = true) + val interactor = mockk(relaxed = true) + val dialog = TabsTrayDialog(context, 0) { interactor } + + dialog.onBackPressed() + + verify { interactor.onBackPressed() } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapterTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapterTest.kt new file mode 100644 index 000000000..9e9cf6832 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapterTest.kt @@ -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(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(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(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 { + override val selectedItems: Set + get() = internalState + + val internalState = mutableSetOf() + } +} diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/DefaultBrowserTrayInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/DefaultBrowserTrayInteractorTest.kt index 0b71a107e..f10f02712 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/browser/DefaultBrowserTrayInteractorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/DefaultBrowserTrayInteractorTest.kt @@ -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() val settings = mockk() - 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() val settings = mockk() - 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 } diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/SelectedItemAdapterBindingTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/SelectedItemAdapterBindingTest.kt new file mode 100644 index 000000000..5120b854d --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/SelectedItemAdapterBindingTest.kt @@ -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(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)) + } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/TabAdapterIdStorageTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/TabAdapterIdStorageTest.kt deleted file mode 100644 index e67903edd..000000000 --- a/app/src/test/java/org/mozilla/fenix/tabstray/browser/TabAdapterIdStorageTest.kt +++ /dev/null @@ -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) - } -} diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/TabsAdapterTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/TabsAdapterTest.kt deleted file mode 100644 index 382502470..000000000 --- a/app/src/test/java/org/mozilla/fenix/tabstray/browser/TabsAdapterTest.kt +++ /dev/null @@ -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 - - @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() { - - inner class ViewHolder(view: View) : TabViewHolder(view) { - override var tab: Tab? = null - - override fun bind( - tab: Tab, - isSelected: Boolean, - styling: TabsTrayStyling, - observable: Observable - ) = throw UnsupportedOperationException() - } - - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): ViewHolder = throw UnsupportedOperationException() - } -} diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 40c8517a6..3e117e06c 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -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}"