No issue: Replace TabsTray.Tab with TabSessionState

Co-authored-by: Arturo Mejia <arturomejiamarmol@gmail.com>
upstream-sync
Jonathan Almeida 3 years ago committed by mergify[bot]
parent b2a7877c83
commit 7666f0e4c2

@ -76,7 +76,9 @@ val BrowserState.lastSearchGroup: RecentTab.SearchGroup?
* Returns a pair containing a list of search term groups sorted by last access time, and "remainder" tabs that have
* search terms but should not be in groups (because the group is of size one).
*/
fun List<TabSessionState>.toSearchGroup(): Pair<List<TabGroup>, List<TabSessionState>> {
fun List<TabSessionState>.toSearchGroup(
groupSet: Set<String> = emptySet()
): Pair<List<TabGroup>, List<TabSessionState>> {
val data = filter {
it.isNormalTabActiveWithSearchTerm(maxActiveTime)
}.groupBy {
@ -100,7 +102,9 @@ fun List<TabSessionState>.toSearchGroup(): Pair<List<TabGroup>, List<TabSessionS
)
}
val groups = groupings.filter { it.tabs.size > 1 }.sortedBy { it.lastAccess }
val groups = groupings
.filter { it.tabs.size > 1 || groupSet.contains(it.searchTerm) }
.sortedBy { it.lastAccess }
val remainderTabs = (groupings - groups).flatMap { it.tabs }
return groups to remainderTabs

@ -11,10 +11,10 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.storage.sync.Tab as SyncTab
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.concept.tabstray.Tab
import mozilla.components.service.fxa.manager.FxaAccountManager
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
@ -44,9 +44,9 @@ interface NavigationInteractor {
fun onAccountSettingsClicked()
/**
* Called when sharing a list of [Tab]s.
* Called when sharing a list of [TabSessionState]s.
*/
fun onShareTabs(tabs: Collection<Tab>)
fun onShareTabs(tabs: Collection<TabSessionState>)
/**
* Called when clicking the share tabs button.
@ -71,12 +71,12 @@ interface NavigationInteractor {
/**
* Used when opening the add-to-collections user flow.
*/
fun onSaveToCollections(tabs: Collection<Tab>)
fun onSaveToCollections(tabs: Collection<TabSessionState>)
/**
* Used when adding [Tab]s as bookmarks.
* Used when adding [TabSessionState]s as bookmarks.
*/
fun onSaveToBookmarks(tabs: Collection<Tab>)
fun onSaveToBookmarks(tabs: Collection<TabSessionState>)
/**
* Called when clicking on a SyncedTab item.
@ -138,9 +138,9 @@ class DefaultNavigationInteractor(
metrics.track(Event.RecentlyClosedTabsOpened)
}
override fun onShareTabs(tabs: Collection<Tab>) {
override fun onShareTabs(tabs: Collection<TabSessionState>) {
val data = tabs.map {
ShareData(url = it.url, title = it.title)
ShareData(url = it.content.url, title = it.content.title)
}
val directions = TabsTrayFragmentDirections.actionGlobalShareFragment(
data = data.toTypedArray()
@ -170,7 +170,7 @@ class DefaultNavigationInteractor(
dismissTabTrayAndNavigateHome(sessionsToClose)
}
override fun onSaveToCollections(tabs: Collection<Tab>) {
override fun onSaveToCollections(tabs: Collection<TabSessionState>) {
metrics.track(Event.TabsTraySaveToCollectionPressed)
tabsTrayStore.dispatch(TabsTrayAction.ExitSelectMode)
@ -195,13 +195,13 @@ class DefaultNavigationInteractor(
).show(context)
}
override fun onSaveToBookmarks(tabs: Collection<Tab>) {
override fun onSaveToBookmarks(tabs: Collection<TabSessionState>) {
tabs.forEach { tab ->
// We don't combine the context with lifecycleScope so that our jobs are not cancelled
// if we leave the fragment, i.e. we still want the bookmarks to be added if the
// tabs tray closes before the job is done.
CoroutineScope(ioDispatcher).launch {
bookmarksUseCase.addBookmark(tab.url, tab.title)
bookmarksUseCase.addBookmark(tab.content.url, tab.content.title)
}
}

@ -13,7 +13,6 @@ import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.base.profiler.Profiler
import mozilla.components.concept.tabstray.Tab
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.lib.state.DelicateAction
import org.mozilla.fenix.R
@ -47,18 +46,18 @@ interface TabsTrayController {
fun handleNavigateToBrowser()
/**
* Deletes the [Tab] with the specified [tabId].
* Deletes the [TabSessionState] with the specified [tabId].
*
* @param tabId The id of the [Tab] to be removed from TabsTray.
* @param tabId The id of the [TabSessionState] to be removed from TabsTray.
*/
fun handleTabDeletion(tabId: String)
/**
* Deletes a list of [tabs].
*
* @param tabs List of [Tab]s (sessions) to be removed.
* @param tabs List of [TabSessionState]s (sessions) to be removed.
*/
fun handleMultipleTabsDeletion(tabs: Collection<Tab>)
fun handleMultipleTabsDeletion(tabs: Collection<TabSessionState>)
/**
* Navigate from TabsTray to Recently Closed section in the History fragment.
@ -70,10 +69,10 @@ interface TabsTrayController {
*
* DO NOT USE THIS OUTSIDE OF DEBUGGING/TESTING.
*
* @param tabs List of [Tab]s to be removed.
* @param tabs List of [TabSessionState]s to be removed.
*/
fun forceTabsAsInactive(
tabs: Collection<Tab>,
tabs: Collection<TabSessionState>,
numOfDays: Long = DEFAULT_ACTIVE_DAYS + 1
)
@ -132,9 +131,9 @@ class DefaultTabsTrayController(
}
/**
* Deletes the [Tab] with the specified [tabId].
* Deletes the [TabSessionState] with the specified [tabId].
*
* @param tabId The id of the [Tab] to be removed from TabsTray.
* @param tabId The id of the [TabSessionState] to be removed from TabsTray.
* This method has no effect if the tab does not exist.
*/
override fun handleTabDeletion(tabId: String) {
@ -153,10 +152,11 @@ class DefaultTabsTrayController(
/**
* Deletes a list of [tabs] offering an undo option.
*
* @param tabs List of [Tab]s (sessions) to be removed. This method has no effect for tabs that do not exist.
* @param tabs List of [TabSessionState]s (sessions) to be removed.
* This method has no effect for tabs that do not exist.
*/
override fun handleMultipleTabsDeletion(tabs: Collection<Tab>) {
val isPrivate = tabs.any { it.private }
override fun handleMultipleTabsDeletion(tabs: Collection<TabSessionState>) {
val isPrivate = tabs.any { it.content.private }
// If user closes all the tabs from selected tabs page dismiss tray and navigate home.
if (tabs.size == browserStore.state.getNormalOrPrivateTabs(isPrivate).size) {
@ -189,7 +189,7 @@ class DefaultTabsTrayController(
* DO NOT USE THIS OUTSIDE OF DEBUGGING/TESTING.
*/
@OptIn(DelicateAction::class)
override fun forceTabsAsInactive(tabs: Collection<Tab>, numOfDays: Long) {
override fun forceTabsAsInactive(tabs: Collection<TabSessionState>, numOfDays: Long) {
tabs.forEach { tab ->
val daysSince = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(numOfDays)
browserStore.apply {

@ -4,7 +4,7 @@
package org.mozilla.fenix.tabstray
import mozilla.components.concept.tabstray.Tab
import mozilla.components.browser.state.state.TabSessionState
interface TabsTrayInteractor {
/**
@ -26,14 +26,14 @@ interface TabsTrayInteractor {
fun onDeleteTab(tabId: String)
/**
* Invoked when [Tab]s need to be deleted.
* Invoked when [TabSessionState]s need to be deleted.
*/
fun onDeleteTabs(tabs: Collection<Tab>)
fun onDeleteTabs(tabs: Collection<TabSessionState>)
/**
* Called when clicking the debug menu option for inactive tabs.
*/
fun onInactiveDebugClicked(tabs: Collection<Tab>)
fun onInactiveDebugClicked(tabs: Collection<TabSessionState>)
/**
* Deletes all inactive tabs.
@ -61,11 +61,11 @@ class DefaultTabsTrayInteractor(
controller.handleTabDeletion(tabId)
}
override fun onDeleteTabs(tabs: Collection<Tab>) {
override fun onDeleteTabs(tabs: Collection<TabSessionState>) {
controller.handleMultipleTabsDeletion(tabs)
}
override fun onInactiveDebugClicked(tabs: Collection<Tab>) {
override fun onInactiveDebugClicked(tabs: Collection<TabSessionState>) {
controller.forceTabsAsInactive(tabs)
}

@ -4,7 +4,7 @@
package org.mozilla.fenix.tabstray
import mozilla.components.concept.tabstray.Tab
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.State
@ -33,7 +33,7 @@ data class TabsTrayState(
/**
* A set of selected tabs which we would want to perform an action on.
*/
open val selectedTabs = emptySet<Tab>()
open val selectedTabs = emptySet<TabSessionState>()
/**
* The default mode the tabs list is in.
@ -44,7 +44,7 @@ data class TabsTrayState(
* The multi-select mode that the tabs list is in containing the set of currently
* selected tabs.
*/
data class Select(override val selectedTabs: Set<Tab>) : Mode()
data class Select(override val selectedTabs: Set<TabSessionState>) : Mode()
}
}
@ -95,14 +95,14 @@ sealed class TabsTrayAction : Action {
object ExitSelectMode : TabsTrayAction()
/**
* Added a new [Tab] to the selection set.
* Added a new [TabSessionState] to the selection set.
*/
data class AddSelectTab(val tab: Tab) : TabsTrayAction()
data class AddSelectTab(val tab: TabSessionState) : TabsTrayAction()
/**
* Removed a [Tab] from the selection set.
* Removed a [TabSessionState] from the selection set.
*/
data class RemoveSelectTab(val tab: Tab) : TabsTrayAction()
data class RemoveSelectTab(val tab: TabSessionState) : TabsTrayAction()
/**
* The active page in the tray that is now in focus.

@ -14,17 +14,16 @@ import androidx.appcompat.widget.AppCompatImageButton
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import mozilla.components.browser.state.selector.findTabOrCustomTab
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.tabstray.TabViewHolder
import mozilla.components.browser.tabstray.TabsTray
import mozilla.components.browser.tabstray.TabsTrayStyling
import mozilla.components.browser.tabstray.thumbnail.TabThumbnailView
import mozilla.components.browser.toolbar.MAX_URI_LENGTH
import mozilla.components.concept.base.images.ImageLoadRequest
import mozilla.components.concept.base.images.ImageLoader
import mozilla.components.concept.engine.mediasession.MediaSession
import mozilla.components.concept.tabstray.Tab
import mozilla.components.concept.tabstray.TabsTray
import mozilla.components.support.base.observer.Observable
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
@ -54,7 +53,7 @@ abstract class AbstractBrowserTabViewHolder(
itemView: View,
private val imageLoader: ImageLoader,
private val trayStore: TabsTrayStore,
private val selectionHolder: SelectionHolder<Tab>?,
private val selectionHolder: SelectionHolder<TabSessionState>?,
@VisibleForTesting
internal val featureName: String,
private val store: BrowserStore = itemView.context.components.core.store,
@ -76,35 +75,35 @@ abstract class AbstractBrowserTabViewHolder(
abstract val browserTrayInteractor: BrowserTrayInteractor
abstract val thumbnailSize: Int
override var tab: Tab? = null
override var tab: TabSessionState? = null
/**
* Displays the data of the given session and notifies the given observable about events.
*/
@Suppress("ComplexMethod", "LongMethod")
override fun bind(
tab: Tab,
tab: TabSessionState,
isSelected: Boolean,
styling: TabsTrayStyling,
observable: Observable<TabsTray.Observer>
delegate: TabsTray.Delegate
) {
this.tab = tab
updateTitle(tab)
updateUrl(tab)
updateFavicon(tab)
updateCloseButtonDescription(tab.title)
updateCloseButtonDescription(tab.content.title)
updateSelectedTabIndicator(isSelected)
updateMediaState(tab)
if (selectionHolder != null) {
setSelectionInteractor(tab, selectionHolder, browserTrayInteractor)
} else {
itemView.setOnClickListener { browserTrayInteractor.open(tab, featureName) }
itemView.setOnClickListener { browserTrayInteractor.onTabSelected(tab, featureName) }
}
if (tab.thumbnail != null) {
thumbnailView.setImageBitmap(tab.thumbnail)
if (tab.content.thumbnail != null) {
thumbnailView.setImageBitmap(tab.content.thumbnail)
} else {
loadIntoThumbnailView(thumbnailView, tab.id)
}
@ -115,31 +114,31 @@ abstract class AbstractBrowserTabViewHolder(
closeView.isInvisible = trayStore.state.mode is TabsTrayState.Mode.Select
}
private fun updateFavicon(tab: Tab) {
if (tab.icon != null) {
private fun updateFavicon(tab: TabSessionState) {
if (tab.content.icon != null) {
faviconView?.visibility = View.VISIBLE
faviconView?.setImageBitmap(tab.icon)
faviconView?.setImageBitmap(tab.content.icon)
} else {
faviconView?.visibility = View.GONE
}
}
private fun updateTitle(tab: Tab) {
val title = if (tab.title.isNotEmpty()) {
tab.title
private fun updateTitle(tab: TabSessionState) {
val title = if (tab.content.title.isNotEmpty()) {
tab.content.title
} else {
tab.url
tab.content.url
}
titleView.text = title
}
private fun updateUrl(tab: Tab) {
private fun updateUrl(tab: TabSessionState) {
// 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
urlView?.text = tab.content.url
.toShortUrl(itemView.context.components.publicSuffixList)
.take(MAX_URI_LENGTH)
}
@ -149,11 +148,7 @@ abstract class AbstractBrowserTabViewHolder(
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) {
private fun updateMediaState(tab: TabSessionState) {
// Media state
playPauseButtonView.increaseTapArea(PLAY_PAUSE_BUTTON_EXTRA_DPS)
@ -209,14 +204,16 @@ abstract class AbstractBrowserTabViewHolder(
}
private fun setSelectionInteractor(
item: Tab,
holder: SelectionHolder<Tab>,
item: TabSessionState,
holder: SelectionHolder<TabSessionState>,
interactor: BrowserTrayInteractor
) {
itemView.setOnClickListener {
val selected = holder.selectedItems
when {
selected.isEmpty() && trayStore.state.mode.isSelect().not() -> interactor.open(item, featureName)
selected.isEmpty() && trayStore.state.mode.isSelect().not() -> {
interactor.onTabSelected(item, featureName)
}
item in selected -> interactor.deselect(item)
else -> interactor.select(item)
}

@ -9,11 +9,10 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.AppCompatImageButton
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.tabstray.TabsTray
import mozilla.components.browser.tabstray.TabsTrayStyling
import mozilla.components.concept.base.images.ImageLoader
import mozilla.components.concept.tabstray.Tab
import mozilla.components.concept.tabstray.TabsTray
import mozilla.components.support.base.observer.Observable
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.TabTrayGridItemBinding
import org.mozilla.fenix.ext.increaseTapArea
@ -28,7 +27,8 @@ sealed class BrowserTabViewHolder(itemView: View) : RecyclerView.ViewHolder(item
* @param imageLoader [ImageLoader] used to load tab thumbnails.
* @param browserTrayInteractor [BrowserTrayInteractor] handling tabs interactions in a tab tray.
* @param store [TabsTrayStore] containing the complete state of tabs tray and methods to update that.
* @param selectionHolder [SelectionHolder]<[Tab]> for helping with selecting any number of displayed [Tab]s.
* @param selectionHolder [SelectionHolder]<[TabSessionState]> for helping with selecting
* any number of displayed [TabSessionState]s.
* @param itemView [View] that displays a "tab".
* @param featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting.
*/
@ -36,7 +36,7 @@ sealed class BrowserTabViewHolder(itemView: View) : RecyclerView.ViewHolder(item
imageLoader: ImageLoader,
override val browserTrayInteractor: BrowserTrayInteractor,
store: TabsTrayStore,
selectionHolder: SelectionHolder<Tab>? = null,
selectionHolder: SelectionHolder<TabSessionState>? = null,
itemView: View,
featureName: String
) : AbstractBrowserTabViewHolder(itemView, imageLoader, store, selectionHolder, featureName) {
@ -60,12 +60,12 @@ sealed class BrowserTabViewHolder(itemView: View) : RecyclerView.ViewHolder(item
}
override fun bind(
tab: Tab,
tab: TabSessionState,
isSelected: Boolean,
styling: TabsTrayStyling,
observable: Observable<TabsTray.Observer>
delegate: TabsTray.Delegate
) {
super.bind(tab, isSelected, styling, observable)
super.bind(tab, isSelected, styling, delegate)
closeButton.increaseTapArea(GRID_ITEM_CLOSE_BUTTON_EXTRA_DPS)
}
@ -81,7 +81,8 @@ sealed class BrowserTabViewHolder(itemView: View) : RecyclerView.ViewHolder(item
* @param imageLoader [ImageLoader] used to load tab thumbnails.
* @param browserTrayInteractor [BrowserTrayInteractor] handling tabs interactions in a tab tray.
* @param store [TabsTrayStore] containing the complete state of tabs tray and methods to update that.
* @param selectionHolder [SelectionHolder]<[Tab]> for helping with selecting any number of displayed [Tab]s.
* @param selectionHolder [SelectionHolder]<[TabSessionState]> for helping with selecting
* any number of displayed [TabSessionState]s.
* @param itemView [View] that displays a "tab".
* @param featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting.
*/
@ -89,7 +90,7 @@ sealed class BrowserTabViewHolder(itemView: View) : RecyclerView.ViewHolder(item
imageLoader: ImageLoader,
override val browserTrayInteractor: BrowserTrayInteractor,
store: TabsTrayStore,
selectionHolder: SelectionHolder<Tab>? = null,
selectionHolder: SelectionHolder<TabSessionState>? = null,
itemView: View,
featureName: String
) : AbstractBrowserTabViewHolder(itemView, imageLoader, store, selectionHolder, featureName) {

@ -9,13 +9,10 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.browser.state.state.TabSessionState
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.components.Components
import org.mozilla.fenix.databinding.TabTrayGridItemBinding
import org.mozilla.fenix.databinding.TabTrayItemBinding
@ -30,15 +27,13 @@ import org.mozilla.fenix.tabstray.TabsTrayStore
* @param interactor [BrowserTrayInteractor] handling tabs interactions in a tab tray.
* @param store [TabsTrayStore] containing the complete state of tabs tray and methods to update that.
* @param featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting.
* @param delegate [Observable]<[TabsTray.Observer]> for observing tabs tray changes. Defaults to [ObserverRegistry].
*/
class BrowserTabsAdapter(
private val context: Context,
private val interactor: BrowserTrayInteractor,
val interactor: BrowserTrayInteractor,
private val store: TabsTrayStore,
private val featureName: String,
delegate: Observable<TabsTray.Observer> = ObserverRegistry()
) : TabsAdapter<AbstractBrowserTabViewHolder>(delegate) {
override val featureName: String
) : TabsAdapter<AbstractBrowserTabViewHolder>(interactor), FeatureNameHolder {
/**
* The layout types for the tabs.
@ -51,7 +46,7 @@ class BrowserTabsAdapter(
/**
* Tracks the selected tabs in multi-select mode.
*/
var selectionHolder: SelectionHolder<Tab>? = null
var selectionHolder: SelectionHolder<TabSessionState>? = null
private val selectedItemAdapterBinding = SelectedItemAdapterBinding(store, this)
private val imageLoader = ThumbnailLoader(context.components.core.thumbnailStorage)
@ -113,7 +108,8 @@ class BrowserTabsAdapter(
return
}
if (position == selectedIndex) {
val tab = getItem(position)
if (tab.id == selectedTabId) {
if (payloads.contains(PAYLOAD_HIGHLIGHT_SELECTED_ITEM)) {
holder.updateSelectedTabIndicator(true)
} else if (payloads.contains(PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM)) {

@ -4,7 +4,8 @@
package org.mozilla.fenix.tabstray.browser
import mozilla.components.concept.tabstray.Tab
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.tabstray.TabsTray
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.support.base.feature.UserInteractionHandler
import org.mozilla.fenix.selection.SelectionInteractor
@ -19,23 +20,23 @@ import org.mozilla.fenix.tabstray.TabsTrayStore
* For interacting with UI that is specifically for [AbstractBrowserTrayList] and other browser
* tab tray views.
*/
interface BrowserTrayInteractor : SelectionInteractor<Tab>, UserInteractionHandler {
interface BrowserTrayInteractor : SelectionInteractor<TabSessionState>, UserInteractionHandler, TabsTray.Delegate {
/**
* Open a tab.
*
* @param tab [Tab] to open in browser.
* @param tab [TabSessionState] to open in browser.
* @param source app feature from which the [tab] was opened.
*/
fun open(tab: Tab, source: String? = null)
fun open(tab: TabSessionState, source: String? = null)
/**
* Close the tab.
*
* @param tab [Tab] to close.
* @param tab [TabSessionState] to close.
* @param source app feature from which the [tab] was closed.
*/
fun close(tab: Tab, source: String? = null)
fun close(tab: TabSessionState, source: String? = null)
/**
* TabTray's Floating Action Button clicked.
@ -51,6 +52,7 @@ interface BrowserTrayInteractor : SelectionInteractor<Tab>, UserInteractionHandl
/**
* A default implementation of [BrowserTrayInteractor].
*/
@Suppress("TooManyFunctions")
class DefaultBrowserTrayInteractor(
private val store: TabsTrayStore,
private val trayInteractor: TabsTrayInteractor,
@ -75,35 +77,35 @@ class DefaultBrowserTrayInteractor(
/**
* See [SelectionInteractor.open]
*/
override fun open(item: Tab) {
override fun open(item: TabSessionState) {
open(item, null)
}
/**
* See [BrowserTrayInteractor.open].
*/
override fun open(tab: Tab, source: String?) {
selectTabWrapper.invoke(tab.id, source)
override fun open(tab: TabSessionState, source: String?) {
selectTab(tab, source)
}
/**
* See [BrowserTrayInteractor.close].
*/
override fun close(tab: Tab, source: String?) {
removeTabWrapper.invoke(tab.id, source)
override fun close(tab: TabSessionState, source: String?) {
closeTab(tab, source)
}
/**
* See [SelectionInteractor.select]
*/
override fun select(item: Tab) {
override fun select(item: TabSessionState) {
store.dispatch(TabsTrayAction.AddSelectTab(item))
}
/**
* See [SelectionInteractor.deselect]
*/
override fun deselect(item: Tab) {
override fun deselect(item: TabSessionState) {
store.dispatch(TabsTrayAction.RemoveSelectTab(item))
}
@ -120,6 +122,14 @@ class DefaultBrowserTrayInteractor(
return false
}
override fun onTabClosed(tab: TabSessionState, source: String?) {
closeTab(tab, source)
}
override fun onTabSelected(tab: TabSessionState, source: String?) {
selectTab(tab, source)
}
/**
* See [BrowserTrayInteractor.onFabClicked]
*/
@ -133,4 +143,12 @@ class DefaultBrowserTrayInteractor(
override fun onRecentlyClosedClicked() {
controller.handleNavigateToRecentlyClosed()
}
private fun selectTab(tab: TabSessionState, source: String? = null) {
selectTabWrapper.invoke(tab.id, source)
}
private fun closeTab(tab: TabSessionState, source: String? = null) {
removeTabWrapper.invoke(tab.id, source)
}
}

@ -0,0 +1,14 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.tabstray.browser
/**
* Contains the identifying name of the feature.
*
* This is commonly used for telemetry.
*/
interface FeatureNameHolder {
val featureName: String
}

@ -7,8 +7,9 @@ package org.mozilla.fenix.tabstray.browser
import android.view.View
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.tabstray.TabsTray
import mozilla.components.browser.toolbar.MAX_URI_LENGTH
import mozilla.components.concept.tabstray.Tab
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.FenixSnackbar
@ -108,36 +109,35 @@ sealed class InactiveTabViewHolder(itemView: View) : RecyclerView.ViewHolder(ite
* A RecyclerView ViewHolder implementation for an inactive tab view.
*
* @param itemView the inactive tab [View].
* @param browserTrayInteractor [BrowserTrayInteractor] handling tabs interactions in a tab tray.
* @param featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting.
*/
class TabViewHolder(
itemView: View,
private val browserTrayInteractor: BrowserTrayInteractor,
private val delegate: TabsTray.Delegate,
private val featureName: String
) : InactiveTabViewHolder(itemView) {
private val binding = InactiveTabListItemBinding.bind(itemView)
fun bind(tab: Tab) {
fun bind(tab: TabSessionState) {
val components = itemView.context.components
val title = tab.title.ifEmpty { tab.url.take(MAX_URI_LENGTH) }
val url = tab.url.toShortUrl(components.publicSuffixList).take(MAX_URI_LENGTH)
val title = tab.content.title.ifEmpty { tab.content.url.take(MAX_URI_LENGTH) }
val url = tab.content.url.toShortUrl(components.publicSuffixList).take(MAX_URI_LENGTH)
itemView.setOnClickListener {
components.analytics.metrics.track(Event.TabsTrayOpenInactiveTab)
browserTrayInteractor.open(tab, featureName)
delegate.onTabSelected(tab, featureName)
}
binding.siteListItem.apply {
components.core.icons.loadIntoView(iconView, tab.url)
components.core.icons.loadIntoView(iconView, tab.content.url)
setText(title, url)
setSecondaryButton(
R.drawable.mozac_ic_close,
R.string.content_description_close_button
) {
components.analytics.metrics.track(Event.TabsTrayCloseInactiveTab())
browserTrayInteractor.close(tab, featureName)
delegate.onTabClosed(tab, featureName)
}
}
}

@ -9,10 +9,8 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import mozilla.components.concept.tabstray.Tab as TabsTrayTab
import mozilla.components.concept.tabstray.Tabs
import mozilla.components.concept.tabstray.TabsTray
import mozilla.components.support.base.observer.ObserverRegistry
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.tabstray.TabsTray
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.tabstray.TabsTrayInteractor
import org.mozilla.fenix.tabstray.browser.InactiveTabViewHolder.AutoCloseDialogHolder
@ -20,34 +18,26 @@ import org.mozilla.fenix.tabstray.browser.InactiveTabViewHolder.FooterHolder
import org.mozilla.fenix.tabstray.browser.InactiveTabViewHolder.HeaderHolder
import org.mozilla.fenix.tabstray.browser.InactiveTabViewHolder.TabViewHolder
import org.mozilla.fenix.utils.Settings
import mozilla.components.support.base.observer.Observable as ComponentObservable
/**
* A convenience alias for readability.
*/
private typealias Adapter = ListAdapter<InactiveTabsAdapter.Item, InactiveTabViewHolder>
/**
* A convenience alias for readability.
*/
private typealias Observable = ComponentObservable<TabsTray.Observer>
/**
* The [ListAdapter] for displaying the list of inactive tabs.
*
* @param context [Context] used for various platform interactions or accessing [Components]
* @param browserTrayInteractor [BrowserTrayInteractor] handling tabs interactions in a tab tray.
* @param featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting.
* @param delegate [Observable]<[TabsTray.Observer]> for observing tabs tray changes. Defaults to [ObserverRegistry].
*/
class InactiveTabsAdapter(
private val context: Context,
private val browserTrayInteractor: BrowserTrayInteractor,
private val tabsTrayInteractor: TabsTrayInteractor,
private val featureName: String,
override val featureName: String,
private val settings: Settings,
delegate: Observable = ObserverRegistry()
) : Adapter(DiffCallback), TabsTray, Observable by delegate {
) : Adapter(DiffCallback), TabsTray, FeatureNameHolder {
internal lateinit var inactiveTabsInteractor: InactiveTabsInteractor
internal lateinit var inactiveTabsAutoCloseDialogInteractor: InactiveTabsAutoCloseDialogInteractor
@ -92,11 +82,11 @@ class InactiveTabsAdapter(
}
}
override fun updateTabs(tabs: Tabs) {
inActiveTabsCount = tabs.list.size
override fun updateTabs(tabs: List<TabSessionState>, selectedTabId: String?) {
inActiveTabsCount = tabs.size
// Early return with an empty list to remove the header/footer items.
if (tabs.list.isEmpty()) {
if (tabs.isEmpty()) {
submitList(emptyList())
return
}
@ -107,7 +97,7 @@ class InactiveTabsAdapter(
return
}
val items = tabs.list.map { Item.Tab(it) }
val items = tabs.map { Item.Tab(it) }
val footer = Item.Footer
val headerItems = if (settings.shouldShowInactiveTabsAutoCloseDialog(items.size)) {
listOf(Item.Header, Item.AutoCloseMessage)
@ -117,8 +107,6 @@ class InactiveTabsAdapter(
submitList(headerItems + items + listOf(footer))
}
override fun isTabSelected(tabs: Tabs, position: Int): Boolean = false
private object DiffCallback : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return if (oldItem is Item.Tab && newItem is Item.Tab) {
@ -147,7 +135,7 @@ class InactiveTabsAdapter(
/**
* A tab that is now considered inactive.
*/
data class Tab(val tab: TabsTrayTab) : Item()
data class Tab(val tab: TabSessionState) : Item()
/**
* A dialog for when the inactive tabs section reach 20 tabs.

@ -7,8 +7,7 @@ package org.mozilla.fenix.tabstray.browser
import androidx.annotation.VisibleForTesting
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.tabstray.TabsTray
import mozilla.components.feature.tabs.ext.toTabs
import mozilla.components.browser.tabstray.TabsTray
import org.mozilla.fenix.utils.Settings
class InactiveTabsAutoCloseDialogController(
@ -39,7 +38,7 @@ class InactiveTabsAutoCloseDialogController(
@VisibleForTesting
internal fun refeshInactiveTabsSecion() {
val tabs = browserStore.state.toTabs { tabFilter.invoke(it) }
tray.updateTabs(tabs)
val tabs = browserStore.state.tabs.filter(tabFilter)
tray.updateTabs(tabs, browserStore.state.selectedTabId)
}
}

@ -6,8 +6,7 @@ package org.mozilla.fenix.tabstray.browser
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.tabstray.TabsTray
import mozilla.components.feature.tabs.ext.toTabs
import mozilla.components.browser.tabstray.TabsTray
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.metrics.Event
@ -31,8 +30,7 @@ class InactiveTabsController(
}
)
val tabs = browserStore.state.toTabs { tabFilter.invoke(it) }
tray.updateTabs(tabs)
val tabs = browserStore.state.tabs.filter(tabFilter)
tray.updateTabs(tabs, browserStore.state.selectedTabId)
}
}

@ -9,12 +9,9 @@ import android.util.AttributeSet
import androidx.recyclerview.widget.ConcatAdapter
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.tabstray.TabViewHolder
import mozilla.components.concept.tabstray.Tab
import mozilla.components.concept.tabstray.TabsTray
import mozilla.components.feature.tabs.tabstray.TabsFeature
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.tabstray.TrayPagerAdapter.Companion.TABS_TRAY_FEATURE_NAME
import org.mozilla.fenix.tabstray.ext.browserAdapter
import org.mozilla.fenix.tabstray.ext.inactiveTabsAdapter
import org.mozilla.fenix.tabstray.ext.isNormalTabInactive
@ -36,14 +33,12 @@ class NormalBrowserTrayList @JvmOverloads constructor(
defStyleAttr: Int = 0
) : AbstractBrowserTrayList(context, attrs, defStyleAttr) {
private val swipeDelegate = SwipeToDeleteDelegate()
private val concatAdapter by lazy { adapter as ConcatAdapter }
private val tabSorter by lazy {
TabSorter(
context.settings(),
context.components.analytics.metrics,
concatAdapter,
context.components.core.store
concatAdapter
)
}
private val inactiveTabsFilter: (TabSessionState) -> Boolean = filter@{
@ -79,20 +74,18 @@ class NormalBrowserTrayList @JvmOverloads constructor(
TabsFeature(
tabSorter,
context.components.core.store,
selectTabUseCase,
removeTabUseCase,
{ !it.content.private },
{}
)
}
private val touchHelper by lazy {
TabsTouchHelper(
observable = concatAdapter.browserAdapter,
interactionDelegate = concatAdapter.browserAdapter.interactor,
onViewHolderTouched = {
it is TabViewHolder && swipeToDelete.isSwipeable
},
onViewHolderDraw = { context.components.settings.gridTabView.not() }
onViewHolderDraw = { context.components.settings.gridTabView.not() },
featureNameHolder = concatAdapter.browserAdapter
)
}
@ -104,8 +97,6 @@ class NormalBrowserTrayList @JvmOverloads constructor(
tabsFeature.start()
concatAdapter.browserAdapter.register(swipeDelegate)
touchHelper.attachToRecyclerView(this)
}
@ -114,21 +105,6 @@ class NormalBrowserTrayList @JvmOverloads constructor(
tabsFeature.stop()
concatAdapter.browserAdapter.unregister(swipeDelegate)
touchHelper.attachToRecyclerView(null)
}
/**
* A delegate for handling open/selected events from swipe-to-delete gestures.
*/
inner class SwipeToDeleteDelegate : TabsTray.Observer {
override fun onTabClosed(tab: Tab) {
removeTabUseCase.invoke(tab.id, TABS_TRAY_FEATURE_NAME)
}
override fun onTabSelected(tab: Tab) {
selectTabUseCase.invoke(tab.id)
}
}
}

@ -21,19 +21,16 @@ class PrivateBrowserTrayList @JvmOverloads constructor(
// NB: The use cases here are duplicated because there isn't a nicer
// way to share them without a better dependency injection solution.
TabsFeature(
adapter as TabsAdapter,
adapter as BrowserTabsAdapter,
context.components.core.store,
selectTabUseCase,
removeTabUseCase,
{ it.content.private },
{ }
)
) { it.content.private }
}
private val touchHelper by lazy {
TabsTouchHelper(
observable = adapter as TabsAdapter,
interactionDelegate = (adapter as BrowserTabsAdapter).delegate,
onViewHolderTouched = { swipeToDelete.isSwipeable },
onViewHolderDraw = { context.components.settings.gridTabView.not() }
onViewHolderDraw = { context.components.settings.gridTabView.not() },
featureNameHolder = (adapter as BrowserTabsAdapter)
)
}

@ -13,17 +13,12 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
import androidx.recyclerview.widget.RecyclerView.VERTICAL
import mozilla.components.concept.tabstray.Tabs
import mozilla.components.concept.tabstray.TabsTray
import mozilla.components.support.base.observer.ObserverRegistry
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.tabstray.TabsTray
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.selection.SelectionHolder
import org.mozilla.fenix.tabstray.TabsTrayStore
import mozilla.components.concept.tabstray.Tab as TabsTrayTab
import mozilla.components.support.base.observer.Observable
typealias TrayObservable = Observable<TabsTray.Observer>
/**
* The [ListAdapter] for displaying the list of search term tabs.
@ -31,40 +26,19 @@ typealias TrayObservable = Observable<TabsTray.Observer>
* @param context [Context] used for various platform interactions or accessing [Components]
* @param browserTrayInteractor [BrowserTrayInteractor] handling tabs interactions in a tab tray.
* @param featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting.
* @param delegate [Observable]<[TabsTray.Observer]> for observing tabs tray changes. Defaults to [ObserverRegistry].
*/
@Suppress("TooManyFunctions")
class TabGroupAdapter(
private val context: Context,
private val browserTrayInteractor: BrowserTrayInteractor,
private val store: TabsTrayStore,
private val featureName: String,
delegate: TrayObservable = ObserverRegistry()
) : ListAdapter<TabGroupAdapter.Group, TabGroupViewHolder>(DiffCallback), TabsTray, TrayObservable by delegate {
// TODO use [List<TabSessionState>.toSearchGroup()]
// see https://github.com/mozilla-mobile/android-components/issues/11012
data class Group(
/**
* A title for the tab group.
*/
val title: String,
/**
* The list of tabs belonging to this tab group.
*/
val tabs: List<TabsTrayTab>,
/**
* The last time tabs in this group was accessed.
*/
val lastAccess: Long
)
override val featureName: String,
) : ListAdapter<TabGroup, TabGroupViewHolder>(DiffCallback), TabsTray, FeatureNameHolder {
/**
* Tracks the selected tabs in multi-select mode.
*/
var selectionHolder: SelectionHolder<TabsTrayTab>? = null
var selectionHolder: SelectionHolder<TabSessionState>? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabGroupViewHolder {
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
@ -81,7 +55,7 @@ class TabGroupAdapter(
override fun onBindViewHolder(holder: TabGroupViewHolder, position: Int) {
val group = getItem(position)
holder.bind(group, this)
holder.bind(group)
}
override fun getItemViewType(position: Int) = TabGroupViewHolder.LAYOUT_ID
@ -103,19 +77,15 @@ class TabGroupAdapter(
/**
* Not implemented; implementation is handled [List<Tab>.toSearchGroups]
*/
override fun updateTabs(tabs: Tabs) = throw UnsupportedOperationException("Use submitList instead.")
/**
* Not implemented; handled by nested [RecyclerView].
*/
override fun isTabSelected(tabs: Tabs, position: Int): Boolean = false
override fun updateTabs(tabs: List<TabSessionState>, selectedTabId: String?) =
throw UnsupportedOperationException("Use submitList instead.")
private object DiffCallback : DiffUtil.ItemCallback<Group>() {
override fun areItemsTheSame(oldItem: Group, newItem: Group) = oldItem.title == newItem.title
override fun areContentsTheSame(oldItem: Group, newItem: Group) = oldItem == newItem
private object DiffCallback : DiffUtil.ItemCallback<TabGroup>() {
override fun areItemsTheSame(oldItem: TabGroup, newItem: TabGroup) = oldItem.searchTerm == newItem.searchTerm
override fun areContentsTheSame(oldItem: TabGroup, newItem: TabGroup) = oldItem == newItem
}
}
internal fun TabGroupAdapter.Group.containsTabId(tabId: String): Boolean {
internal fun TabGroup.containsTabId(tabId: String): Boolean {
return tabs.firstOrNull { it.id == tabId } != null
}

@ -11,13 +11,11 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.browser.state.state.TabSessionState
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.tabstray.TabsTrayStyling
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 org.mozilla.fenix.R
import org.mozilla.fenix.databinding.TabTrayGridItemBinding
import org.mozilla.fenix.databinding.TabTrayItemBinding
@ -33,17 +31,15 @@ import org.mozilla.fenix.tabstray.ext.MIN_COLUMN_WIDTH_DP
* @param context [Context] used for various platform interactions or accessing [Components]
* @param interactor [BrowserTrayInteractor] handling tabs interactions in a tab tray.
* @param store [TabsTrayStore] containing the complete state of tabs tray and methods to update that.
* @param delegate [Observable]<[TabsTray.Observer]> for observing tabs tray changes. Defaults to [ObserverRegistry].
* @param featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting.
*/
class TabGroupListAdapter(
private val context: Context,
private val interactor: BrowserTrayInteractor,
private val store: TabsTrayStore,
private val delegate: Observable<TabsTray.Observer>,
private val selectionHolder: SelectionHolder<Tab>?,
private val selectionHolder: SelectionHolder<TabSessionState>?,
private val featureName: String,
) : ListAdapter<Tab, AbstractBrowserTabViewHolder>(DiffCallback) {
) : ListAdapter<TabSessionState, AbstractBrowserTabViewHolder>(DiffCallback) {
private val selectedItemAdapterBinding = SelectedItemAdapterBinding(store, this)
private val imageLoader = ThumbnailLoader(context.components.core.thumbnailStorage)
@ -67,7 +63,7 @@ class TabGroupListAdapter(
override fun onBindViewHolder(holder: AbstractBrowserTabViewHolder, position: Int) {
val tab = getItem(position)
val selectedTabId = context.components.core.store.state.selectedTabId
holder.bind(tab, tab.id == selectedTabId, TabsTrayStyling(), delegate)
holder.bind(tab, tab.id == selectedTabId, TabsTrayStyling(), interactor)
holder.tab?.let { holderTab ->
when {
context.components.settings.gridTabView -> {
@ -147,8 +143,8 @@ class TabGroupListAdapter(
selectedItemAdapterBinding.stop()
}
private object DiffCallback : DiffUtil.ItemCallback<Tab>() {
override fun areItemsTheSame(oldItem: Tab, newItem: Tab) = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Tab, newItem: Tab) = oldItem == newItem
private object DiffCallback : DiffUtil.ItemCallback<TabSessionState>() {
override fun areItemsTheSame(oldItem: TabSessionState, newItem: TabSessionState) = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: TabSessionState, newItem: TabSessionState) = oldItem == newItem
}
}

@ -5,9 +5,7 @@
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.concept.tabstray.Tab
import mozilla.components.concept.tabstray.TabsTray
import mozilla.components.support.base.observer.Observable
import mozilla.components.browser.state.state.TabSessionState
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.TabGroupItemBinding
import org.mozilla.fenix.ext.components
@ -15,7 +13,7 @@ import org.mozilla.fenix.selection.SelectionHolder
import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.tabstray.TrayPagerAdapter
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
import org.mozilla.fenix.tabstray.browser.TabGroupAdapter
import org.mozilla.fenix.tabstray.browser.TabGroup
import org.mozilla.fenix.tabstray.browser.TabGroupListAdapter
/**
@ -32,27 +30,25 @@ class TabGroupViewHolder(
val orientation: Int,
val interactor: BrowserTrayInteractor,
val store: TabsTrayStore,
val selectionHolder: SelectionHolder<Tab>? = null
val selectionHolder: SelectionHolder<TabSessionState>? = null
) : RecyclerView.ViewHolder(itemView) {
private val binding = TabGroupItemBinding.bind(itemView)
lateinit var groupListAdapter: TabGroupListAdapter
fun bind(
group: TabGroupAdapter.Group,
observable: Observable<TabsTray.Observer>
group: TabGroup,
) {
val selectedTabId = itemView.context.components.core.store.state.selectedTabId
val selectedIndex = group.tabs.indexOfFirst { it.id == selectedTabId }
binding.tabGroupTitle.text = group.title
binding.tabGroupTitle.text = group.searchTerm
binding.tabGroupList.apply {
layoutManager = LinearLayoutManager(itemView.context, orientation, false)
groupListAdapter = TabGroupListAdapter(
context = itemView.context,
interactor = interactor,
store = store,
delegate = observable,
selectionHolder = selectionHolder,
featureName = TrayPagerAdapter.TAB_GROUP_FEATURE_NAME
)

@ -5,21 +5,20 @@
package org.mozilla.fenix.tabstray.browser
import androidx.recyclerview.widget.ConcatAdapter
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.tabstray.Tab
import mozilla.components.concept.tabstray.Tabs
import mozilla.components.concept.tabstray.TabsTray
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.tabstray.TabsTray
import mozilla.components.feature.tabs.tabstray.TabsFeature
import mozilla.components.support.base.observer.Observable
import mozilla.components.support.base.observer.ObserverRegistry
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.toSearchGroup
import org.mozilla.fenix.tabstray.ext.browserAdapter
import org.mozilla.fenix.tabstray.ext.hasSearchTerm
import org.mozilla.fenix.tabstray.ext.inactiveTabsAdapter
import org.mozilla.fenix.tabstray.ext.isActive
import org.mozilla.fenix.tabstray.ext.isNormalTabActiveWithSearchTerm
import org.mozilla.fenix.tabstray.ext.tabGroupAdapter
import org.mozilla.fenix.tabstray.ext.titleHeaderAdapter
import org.mozilla.fenix.utils.Settings
import kotlin.math.max
/**
* An intermediary layer to consume tabs from [TabsFeature] for sorting into the various adapters.
@ -27,36 +26,31 @@ import kotlin.math.max
class TabSorter(
private val settings: Settings,
private val metrics: MetricController,
private val concatAdapter: ConcatAdapter,
private val store: BrowserStore
) : TabsTray, Observable<TabsTray.Observer> by ObserverRegistry() {
private val concatAdapter: ConcatAdapter
) : TabsTray {
private var shouldReportMetrics: Boolean = true
private val groupsSet = mutableSetOf<String>()
override fun updateTabs(tabs: Tabs) {
val inactiveTabs = tabs.list.getInactiveTabs(settings)
val searchTermTabs = tabs.list.getSearchGroupTabs(settings)
val normalTabs = tabs.list - inactiveTabs - searchTermTabs
val selectedTabId = store.state.selectedTabId
override fun updateTabs(tabs: List<TabSessionState>, selectedTabId: String?) {
val inactiveTabs = tabs.getInactiveTabs(settings)
val searchTermTabs = tabs.getSearchGroupTabs(settings)
val normalTabs = tabs - inactiveTabs - searchTermTabs
// Inactive tabs
val selectedInactiveIndex = inactiveTabs.findSelectedIndex(selectedTabId)
concatAdapter.inactiveTabsAdapter.updateTabs((Tabs(inactiveTabs, selectedInactiveIndex)))
concatAdapter.inactiveTabsAdapter.updateTabs(inactiveTabs, selectedTabId)
// Tab groups
// We don't need to provide a selectedId, because the [TabGroupAdapter] has that built-in with support from
// NormalBrowserPageViewHolder.scrollToTab.
val (groups, remainderTabs) = searchTermTabs.toSearchGroups(groupsSet)
val (groups, remainderTabs) = searchTermTabs.toSearchGroup(groupsSet)
groupsSet.clear()
groupsSet.addAll(groups.map { it.title })
groupsSet.addAll(groups.map { it.searchTerm })
concatAdapter.tabGroupAdapter.submitList(groups)
// Normal tabs.
val totalNormalTabs = (normalTabs + remainderTabs)
val selectedTabIndex = totalNormalTabs.findSelectedIndex(selectedTabId)
concatAdapter.browserAdapter.updateTabs(Tabs(totalNormalTabs, selectedTabIndex))
concatAdapter.browserAdapter.updateTabs(totalNormalTabs, selectedTabId)
// Normal tab title header.
concatAdapter.titleHeaderAdapter
@ -70,19 +64,12 @@ class TabSorter(
}
}
}
override fun isTabSelected(tabs: Tabs, position: Int): Boolean = false
}
private fun List<Tab>.findSelectedIndex(tabId: String?): Int {
val id = tabId ?: return -1
return indexOfFirst { it.id == id }
}
/**
* Returns a list of inactive tabs based on our preferences.
*/
private fun List<Tab>.getInactiveTabs(settings: Settings): List<Tab> {
private fun List<TabSessionState>.getInactiveTabs(settings: Settings): List<TabSessionState> {
val inactiveTabsEnabled = settings.inactiveTabsAreEnabled
return if (inactiveTabsEnabled) {
filter { !it.isActive(maxActiveTime) }
@ -94,63 +81,16 @@ private fun List<Tab>.getInactiveTabs(settings: Settings): List<Tab> {
/**
* Returns a list of search term tabs based on our preferences.
*/
private fun List<Tab>.getSearchGroupTabs(settings: Settings): List<Tab> {
private fun List<TabSessionState>.getSearchGroupTabs(settings: Settings): List<TabSessionState> {
val inactiveTabsEnabled = settings.inactiveTabsAreEnabled
val tabGroupsEnabled = settings.searchTermTabGroupsAreEnabled
return when {
tabGroupsEnabled && inactiveTabsEnabled ->
filter { it.searchTerm.isNotBlank() && it.isActive(maxActiveTime) }
filter { it.isNormalTabActiveWithSearchTerm(maxActiveTime) }
tabGroupsEnabled ->
filter { it.searchTerm.isNotBlank() }
filter { it.hasSearchTerm() }
else -> emptyList()
}
}
/**
* Returns true if a tab has not been selected since [maxActiveTime].
*
* N.B: This is duplicated from [TabSessionState.isActive(Long)] to work for [Tab].
*
* See also: https://github.com/mozilla-mobile/android-components/issues/11012
*/
private fun Tab.isActive(maxActiveTime: Long): Boolean {
val lastActiveTime = maxOf(lastAccess, createdAt)
val now = System.currentTimeMillis()
return (now - lastActiveTime <= maxActiveTime)
}
/**
* Creates a list of grouped search term tabs sorted by last access time and a list of tabs
* that have search terms but would only create groups with a single tab.
*
* N.B: This is duplicated from [List<TabSessionState>.toSearchGroup()] to work for [Tab].
*
* See also: https://github.com/mozilla-mobile/android-components/issues/11012
*/
private fun List<Tab>.toSearchGroups(groupSet: Set<String>): Pair<List<TabGroupAdapter.Group>, List<Tab>> {
val data = groupBy { it.searchTerm.lowercase() }
val groupings = data.map { mapEntry ->
// Uppercase since we use it for the title.
val searchTerm = mapEntry.key.replaceFirstChar(Char::uppercase)
val groupTabs = mapEntry.value
// Calculate when the group was last used.
val groupMax = groupTabs.fold(0L) { acc, tab ->
max(tab.lastAccess, acc)
}
TabGroupAdapter.Group(
title = searchTerm,
tabs = groupTabs,
lastAccess = groupMax
)
}
val groups = groupings.filter { it.tabs.size > 1 || groupSet.contains(it.title) }.sortedBy { it.lastAccess }
val remainderTabs = (groupings - groups).flatMap { it.tabs }
return groups to remainderTabs
}

@ -7,13 +7,10 @@ package org.mozilla.fenix.tabstray.browser
import androidx.annotation.CallSuper
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.tabstray.TabViewHolder
import mozilla.components.browser.tabstray.TabsTray
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 mozilla.components.support.base.observer.ObserverRegistry
/**
* RecyclerView adapter implementation to display a list/grid of tabs.
@ -23,38 +20,33 @@ import mozilla.components.support.base.observer.ObserverRegistry
* for Android UI APIs.
*
* TODO Let's upstream this to AC with tests.
*
* @param delegate TabsTray.Observer registry to allow `TabsAdapter` to conform to `Observable<TabsTray.Observer>`.
*/
abstract class TabsAdapter<T : TabViewHolder>(
delegate: Observable<TabsTray.Observer> = ObserverRegistry()
) : ListAdapter<Tab, T>(DiffCallback), TabsTray, Observable<TabsTray.Observer> by delegate {
val delegate: TabsTray.Delegate,
) : ListAdapter<TabSessionState, T>(DiffCallback), TabsTray {
protected var selectedIndex: Int? = null
protected var selectedTabId: String? = null
protected var styling: TabsTrayStyling = TabsTrayStyling()
@CallSuper
override fun updateTabs(tabs: Tabs) {
this.selectedIndex = tabs.selectedIndex
submitList(tabs.list)
override fun updateTabs(tabs: List<TabSessionState>, selectedTabId: String?) {
this.selectedTabId = selectedTabId
notifyObservers { onTabsUpdated() }
submitList(tabs)
}
@CallSuper
override fun onBindViewHolder(holder: T, position: Int) {
holder.bind(getItem(position), selectedIndex == position, styling, this)
val tab = getItem(position)
holder.bind(getItem(position), tab.id == selectedTabId, styling, delegate)
}
override fun isTabSelected(tabs: Tabs, position: Int): Boolean = false
private object DiffCallback : DiffUtil.ItemCallback<Tab>() {
override fun areItemsTheSame(oldItem: Tab, newItem: Tab): Boolean {
private object DiffCallback : DiffUtil.ItemCallback<TabSessionState>() {
override fun areItemsTheSame(oldItem: TabSessionState, newItem: TabSessionState): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Tab, newItem: Tab): Boolean {
override fun areContentsTheSame(oldItem: TabSessionState, newItem: TabSessionState): Boolean {
return oldItem == newItem
}
}

@ -10,9 +10,9 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_IDLE
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.tabstray.TabTouchCallback
import mozilla.components.concept.tabstray.TabsTray
import mozilla.components.support.base.observer.Observable
import mozilla.components.browser.tabstray.TabsTray
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.android.content.getDrawableWithTint
import mozilla.components.support.ktx.android.util.dpToPx
@ -37,10 +37,11 @@ typealias OnViewHolderToDraw = (RecyclerView.ViewHolder) -> Boolean
* @param onViewHolderTouched See [OnViewHolderTouched].
*/
class TabsTouchHelper(
observable: Observable<TabsTray.Observer>,
interactionDelegate: TabsTray.Delegate,
onViewHolderTouched: OnViewHolderTouched = { true },
onViewHolderDraw: OnViewHolderToDraw = { true },
delegate: Callback = TouchCallback(observable, onViewHolderTouched, onViewHolderDraw)
featureNameHolder: FeatureNameHolder,
delegate: Callback = TouchCallback(interactionDelegate, onViewHolderTouched, onViewHolderDraw, featureNameHolder),
) : ItemTouchHelper(delegate)
/**
@ -49,10 +50,12 @@ class TabsTouchHelper(
* @param onViewHolderTouched invoked when a tab is about to be swiped. See [OnViewHolderTouched].
*/
class TouchCallback(
observable: Observable<TabsTray.Observer>,
delegate: TabsTray.Delegate,
private val onViewHolderTouched: OnViewHolderTouched,
private val onViewHolderDraw: OnViewHolderToDraw
) : TabTouchCallback(observable) {
private val onViewHolderDraw: OnViewHolderToDraw,
featureNameHolder: FeatureNameHolder,
onRemove: (TabSessionState) -> Unit = { delegate.onTabClosed(it, featureNameHolder.featureName) }
) : TabTouchCallback(onRemove) {
override fun getMovementFlags(
recyclerView: RecyclerView,

@ -7,12 +7,11 @@ package org.mozilla.fenix.tabstray.ext
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.tabstray.Tab
/**
* Find and extract a list [TabSessionState] from the [BrowserStore] using the IDs from [tabs].
*/
fun BrowserStore.getTabSessionState(tabs: Collection<Tab>): List<TabSessionState> {
fun BrowserStore.getTabSessionState(tabs: Collection<TabSessionState>): List<TabSessionState> {
return tabs.mapNotNull {
state.findTab(it.id)
}

@ -6,7 +6,7 @@ package org.mozilla.fenix.tabstray.ext
import mozilla.components.browser.state.state.TabSessionState
private fun TabSessionState.isActive(maxActiveTime: Long): Boolean {
fun TabSessionState.isActive(maxActiveTime: Long): Boolean {
val lastActiveTime = maxOf(lastAccess, createdAt)
val now = System.currentTimeMillis()
return (now - lastActiveTime <= maxActiveTime)
@ -15,7 +15,7 @@ private fun TabSessionState.isActive(maxActiveTime: Long): Boolean {
/**
* Returns true if the [TabSessionState] has a search term.
*/
private fun TabSessionState.hasSearchTerm(): Boolean {
fun TabSessionState.hasSearchTerm(): Boolean {
return content.searchTerms.isNotEmpty() || !historyMetadata?.searchTerm.isNullOrBlank()
}

@ -10,8 +10,8 @@ import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.browser.state.selector.selectedNormalTab
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.tabstray.Tab
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.selection.SelectionHolder
@ -39,7 +39,7 @@ class NormalBrowserPageViewHolder(
private val tabsTrayStore: TabsTrayStore,
private val browserStore: BrowserStore,
interactor: TabsTrayInteractor,
) : AbstractBrowserPageViewHolder(containerView, tabsTrayStore, interactor), SelectionHolder<Tab> {
) : AbstractBrowserPageViewHolder(containerView, tabsTrayStore, interactor), SelectionHolder<TabSessionState> {
/**
* Holds the list of selected tabs.
@ -47,7 +47,7 @@ class NormalBrowserPageViewHolder(
* 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>
override val selectedItems: Set<TabSessionState>
get() = tabsTrayStore.state.mode.selectedTabs
override val emptyStringText: String

@ -19,9 +19,9 @@ import io.mockk.verifyOrder
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.base.profiler.Profiler
import mozilla.components.concept.tabstray.Tab
import mozilla.components.feature.tabs.TabsUseCases
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@ -268,9 +268,8 @@ class DefaultTabsTrayControllerTest {
)
)
val privateTab: Tab = mockk {
every { private } returns true
}
val privateTab = createTab(url = "url", private = true)
every { browserStore.state } returns mockk()
try {
mockkStatic("mozilla.components.browser.state.selector.SelectorsKt")
@ -297,9 +296,9 @@ class DefaultTabsTrayControllerTest {
}
)
)
val normalTab: Tab = mockk {
every { private } returns false
}
val normalTab = createTab(url = "url", private = false)
every { browserStore.state } returns mockk()
try {
mockkStatic("mozilla.components.browser.state.selector.SelectorsKt")
@ -319,10 +318,8 @@ class DefaultTabsTrayControllerTest {
fun `WHEN handleMultipleTabsDeletion is called to close some private tabs THEN that it uses tabsUseCases#removeTabs and shows an undo snackbar`() {
var showUndoSnackbarForTabInvoked = false
val controller = spyk(createController(showUndoSnackbarForTab = { showUndoSnackbarForTabInvoked = true }))
val privateTab: Tab = mockk {
every { private } returns true
every { id } returns "42"
}
val privateTab = createTab(id = "42", url = "url", private = true)
every { browserStore.state } returns mockk()
try {
mockkStatic("mozilla.components.browser.state.selector.SelectorsKt")
@ -342,10 +339,8 @@ class DefaultTabsTrayControllerTest {
fun `WHEN handleMultipleTabsDeletion is called to close some normal tabs THEN that it uses tabsUseCases#removeTabs and shows an undo snackbar`() {
var showUndoSnackbarForTabInvoked = false
val controller = spyk(createController(showUndoSnackbarForTab = { showUndoSnackbarForTabInvoked = true }))
val privateTab: Tab = mockk {
every { private } returns false
every { id } returns "24"
}
val privateTab = createTab(id = "24", url = "url", private = false)
every { browserStore.state } returns mockk()
try {
mockkStatic("mozilla.components.browser.state.selector.SelectorsKt")

@ -6,8 +6,8 @@ package org.mozilla.fenix.tabstray
import io.mockk.mockk
import io.mockk.verifySequence
import mozilla.components.browser.state.state.TabSessionState
import org.junit.Test
import mozilla.components.concept.tabstray.Tab
class DefaultTabsTrayInteractorTest {
val controller: TabsTrayController = mockk(relaxed = true)
@ -36,7 +36,7 @@ class DefaultTabsTrayInteractorTest {
@Test
fun `GIVEN user deleted multiple browser tabs WHEN onDeleteTabs is called THEN the Interactor delegates the controller`() {
val tabsToDelete = listOf<Tab>(mockk(), mockk())
val tabsToDelete = listOf<TabSessionState>(mockk(), mockk())
trayInteractor.onDeleteTabs(tabsToDelete)

@ -36,7 +36,6 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import mozilla.components.browser.state.state.createTab as createStateTab
import mozilla.components.browser.storage.sync.Tab as SyncTab
import org.mozilla.fenix.tabstray.browser.createTab as createTrayTab
class NavigationInteractorTest {
private lateinit var store: BrowserStore
@ -148,7 +147,7 @@ class NavigationInteractorTest {
showBookmarkSnackbar = {
showBookmarkSnackbarInvoked = true
}
).onSaveToBookmarks(listOf(createTrayTab()))
).onSaveToBookmarks(listOf(createStateTab("url")))
coVerify(exactly = 1) { bookmarksUseCase.addBookmark(any(), any(), any()) }
assertTrue(showBookmarkSnackbarInvoked)

@ -4,12 +4,12 @@
package org.mozilla.fenix.tabstray
import mozilla.components.browser.state.state.createTab
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.mozilla.fenix.tabstray.browser.createTab
class TabsTrayStoreTest {
@ -24,7 +24,7 @@ class TabsTrayStoreTest {
assertTrue(store.state.mode.selectedTabs.isEmpty())
assertTrue(store.state.mode is TabsTrayState.Mode.Select)
store.dispatch(TabsTrayAction.AddSelectTab(createTab()))
store.dispatch(TabsTrayAction.AddSelectTab(createTab(url = "url")))
store.dispatch(TabsTrayAction.ExitSelectMode)
store.dispatch(TabsTrayAction.EnterSelectMode)
@ -56,7 +56,7 @@ class TabsTrayStoreTest {
fun `WHEN adding a tab to selection THEN it is added to the selectedTabs`() {
val store = TabsTrayStore()
store.dispatch(TabsTrayAction.AddSelectTab(createTab("tab1")))
store.dispatch(TabsTrayAction.AddSelectTab(createTab(url = "url", id = "tab1")))
store.waitUntilIdle()
@ -66,10 +66,10 @@ class TabsTrayStoreTest {
@Test
fun `WHEN removing a tab THEN it is removed from the selectedTabs`() {
val store = TabsTrayStore()
val tabForRemoval = createTab("tab1")
val tabForRemoval = createTab(url = "url", id = "tab1")
store.dispatch(TabsTrayAction.AddSelectTab(tabForRemoval))
store.dispatch(TabsTrayAction.AddSelectTab(createTab("tab2")))
store.dispatch(TabsTrayAction.AddSelectTab(createTab(url = "url", id = "tab2")))
store.waitUntilIdle()

@ -8,9 +8,9 @@ import android.view.LayoutInflater
import android.view.View
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.base.images.ImageLoader
import mozilla.components.concept.tabstray.Tab
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertTrue
import org.junit.Test
@ -20,6 +20,7 @@ import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.selection.SelectionHolder
import org.mozilla.fenix.tabstray.TabsTrayStore
import mozilla.components.browser.state.state.createTab
@RunWith(FenixRobolectricTestRunner::class)
class AbstractBrowserTabViewHolderTest {
@ -40,11 +41,11 @@ class AbstractBrowserTabViewHolderTest {
interactor
)
holder.bind(createTab(), false, mockk(), mockk())
holder.bind(createTab(url = "url"), false, mockk(), mockk())
holder.itemView.performClick()
verify { interactor.open(any(), holder.featureName) }
verify { interactor.onTabSelected(any(), holder.featureName) }
}
@Test
@ -61,11 +62,11 @@ class AbstractBrowserTabViewHolderTest {
interactor
)
holder.bind(createTab(), false, mockk(), mockk())
holder.bind(createTab(url = "url"), false, mockk(), mockk())
holder.itemView.performClick()
verify { interactor.open(any(), holder.featureName) }
verify { interactor.onTabSelected(any(), holder.featureName) }
assertTrue(selectionHolder.invoked)
}
@ -74,7 +75,7 @@ class AbstractBrowserTabViewHolderTest {
itemView: View,
imageLoader: ImageLoader,
trayStore: TabsTrayStore,
selectionHolder: SelectionHolder<Tab>?,
selectionHolder: SelectionHolder<TabSessionState>?,
store: BrowserStore,
metrics: MetricController,
override val browserTrayInteractor: BrowserTrayInteractor,
@ -89,9 +90,9 @@ class AbstractBrowserTabViewHolderTest {
}
class TestSelectionHolder(
private val testItems: Set<Tab>
) : SelectionHolder<Tab> {
override val selectedItems: Set<Tab>
private val testItems: Set<TabSessionState>
) : SelectionHolder<TabSessionState> {
override val selectedItems: Set<TabSessionState>
get() {
invoked = true
return testItems

@ -9,10 +9,9 @@ import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import mozilla.components.browser.state.state.TabSessionState
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
@ -20,6 +19,7 @@ import org.mozilla.fenix.databinding.TabTrayItemBinding
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.selection.SelectionHolder
import org.mozilla.fenix.tabstray.TabsTrayStore
import mozilla.components.browser.state.state.createTab
@RunWith(FenixRobolectricTestRunner::class)
class BrowserTabsAdapterTest {
@ -34,12 +34,10 @@ class BrowserTabsAdapterTest {
val holder = mockk<AbstractBrowserTabViewHolder>(relaxed = true)
adapter.updateTabs(
Tabs(
list = listOf(
createTab("tab1")
),
selectedIndex = 0
)
listOf(
createTab(url = "url", id = "tab1")
),
selectedTabId = "tab1"
)
adapter.onBindViewHolder(holder, 0, listOf(PAYLOAD_HIGHLIGHT_SELECTED_ITEM))
@ -65,7 +63,7 @@ class BrowserTabsAdapterTest {
featureName = "Test"
)
)
val tab = createTab("tab1")
val tab = createTab(url = "url", id = "tab1")
every { holder.tab }.answers { tab }
@ -73,12 +71,8 @@ class BrowserTabsAdapterTest {
adapter.selectionHolder = testSelectionHolder
adapter.updateTabs(
Tabs(
list = listOf(
tab
),
selectedIndex = 0
)
listOf(tab),
selectedTabId = "tab1"
)
adapter.onBindViewHolder(holder, 0, listOf(PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM))
@ -86,10 +80,10 @@ class BrowserTabsAdapterTest {
verify { holder.showTabIsMultiSelectEnabled(any(), true) }
}
private val testSelectionHolder = object : SelectionHolder<Tab> {
override val selectedItems: Set<Tab>
private val testSelectionHolder = object : SelectionHolder<TabSessionState> {
override val selectedItems: Set<TabSessionState>
get() = internalState
val internalState = mutableSetOf<Tab>()
val internalState = mutableSetOf<TabSessionState>()
}
}

@ -12,7 +12,7 @@ import io.mockk.spyk
import io.mockk.verify
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.tabstray.TabsTray
import mozilla.components.browser.tabstray.TabsTray
import org.junit.Test
import org.mozilla.fenix.utils.Settings

@ -10,8 +10,7 @@ import io.mockk.verify
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.tabstray.Tabs
import mozilla.components.concept.tabstray.TabsTray
import mozilla.components.browser.tabstray.TabsTray
import org.junit.Assert.assertEquals
import mozilla.components.browser.state.state.createTab as createTabState
import org.junit.Test
@ -32,15 +31,15 @@ class InactiveTabsControllerTest {
)
)
val tray: TabsTray = mockk(relaxed = true)
val tabsSlot = slot<Tabs>()
val tabsSlot = slot<List<TabSessionState>>()
val controller = InactiveTabsController(store, filter, tray, mockk(relaxed = true))
controller.updateCardExpansion(true)
verify { tray.updateTabs(capture(tabsSlot)) }
verify { tray.updateTabs(capture(tabsSlot), any()) }
assertEquals(2, tabsSlot.captured.list.size)
assertEquals("1", tabsSlot.captured.list.first().id)
assertEquals(2, tabsSlot.captured.size)
assertEquals("1", tabsSlot.captured.first().id)
}
@Test

@ -1,22 +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 mozilla.components.concept.tabstray.Tab
import java.util.UUID
/**
* Helper for writing tests that need a [Tab].
*/
fun createTab(
tabId: String = UUID.randomUUID().toString(),
lastAccess: Long = 0L,
searchTerm: String = ""
) = Tab(
id = tabId,
url = "https://mozilla.org",
lastAccess = lastAccess,
searchTerm = searchTerm
)

@ -7,9 +7,7 @@ package org.mozilla.fenix.tabstray.browser
import androidx.recyclerview.widget.ConcatAdapter
import io.mockk.every
import io.mockk.mockk
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.tabstray.Tabs
import mozilla.components.browser.state.state.createTab
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
@ -43,26 +41,19 @@ class TabSorterTest {
@Test
fun `WHEN updated with one normal tab THEN adapter have only one normal tab and no header`() {
val store = BrowserStore(
BrowserState(
tabs = emptyList()
)
)
val adapter = ConcatAdapter(
InactiveTabsAdapter(context, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings),
TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME),
TitleHeaderAdapter(),
BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME)
)
val tabSorter = TabSorter(settings, metrics, adapter, store)
val tabSorter = TabSorter(settings, metrics, adapter)
tabSorter.updateTabs(
Tabs(
list = listOf(
createTab("tab1", System.currentTimeMillis())
),
selectedIndex = 0
)
listOf(
createTab(url = "url", id = "tab1", lastAccess = System.currentTimeMillis())
),
selectedTabId = "tab1"
)
assertEquals(adapter.itemCount, 1)
@ -74,28 +65,31 @@ class TabSorterTest {
@Test
fun `WHEN updated with one normal tab and two search term tab THEN adapter have normal tab and a search group`() {
val store = BrowserStore(
BrowserState(
tabs = emptyList()
)
)
val adapter = ConcatAdapter(
InactiveTabsAdapter(context, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings),
TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME),
TitleHeaderAdapter(),
BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME)
)
val tabSorter = TabSorter(settings, metrics, adapter, store)
val tabSorter = TabSorter(settings, metrics, adapter)
tabSorter.updateTabs(
Tabs(
list = listOf(
createTab("tab1", System.currentTimeMillis()),
createTab("tab2", System.currentTimeMillis(), searchTerm = "mozilla"),
createTab("tab3", System.currentTimeMillis(), searchTerm = "mozilla")
listOf(
createTab(url = "url", id = "tab1", lastAccess = System.currentTimeMillis()),
createTab(
url = "url",
id = "tab2",
lastAccess = System.currentTimeMillis(),
searchTerms = "mozilla"
),
selectedIndex = 0
)
createTab(
url = "url",
id = "tab3",
lastAccess = System.currentTimeMillis(),
searchTerms = "mozilla"
)
),
selectedTabId = "tab1"
)
assertEquals(adapter.itemCount, 3)
@ -107,29 +101,37 @@ class TabSorterTest {
@Test
fun `WHEN updated with one normal tab, one inactive tab and two search term tab THEN adapter have normal tab, inactive tab and a search group`() {
val store = BrowserStore(
BrowserState(
tabs = emptyList()
)
)
val adapter = ConcatAdapter(
InactiveTabsAdapter(context, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings),
TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME),
TitleHeaderAdapter(),
BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME)
)
val tabSorter = TabSorter(settings, metrics, adapter, store)
val tabSorter = TabSorter(settings, metrics, adapter)
tabSorter.updateTabs(
Tabs(
list = listOf(
createTab("tab1", System.currentTimeMillis()),
createTab("tab2", inactiveTimestamp),
createTab("tab3", System.currentTimeMillis(), searchTerm = "mozilla"),
createTab("tab4", System.currentTimeMillis(), searchTerm = "mozilla")
listOf(
createTab(url = "url", id = "tab1", lastAccess = System.currentTimeMillis()),
createTab(
url = "url",
id = "tab2",
lastAccess = inactiveTimestamp,
createdAt = inactiveTimestamp
),
selectedIndex = 0
)
createTab(
url = "url",
id = "tab3",
lastAccess = System.currentTimeMillis(),
searchTerms = "mozilla"
),
createTab(
url = "url",
id = "tab4",
lastAccess = System.currentTimeMillis(),
searchTerms = "mozilla"
)
),
selectedTabId = "tab1"
)
assertEquals(adapter.itemCount, 4)
@ -142,29 +144,37 @@ class TabSorterTest {
@Test
fun `WHEN inactive tabs is off THEN adapter have no inactive tab`() {
every { settings.inactiveTabsAreEnabled }.answers { false }
val store = BrowserStore(
BrowserState(
tabs = emptyList()
)
)
val adapter = ConcatAdapter(
InactiveTabsAdapter(context, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings),
TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME),
TitleHeaderAdapter(),
BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME)
)
val tabSorter = TabSorter(settings, metrics, adapter, store)
val tabSorter = TabSorter(settings, metrics, adapter)
tabSorter.updateTabs(
Tabs(
list = listOf(
createTab("tab1", System.currentTimeMillis()),
createTab("tab2", inactiveTimestamp),
createTab("tab3", System.currentTimeMillis(), searchTerm = "mozilla"),
createTab("tab4", System.currentTimeMillis(), searchTerm = "mozilla")
listOf(
createTab(url = "url", id = "tab1", lastAccess = System.currentTimeMillis()),
createTab(
url = "url",
id = "tab2",
lastAccess = inactiveTimestamp,
createdAt = inactiveTimestamp
),
selectedIndex = 0
)
createTab(
url = "url",
id = "tab3",
lastAccess = System.currentTimeMillis(),
searchTerms = "mozilla"
),
createTab(
url = "url",
id = "tab4",
lastAccess = System.currentTimeMillis(),
searchTerms = "mozilla"
)
),
selectedTabId = "tab1"
)
assertEquals(adapter.itemCount, 4)
@ -177,29 +187,37 @@ class TabSorterTest {
@Test
fun `WHEN search term tabs is off THEN adapter have no search term group`() {
every { settings.searchTermTabGroupsAreEnabled }.answers { false }
val store = BrowserStore(
BrowserState(
tabs = emptyList()
)
)
val adapter = ConcatAdapter(
InactiveTabsAdapter(context, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings),
TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME),
TitleHeaderAdapter(),
BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME)
)
val tabSorter = TabSorter(settings, metrics, adapter, store)
val tabSorter = TabSorter(settings, metrics, adapter)
tabSorter.updateTabs(
Tabs(
list = listOf(
createTab("tab1", System.currentTimeMillis()),
createTab("tab2", inactiveTimestamp),
createTab("tab3", System.currentTimeMillis(), searchTerm = "mozilla"),
createTab("tab4", System.currentTimeMillis(), searchTerm = "mozilla")
listOf(
createTab(url = "url", id = "tab1", lastAccess = System.currentTimeMillis()),
createTab(
url = "url",
id = "tab2",
lastAccess = inactiveTimestamp,
createdAt = inactiveTimestamp
),
selectedIndex = 0
)
createTab(
url = "url",
id = "tab3",
lastAccess = System.currentTimeMillis(),
searchTerms = "mozilla"
),
createTab(
url = "url",
id = "tab4",
lastAccess = System.currentTimeMillis(),
searchTerms = "mozilla"
)
),
selectedTabId = "tab1"
)
assertEquals(adapter.itemCount, 4)
@ -213,29 +231,36 @@ class TabSorterTest {
fun `WHEN both inactive tabs and search term tabs are off THEN adapter have only normal tabs`() {
every { settings.inactiveTabsAreEnabled }.answers { false }
every { settings.searchTermTabGroupsAreEnabled }.answers { false }
val store = BrowserStore(
BrowserState(
tabs = emptyList()
)
)
val adapter = ConcatAdapter(
InactiveTabsAdapter(context, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings),
TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME),
TitleHeaderAdapter(),
BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME)
)
val tabSorter = TabSorter(settings, metrics, adapter, store)
val tabSorter = TabSorter(settings, metrics, adapter)
tabSorter.updateTabs(
Tabs(
list = listOf(
createTab("tab1", System.currentTimeMillis()),
createTab("tab2", inactiveTimestamp),
createTab("tab3", System.currentTimeMillis(), searchTerm = "mozilla"),
createTab("tab4", System.currentTimeMillis(), searchTerm = "mozilla")
listOf(
createTab(url = "url", id = "tab1", lastAccess = System.currentTimeMillis()),
createTab(
url = "url",
id = "tab2",
lastAccess = inactiveTimestamp
),
selectedIndex = 0
)
createTab(
url = "url",
id = "tab3",
lastAccess = System.currentTimeMillis(),
searchTerms = "mozilla"
),
createTab(
url = "url",
id = "tab4",
lastAccess = System.currentTimeMillis(),
searchTerms = "mozilla"
)
),
selectedTabId = "tab1"
)
assertEquals(adapter.itemCount, 4)
@ -247,26 +272,22 @@ class TabSorterTest {
@Test
fun `WHEN only one search term tab THEN there is no search group`() {
val store = BrowserStore(
BrowserState(
tabs = emptyList()
)
)
val adapter = ConcatAdapter(
InactiveTabsAdapter(context, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings),
TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME),
TitleHeaderAdapter(),
BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME)
)
val tabSorter = TabSorter(settings, metrics, adapter, store)
val tabSorter = TabSorter(settings, metrics, adapter)
tabSorter.updateTabs(
Tabs(
list = listOf(
createTab("tab1", System.currentTimeMillis(), searchTerm = "mozilla")
),
selectedIndex = 0
)
listOf(
createTab(
url = "url", id = "tab1", lastAccess = System.currentTimeMillis(),
searchTerms = "mozilla"
)
),
selectedTabId = "tab1"
)
assertEquals(adapter.itemCount, 1)
@ -278,27 +299,26 @@ class TabSorterTest {
@Test
fun `WHEN remove second last one search term tab THEN search group is kept even if there's only one tab`() {
val store = BrowserStore(
BrowserState(
tabs = emptyList()
)
)
val adapter = ConcatAdapter(
InactiveTabsAdapter(context, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings),
TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME),
TitleHeaderAdapter(),
BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME)
)
val tabSorter = TabSorter(settings, metrics, adapter, store)
val tabSorter = TabSorter(settings, metrics, adapter)
tabSorter.updateTabs(
Tabs(
list = listOf(
createTab("tab1", System.currentTimeMillis(), searchTerm = "mozilla"),
createTab("tab2", System.currentTimeMillis(), searchTerm = "mozilla")
listOf(
createTab(
url = "url", id = "tab1", lastAccess = System.currentTimeMillis(),
searchTerms = "mozilla"
),
selectedIndex = 0
)
createTab(
url = "url", id = "tab2", lastAccess = System.currentTimeMillis(),
searchTerms = "mozilla"
)
),
selectedTabId = "tab1"
)
assertEquals(adapter.itemCount, 1)
@ -308,12 +328,13 @@ class TabSorterTest {
assertEquals(adapter.browserAdapter.itemCount, 0)
tabSorter.updateTabs(
Tabs(
list = listOf(
createTab("tab1", System.currentTimeMillis(), searchTerm = "mozilla")
),
selectedIndex = 0
)
listOf(
createTab(
url = "url", id = "tab1", lastAccess = System.currentTimeMillis(),
searchTerms = "mozilla"
)
),
selectedTabId = "tab1"
)
assertEquals(adapter.itemCount, 1)

@ -20,12 +20,17 @@ import org.mozilla.fenix.tabstray.viewholders.SyncedTabsPageViewHolder
@RunWith(FenixRobolectricTestRunner::class)
class TabsTouchHelperTest {
private val featureName = object : FeatureNameHolder {
override val featureName: String
get() = "featureName"
}
@Test
fun `movement flags remain unchanged if onSwipeToDelete is true`() {
val recyclerView = RecyclerView(testContext)
val layout = FrameLayout(testContext)
val viewHolder = SyncedTabsPageViewHolder(layout, mockk())
val callback = TouchCallback(mockk(), { true }, { false })
val callback = TouchCallback(mockk(), { true }, { false }, featureName)
assertEquals(0, callback.getDragDirs(recyclerView, viewHolder))
assertEquals(
@ -44,7 +49,7 @@ class TabsTouchHelperTest {
val recyclerView = RecyclerView(testContext)
val layout = FrameLayout(testContext)
val viewHolder = SyncedTabsPageViewHolder(layout, mockk())
val callback = TouchCallback(mockk(), { false }, { false })
val callback = TouchCallback(mockk(), { false }, { false }, featureName)
assertEquals(0, callback.getDragDirs(recyclerView, viewHolder))
assertEquals(

@ -7,11 +7,10 @@ package org.mozilla.fenix.tabstray.ext
import io.mockk.mockk
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.tabstray.Tab
import org.junit.Assert.assertEquals
import org.junit.Test
import org.mozilla.fenix.tabstray.browser.createTab
class BrowserStoreKtTest {
@ -26,9 +25,9 @@ class BrowserStoreKtTest {
)
)
val tabs = listOf<Tab>(
createTab("tab1"),
createTab("tab2")
val tabs = listOf(
createTab(url = "url", id = "tab1"),
createTab(url = "url", id = "tab2"),
)
val result = store.getTabSessionState(tabs)
@ -47,9 +46,9 @@ class BrowserStoreKtTest {
)
)
val tabs = listOf<Tab>(
createTab("tab1"),
createTab("tab2")
val tabs = listOf(
createTab(url = "url", id = "tab1"),
createTab(url = "url", id = "tab2"),
)
val result = store.getTabSessionState(tabs)

@ -9,8 +9,8 @@ import android.view.View.GONE
import android.view.View.VISIBLE
import android.widget.TextView
import io.mockk.mockk
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.tabstray.Tabs
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertTrue
import org.junit.Test
@ -22,7 +22,6 @@ import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.tabstray.browser.AbstractBrowserTrayList
import org.mozilla.fenix.tabstray.browser.BrowserTabsAdapter
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
import org.mozilla.fenix.tabstray.browser.createTab
@RunWith(FenixRobolectricTestRunner::class)
class AbstractBrowserPageViewHolderTest {
@ -43,14 +42,7 @@ class AbstractBrowserPageViewHolderTest {
viewHolder.bind(adapter)
viewHolder.attachedToWindow()
adapter.updateTabs(
Tabs(
list = listOf(
createTab("tab1")
),
selectedIndex = 0
)
)
adapter.updateTabs(listOf(createTab(url = "url", id = "tab1")), "tab1")
assertTrue(trayList.visibility == VISIBLE)
assertTrue(emptyList.visibility == GONE)
@ -67,12 +59,7 @@ class AbstractBrowserPageViewHolderTest {
viewHolder.bind(adapter)
viewHolder.attachedToWindow()
adapter.updateTabs(
Tabs(
list = emptyList(),
selectedIndex = 0
)
)
adapter.updateTabs(emptyList(), "")
assertTrue(trayList.visibility == GONE)
assertTrue(emptyList.visibility == VISIBLE)

Loading…
Cancel
Save