For #21898 - Remove search term tab groups from the Tabs Tray

This commit is contained in:
Noah Bond 2022-07-13 13:53:30 -07:00 committed by mergify[bot]
parent da4328e53f
commit 7e59a644d5
24 changed files with 17 additions and 1122 deletions

View File

@ -281,7 +281,6 @@ class TabsTrayFragment : AppCompatDialogFragment() {
tabsTrayStore
),
store = requireContext().components.core.store,
defaultTabPartitionsFilter = { tabPartitions -> tabPartitions[SEARCH_TERM_TAB_GROUPS] }
),
owner = this,
view = view

View File

@ -8,7 +8,6 @@ import androidx.annotation.VisibleForTesting
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
import org.mozilla.fenix.GleanMetrics.Metrics
import org.mozilla.fenix.GleanMetrics.SearchTerms
import org.mozilla.fenix.GleanMetrics.TabsTray
/**
@ -17,7 +16,6 @@ import org.mozilla.fenix.GleanMetrics.TabsTray
class TabsTrayMiddleware : Middleware<TabsTrayState, TabsTrayAction> {
private var shouldReportInactiveTabMetrics: Boolean = true
private var shouldReportSearchGroupMetrics: Boolean = true
override fun invoke(
context: MiddlewareContext<TabsTrayState, TabsTrayAction>,
@ -35,31 +33,6 @@ class TabsTrayMiddleware : Middleware<TabsTrayState, TabsTrayAction> {
Metrics.inactiveTabsCount.set(action.tabs.size.toLong())
}
}
is TabsTrayAction.UpdateTabPartitions -> {
if (shouldReportSearchGroupMetrics) {
shouldReportSearchGroupMetrics = false
val tabGroups = action.tabPartition?.tabGroups ?: emptyList()
SearchTerms.numberOfSearchTermGroup.record(
SearchTerms.NumberOfSearchTermGroupExtra(
tabGroups.size.toString()
)
)
if (tabGroups.isNotEmpty()) {
val tabsPerGroup = tabGroups.map { it.tabIds.size }
val averageTabsPerGroup = tabsPerGroup.average()
SearchTerms.averageTabsPerGroup.record(
SearchTerms.AverageTabsPerGroupExtra(
averageTabsPerGroup.toString()
)
)
val tabGroupSizeMapping = tabsPerGroup.map { generateTabGroupSizeMappedValue(it) }
SearchTerms.groupSizeDistribution.accumulateSamples(tabGroupSizeMapping.toList())
}
}
}
is TabsTrayAction.EnterSelectMode -> {
TabsTray.enterMultiselectMode.record(TabsTray.EnterMultiselectModeExtra(false))
}

View File

@ -5,7 +5,6 @@
package org.mozilla.fenix.tabstray
import mozilla.components.browser.state.state.ContentState
import mozilla.components.browser.state.state.TabPartition
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.Middleware
@ -20,7 +19,6 @@ import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsListItem
* @property mode Whether the browser tab list is in multi-select mode or not with the set of
* currently selected tabs.
* @property inactiveTabs The list of tabs are considered inactive.
* @property searchTermPartition The tab partition for search term groups.
* @property normalTabs The list of normal tabs that do not fall under [inactiveTabs] or search term groups.
* @property privateTabs The list of tabs that are [ContentState.private].
* @property syncing Whether the Synced Tabs feature should fetch the latest tabs from paired devices.
@ -30,7 +28,6 @@ data class TabsTrayState(
val selectedPage: Page = Page.NormalTabs,
val mode: Mode = Mode.Normal,
val inactiveTabs: List<TabSessionState> = emptyList(),
val searchTermPartition: TabPartition? = null,
val normalTabs: List<TabSessionState> = emptyList(),
val privateTabs: List<TabSessionState> = emptyList(),
val syncedTabs: List<SyncedTabsListItem> = emptyList(),
@ -143,11 +140,6 @@ sealed class TabsTrayAction : Action {
*/
data class UpdateInactiveTabs(val tabs: List<TabSessionState>) : TabsTrayAction()
/**
* Updates the list of tab groups in [TabsTrayState.searchTermPartition].
*/
data class UpdateTabPartitions(val tabPartition: TabPartition?) : TabsTrayAction()
/**
* Updates the list of tabs in [TabsTrayState.normalTabs].
*/
@ -196,8 +188,6 @@ internal object TabsTrayReducer {
state.copy(focusGroupTabId = null)
is TabsTrayAction.UpdateInactiveTabs ->
state.copy(inactiveTabs = action.tabs)
is TabsTrayAction.UpdateTabPartitions ->
state.copy(searchTermPartition = action.tabPartition)
is TabsTrayAction.UpdateNormalTabs ->
state.copy(normalTabs = action.tabs)
is TabsTrayAction.UpdatePrivateTabs ->

View File

@ -18,8 +18,6 @@ import org.mozilla.fenix.tabstray.browser.BrowserTabsAdapter
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
import org.mozilla.fenix.tabstray.browser.InactiveTabsAdapter
import org.mozilla.fenix.tabstray.browser.InactiveTabsInteractor
import org.mozilla.fenix.tabstray.browser.TabGroupAdapter
import org.mozilla.fenix.tabstray.browser.TitleHeaderAdapter
import org.mozilla.fenix.tabstray.viewholders.AbstractPageViewHolder
import org.mozilla.fenix.tabstray.viewholders.NormalBrowserPageViewHolder
import org.mozilla.fenix.tabstray.viewholders.PrivateBrowserPageViewHolder
@ -51,8 +49,6 @@ class TrayPagerAdapter(
inactiveTabsInteractor = inactiveTabsInteractor,
featureName = INACTIVE_TABS_FEATURE_NAME,
),
TabGroupAdapter(context, browserInteractor, tabsTrayStore, TAB_GROUP_FEATURE_NAME, lifecycleOwner),
TitleHeaderAdapter(),
BrowserTabsAdapter(context, browserInteractor, tabsTrayStore, TABS_TRAY_FEATURE_NAME, lifecycleOwner)
)
}
@ -139,7 +135,6 @@ class TrayPagerAdapter(
// Telemetry keys for identifying from which app features the a was opened / closed.
const val TABS_TRAY_FEATURE_NAME = "Tabs tray"
const val TAB_GROUP_FEATURE_NAME = "Tab group"
const val INACTIVE_TABS_FEATURE_NAME = "Inactive tabs"
val POSITION_NORMAL_TABS = Page.NormalTabs.ordinal

View File

@ -37,7 +37,6 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.ext.removeAndDisable
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.selection.SelectionHolder
@ -245,11 +244,8 @@ abstract class AbstractBrowserTabViewHolder(
val touchStart = touchStartPoint
val selected = holder.selectedItems
val selectsOnlyThis = (selected.size == 1 && selected.contains(item))
val featureEnabled = FeatureFlags.tabReorderingFeature &&
!itemView.context.settings().searchTermTabGroupsAreEnabled
if (featureEnabled && selectsOnlyThis && touchStart != null) {
// In a tab group, we do not use a AbstractBrowserTrayList as the parent,
// so we should return early and mark the event as unhandled (return false).
if (FeatureFlags.tabReorderingFeature && selectsOnlyThis && touchStart != null) {
// If the parent is null then return early and mark the event as unhandled
val parent = itemView.parent as? AbstractBrowserTrayList ?: return@setOnTouchListener false
// Prevent scrolling if the user tries to start drag vertically

View File

@ -10,8 +10,6 @@ import androidx.recyclerview.widget.ConcatAdapter
import mozilla.components.browser.tabstray.TabViewHolder
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.tabstray.ext.browserAdapter
import org.mozilla.fenix.tabstray.ext.tabGroupAdapter
import org.mozilla.fenix.tabstray.ext.titleHeaderAdapter
class NormalBrowserTrayList @JvmOverloads constructor(
context: Context,
@ -25,14 +23,6 @@ class NormalBrowserTrayList @JvmOverloads constructor(
NormalTabsBinding(tabsTrayStore, context.components.core.store, concatAdapter.browserAdapter)
}
private val titleHeaderBinding by lazy {
OtherHeaderBinding(tabsTrayStore) { concatAdapter.titleHeaderAdapter.handleListChanges(it) }
}
private val tabGroupBinding by lazy {
TabGroupBinding(tabsTrayStore) { concatAdapter.tabGroupAdapter.submitList(it) }
}
private val touchHelper by lazy {
TabsTouchHelper(
interactionDelegate = concatAdapter.browserAdapter.interactor,
@ -48,8 +38,6 @@ class NormalBrowserTrayList @JvmOverloads constructor(
super.onAttachedToWindow()
normalTabsBinding.start()
titleHeaderBinding.start()
tabGroupBinding.start()
touchHelper.attachToRecyclerView(this)
}
@ -58,8 +46,6 @@ class NormalBrowserTrayList @JvmOverloads constructor(
super.onDetachedFromWindow()
normalTabsBinding.stop()
titleHeaderBinding.stop()
tabGroupBinding.stop()
touchHelper.attachToRecyclerView(null)
}

View File

@ -22,10 +22,9 @@ class NormalTabsBinding(
private val tabsTray: TabsTray
) : AbstractBinding<TabsTrayState>(store) {
override suspend fun onState(flow: Flow<TabsTrayState>) {
flow.ifChanged { Pair(it.normalTabs, it.searchTermPartition) }
flow.ifChanged { it.normalTabs }
.collect {
// Getting the selectedTabId from the BrowserStore at a different time might lead to a race.
tabsTray.updateTabs(it.normalTabs, it.searchTermPartition, browserStore.state.selectedTabId)
tabsTray.updateTabs(it.normalTabs, null, browserStore.state.selectedTabId)
}
}
}

View File

@ -1,35 +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 kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import mozilla.components.browser.state.state.isNotEmpty
import mozilla.components.lib.state.helpers.AbstractBinding
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.tabstray.TabsTrayState
import org.mozilla.fenix.tabstray.TabsTrayStore
/**
* A tabs observer that informs [showHeader] if an "Other tabs" title should be displayed in the tray.
*/
class OtherHeaderBinding(
store: TabsTrayStore,
private val showHeader: (Boolean) -> Unit
) : AbstractBinding<TabsTrayState>(store) {
override suspend fun onState(flow: Flow<TabsTrayState>) {
flow.ifChanged { Pair(it.normalTabs, it.searchTermPartition) }
.collect {
if (
it.normalTabs.isNotEmpty() &&
it.searchTermPartition.isNotEmpty()
) {
showHeader(true)
} else {
showHeader(false)
}
}
}
}

View File

@ -1,100 +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.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
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.browser.state.state.TabGroup
import mozilla.components.browser.state.state.TabPartition
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
/**
* The [ListAdapter] for displaying the list of search term 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 viewLifecycleOwner [LifecycleOwner] life cycle owner for the view.
*/
@Suppress("TooManyFunctions")
class TabGroupAdapter(
private val context: Context,
private val browserTrayInteractor: BrowserTrayInteractor,
private val store: TabsTrayStore,
override val featureName: String,
private val viewLifecycleOwner: LifecycleOwner
) : ListAdapter<TabGroup, TabGroupViewHolder>(DiffCallback), TabsTray, FeatureNameHolder {
/**
* Tracks the selected tabs in multi-select mode.
*/
var selectionHolder: SelectionHolder<TabSessionState>? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabGroupViewHolder {
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
val orientation = if (context.components.settings.gridTabView) {
HORIZONTAL
} else {
VERTICAL
}
return TabGroupViewHolder(
view,
orientation,
browserTrayInteractor,
store,
selectionHolder,
viewLifecycleOwner
)
}
override fun onBindViewHolder(holder: TabGroupViewHolder, position: Int) {
val group = getItem(position)
holder.bind(group)
}
override fun getItemViewType(position: Int) = TabGroupViewHolder.LAYOUT_ID
/**
* Notify the nested [RecyclerView] when this view has been attached.
*/
override fun onViewAttachedToWindow(holder: TabGroupViewHolder) {
holder.rebind()
}
/**
* Notify the nested [RecyclerView] when this view has been detached.
*/
override fun onViewDetachedFromWindow(holder: TabGroupViewHolder) {
holder.unbind()
}
/**
* Not implemented; implementation is handled [List<Tab>.toSearchGroups]
*/
override fun updateTabs(tabs: List<TabSessionState>, tabPartition: TabPartition?, selectedTabId: String?) =
throw UnsupportedOperationException("Use submitList instead.")
private object DiffCallback : DiffUtil.ItemCallback<TabGroup>() {
override fun areItemsTheSame(oldItem: TabGroup, newItem: TabGroup) = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: TabGroup, newItem: TabGroup) = oldItem == newItem
}
}
internal fun TabGroup.containsTabId(tabId: String): Boolean {
return tabIds.contains(tabId)
}

View File

@ -1,30 +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 kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.state.TabGroup
import mozilla.components.lib.state.helpers.AbstractBinding
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.tabstray.TabsTrayState
import org.mozilla.fenix.tabstray.TabsTrayStore
/**
* A search-term tab group observer that updates the provided [tray].
*/
class TabGroupBinding(
store: TabsTrayStore,
private val tray: (List<TabGroup>) -> Unit
) : AbstractBinding<TabsTrayState>(store) {
override suspend fun onState(flow: Flow<TabsTrayState>) {
flow.map { it.searchTermPartition?.tabGroups ?: emptyList() }
.ifChanged()
.collect {
tray.invoke(it.filter { tabGroup -> tabGroup.tabIds.isNotEmpty() })
}
}
}

View File

@ -1,183 +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.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.LifecycleOwner
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.SelectableTabViewHolder
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 org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.TabTrayGridItemBinding
import org.mozilla.fenix.databinding.TabTrayItemBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.home.topsites.dpToPx
import org.mozilla.fenix.selection.SelectionHolder
import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.tabstray.browser.compose.ComposeListViewHolder
import org.mozilla.fenix.tabstray.ext.MIN_COLUMN_WIDTH_DP
/**
* The [ListAdapter] for displaying the list of tabs that have the same search term.
*
* @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 featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting.
* @param viewLifecycleOwner [LifecycleOwner] life cycle owner for the view.
*/
class TabGroupListAdapter(
private val context: Context,
private val interactor: BrowserTrayInteractor,
private val store: TabsTrayStore,
private val selectionHolder: SelectionHolder<TabSessionState>?,
private val featureName: String,
private val viewLifecycleOwner: LifecycleOwner
) : ListAdapter<TabSessionState, SelectableTabViewHolder>(DiffCallback) {
private val selectedItemAdapterBinding = SelectedItemAdapterBinding(store, this)
private val imageLoader = ThumbnailLoader(context.components.core.thumbnailStorage)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): SelectableTabViewHolder {
return when {
context.components.settings.gridTabView -> {
val view = LayoutInflater.from(parent.context).inflate(R.layout.tab_tray_grid_item, parent, false)
view.layoutParams.width = view.dpToPx(MIN_COLUMN_WIDTH_DP.toFloat())
BrowserTabViewHolder.GridViewHolder(imageLoader, interactor, store, selectionHolder, view, featureName)
}
else -> {
if (FeatureFlags.composeTabsTray) {
ComposeListViewHolder(
interactor = interactor,
tabsTrayStore = store,
selectionHolder = selectionHolder,
composeItemView = ComposeView(parent.context),
featureName = featureName,
viewLifecycleOwner = viewLifecycleOwner
)
} else {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.tab_tray_item, parent, false)
BrowserTabViewHolder.ListViewHolder(
imageLoader,
interactor,
store,
selectionHolder,
view,
featureName
)
}
}
}
}
override fun onBindViewHolder(holder: SelectableTabViewHolder, position: Int) {
val tab = getItem(position)
val selectedTabId = context.components.core.store.state.selectedTabId
holder.bind(tab, tab.id == selectedTabId, TabsTrayStyling(), interactor)
holder.tab?.let { holderTab ->
when {
context.components.settings.gridTabView -> {
val gridBinding = TabTrayGridItemBinding.bind(holder.itemView)
gridBinding.mozacBrowserTabstrayClose.setOnClickListener {
interactor.close(holderTab, featureName)
}
}
else -> {
val listBinding = TabTrayItemBinding.bind(holder.itemView)
listBinding.mozacBrowserTabstrayClose.setOnClickListener {
interactor.close(holderTab, featureName)
}
}
}
}
}
/**
* Over-ridden [onBindViewHolder] that uses the payloads to notify the selected tab how to
* display itself.
*
* N.B: this is a modified version of [BrowserTabsAdapter.onBindViewHolder].
*/
override fun onBindViewHolder(holder: SelectableTabViewHolder, position: Int, payloads: List<Any>) {
val tabs = currentList
val selectedTabId = context.components.core.store.state.selectedTabId
val selectedIndex = tabs.indexOfFirst { it.id == selectedTabId }
if (tabs.isEmpty()) return
if (payloads.isEmpty()) {
onBindViewHolder(holder, position)
return
}
if (position == 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 {
var selectedMaskView: View? = null
when (getItemViewType(position)) {
BrowserTabsAdapter.ViewType.GRID.layoutRes -> {
val gridBinding = TabTrayGridItemBinding.bind(holder.itemView)
selectedMaskView = gridBinding.checkboxInclude.selectedMask
}
BrowserTabsAdapter.ViewType.LIST.layoutRes -> {
val listBinding = TabTrayItemBinding.bind(holder.itemView)
selectedMaskView = listBinding.checkboxInclude.selectedMask
}
}
holder.showTabIsMultiSelectEnabled(
selectedMaskView,
it.selectedItems.contains(holder.tab)
)
}
}
override fun getItemViewType(position: Int): Int {
return when {
context.components.settings.gridTabView -> {
BrowserTabsAdapter.ViewType.GRID.layoutRes
}
else -> {
if (FeatureFlags.composeTabsTray) {
BrowserTabsAdapter.ViewType.COMPOSE_LIST.layoutRes
} else {
BrowserTabsAdapter.ViewType.LIST.layoutRes
}
}
}
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
selectedItemAdapterBinding.start()
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
selectedItemAdapterBinding.stop()
}
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
}
}

View File

@ -1,89 +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 androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.state.TabGroup
import mozilla.components.browser.state.state.TabSessionState
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.TabGroupItemBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.selection.SelectionHolder
import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.tabstray.TrayPagerAdapter
/**
* A RecyclerView ViewHolder implementation for tab group items.
*
* @param itemView [View] that displays a "tab".
* @param orientation [Int] orientation of the items. Horizontal for grid layout, vertical for list layout
* @param interactor the [BrowserTrayInteractor] for tab interactions.
* @param store the [TabsTrayStore] instance.
* @param selectionHolder the store that holds the currently selected tabs.
* @param viewLifecycleOwner [LifecycleOwner] life cycle owner for the view.
*/
class TabGroupViewHolder(
itemView: View,
val orientation: Int,
val interactor: BrowserTrayInteractor,
val store: TabsTrayStore,
val selectionHolder: SelectionHolder<TabSessionState>? = null,
private val viewLifecycleOwner: LifecycleOwner
) : RecyclerView.ViewHolder(itemView) {
private val binding = TabGroupItemBinding.bind(itemView)
private lateinit var groupListAdapter: TabGroupListAdapter
fun bind(
tabGroup: TabGroup,
) {
val selectedTabId = itemView.context.components.core.store.state.selectedTabId
val selectedIndex = tabGroup.tabIds.indexOfFirst { it == selectedTabId }
binding.tabGroupTitle.text = tabGroup.id
binding.tabGroupList.apply {
layoutManager = LinearLayoutManager(itemView.context, orientation, false)
groupListAdapter = TabGroupListAdapter(
context = itemView.context,
interactor = interactor,
store = store,
selectionHolder = selectionHolder,
featureName = TrayPagerAdapter.TAB_GROUP_FEATURE_NAME,
viewLifecycleOwner
)
adapter = groupListAdapter
val tabGroupTabs = itemView.context.components.core.store.state.normalTabs.filter {
tabGroup.tabIds.contains(it.id)
}
groupListAdapter.submitList(tabGroupTabs)
scrollToPosition(selectedIndex)
}
}
/**
* Notify the nested [RecyclerView] that it has been detached.
*/
fun unbind() {
groupListAdapter.onDetachedFromRecyclerView(binding.tabGroupList)
}
/**
* Notify the nested [RecyclerView] that it has been attached. This is so our observers know when to start again.
*/
fun rebind() {
groupListAdapter.onAttachedToRecyclerView(binding.tabGroupList)
}
companion object {
const val LAYOUT_ID = R.layout.tab_group_item
}
}

View File

@ -4,13 +4,11 @@
package org.mozilla.fenix.tabstray.browser
import mozilla.components.browser.state.state.TabGroup
import mozilla.components.browser.state.state.TabPartition
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.tabstray.TabsTray
import mozilla.components.feature.tabs.tabstray.TabsFeature
import org.mozilla.fenix.ext.maxActiveTime
import org.mozilla.fenix.tabstray.SEARCH_TERM_TAB_GROUPS_MIN_SIZE
import org.mozilla.fenix.tabstray.TabsTrayAction
import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.tabstray.ext.isActive
@ -23,16 +21,12 @@ class TabSorter(
private val settings: Settings,
private val tabsTrayStore: TabsTrayStore? = null
) : TabsTray {
private val groupsSet = mutableSetOf<String>()
override fun updateTabs(tabs: List<TabSessionState>, tabPartition: TabPartition?, selectedTabId: String?) {
val privateTabs = tabs.filter { it.content.private }
val allNormalTabs = tabs - privateTabs
val inactiveTabs = allNormalTabs.getInactiveTabs(settings)
val tabGroups = tabPartition?.getTabGroups(settings, groupsSet) ?: emptyList()
val tabGroupTabIds = tabGroups.flatMap { it.tabIds }
val normalTabs = (allNormalTabs - inactiveTabs).filterNot { tabGroupTabIds.contains(it.id) }
val minTabPartition = tabPartition?.let { TabPartition(tabPartition.id, tabGroups) }
val normalTabs = allNormalTabs - inactiveTabs
// Private tabs
tabsTrayStore?.dispatch(TabsTrayAction.UpdatePrivateTabs(privateTabs))
@ -42,12 +36,6 @@ class TabSorter(
// Normal tabs
tabsTrayStore?.dispatch(TabsTrayAction.UpdateNormalTabs(normalTabs))
// Search term tabs
tabsTrayStore?.dispatch(TabsTrayAction.UpdateTabPartitions(minTabPartition))
groupsSet.clear()
groupsSet.addAll(tabGroups.map { it.id })
}
}
@ -62,16 +50,3 @@ private fun List<TabSessionState>.getInactiveTabs(settings: Settings): List<TabS
emptyList()
}
}
/**
* Returns a list of tab groups based on our preferences.
*/
private fun TabPartition.getTabGroups(settings: Settings, groupsSet: Set<String>): List<TabGroup> {
return if (settings.searchTermTabGroupsAreEnabled) {
tabGroups.filter {
it.tabIds.size >= SEARCH_TERM_TAB_GROUPS_MIN_SIZE || groupsSet.contains(it.id)
}
} else {
emptyList()
}
}

View File

@ -1,61 +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.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.TabTrayTitleHeaderItemBinding
/**
* A [RecyclerView.Adapter] for tab header.
*/
class TitleHeaderAdapter :
ListAdapter<TitleHeaderAdapter.Header, TitleHeaderAdapter.HeaderViewHolder>(DiffCallback) {
class Header
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return HeaderViewHolder(view)
}
override fun getItemViewType(position: Int) = HeaderViewHolder.LAYOUT_ID
/* Do nothing */
override fun onBindViewHolder(holder: HeaderViewHolder, position: Int) = Unit
fun handleListChanges(showHeader: Boolean) {
val header = if (showHeader) {
listOf(Header())
} else {
emptyList()
}
submitList(header)
}
class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val binding = TabTrayTitleHeaderItemBinding.bind(itemView)
fun bind() {
binding.tabTrayHeaderTitle.text =
itemView.context.getString(R.string.tab_tray_header_title_1)
}
companion object {
const val LAYOUT_ID = R.layout.tab_tray_title_header_item
}
}
private object DiffCallback : DiffUtil.ItemCallback<Header>() {
override fun areItemsTheSame(oldItem: Header, newItem: Header) = true
override fun areContentsTheSame(oldItem: Header, newItem: Header) = true
}
}

View File

@ -6,9 +6,7 @@ package org.mozilla.fenix.tabstray.ext
import androidx.recyclerview.widget.ConcatAdapter
import org.mozilla.fenix.tabstray.browser.BrowserTabsAdapter
import org.mozilla.fenix.tabstray.browser.TitleHeaderAdapter
import org.mozilla.fenix.tabstray.browser.InactiveTabsAdapter
import org.mozilla.fenix.tabstray.browser.TabGroupAdapter
/**
* A convenience binding for retrieving the [BrowserTabsAdapter] from the [ConcatAdapter].
@ -21,15 +19,3 @@ internal val ConcatAdapter.browserAdapter
*/
internal val ConcatAdapter.inactiveTabsAdapter
get() = adapters.find { it is InactiveTabsAdapter } as InactiveTabsAdapter
/**
* A convenience binding for retrieving the [TabGroupAdapter] from the [ConcatAdapter].
*/
internal val ConcatAdapter.tabGroupAdapter
get() = adapters.find { it is TabGroupAdapter } as TabGroupAdapter
/**
* A convenience binding for retrieving the [TitleHeaderAdapter] from the [ConcatAdapter].
*/
internal val ConcatAdapter.titleHeaderAdapter
get() = adapters.find { it is TitleHeaderAdapter } as TitleHeaderAdapter

View File

@ -7,11 +7,8 @@ package org.mozilla.fenix.tabstray.ext
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabGroup
import mozilla.components.browser.state.state.TabSessionState
import org.mozilla.fenix.ext.maxActiveTime
import org.mozilla.fenix.tabstray.SEARCH_TERM_TAB_GROUPS
import org.mozilla.fenix.tabstray.SEARCH_TERM_TAB_GROUPS_MIN_SIZE
/**
* The currently selected tab if there's one that is private.
@ -38,24 +35,13 @@ fun BrowserState.findPrivateTab(tabId: String): TabSessionState? {
* The list of normal tabs in the tabs tray filtered appropriately based on feature flags.
*/
fun BrowserState.getNormalTrayTabs(
searchTermTabGroupsAreEnabled: Boolean,
inactiveTabsEnabled: Boolean
): List<TabSessionState> {
val tabGroupsTabIds = getTabGroups()?.flatMap { it.tabIds } ?: emptyList()
return normalTabs.run {
if (searchTermTabGroupsAreEnabled && tabGroupsTabIds.isNotEmpty() && inactiveTabsEnabled) {
filter { it.isNormalTabActive(maxActiveTime) }.filter { tabGroupsTabIds.contains(it.id) }
} else if (inactiveTabsEnabled) {
if (inactiveTabsEnabled) {
filter { it.isNormalTabActive(maxActiveTime) }
} else if (searchTermTabGroupsAreEnabled && tabGroupsTabIds.isNotEmpty()) {
filter { it.isNormalTab() }.filter { tabGroupsTabIds.contains(it.id) }
} else {
this
}
}
}
fun BrowserState.getTabGroups(): List<TabGroup>? {
return tabPartitions[SEARCH_TERM_TAB_GROUPS]?.tabGroups
?.filter { it.tabIds.size >= SEARCH_TERM_TAB_GROUPS_MIN_SIZE }
}

View File

@ -23,19 +23,14 @@ import org.mozilla.fenix.ext.maxActiveTime
import org.mozilla.fenix.ext.potentialInactiveTabs
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.selection.SelectionHolder
import org.mozilla.fenix.tabstray.TabsTrayAction
import org.mozilla.fenix.tabstray.TabsTrayInteractor
import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.tabstray.browser.containsTabId
import org.mozilla.fenix.tabstray.ext.browserAdapter
import org.mozilla.fenix.tabstray.ext.defaultBrowserLayoutColumns
import org.mozilla.fenix.tabstray.ext.getNormalTrayTabs
import org.mozilla.fenix.tabstray.ext.inactiveTabsAdapter
import org.mozilla.fenix.tabstray.ext.isNormalTabActiveWithSearchTerm
import org.mozilla.fenix.tabstray.ext.isNormalTabInactive
import org.mozilla.fenix.tabstray.ext.observeFirstInsert
import org.mozilla.fenix.tabstray.ext.tabGroupAdapter
import org.mozilla.fenix.tabstray.ext.titleHeaderAdapter
/**
* View holder for the normal tabs tray list.
@ -68,10 +63,8 @@ class NormalBrowserPageViewHolder(
) {
val concatAdapter = adapter as ConcatAdapter
val browserAdapter = concatAdapter.browserAdapter
val tabGroupAdapter = concatAdapter.tabGroupAdapter
val manager = setupLayoutManager(containerView.context, concatAdapter)
browserAdapter.selectionHolder = this
tabGroupAdapter.selectionHolder = this
observeTabsTrayInactiveTabsState(adapter)
@ -86,12 +79,9 @@ class NormalBrowserPageViewHolder(
layoutManager: RecyclerView.LayoutManager
) {
val concatAdapter = adapter as ConcatAdapter
val headerAdapter = concatAdapter.titleHeaderAdapter
val browserAdapter = concatAdapter.browserAdapter
val inactiveTabAdapter = concatAdapter.inactiveTabsAdapter
val tabGroupAdapter = concatAdapter.tabGroupAdapter
val inactiveTabsAreEnabled = containerView.context.settings().inactiveTabsAreEnabled
val searchTermTabGroupsAreEnabled = containerView.context.settings().searchTermTabGroupsAreEnabled
val selectedTab = browserStore.state.selectedNormalTab ?: return
// It's safe to read the state directly (i.e. won't cause bugs because of the store actions
@ -117,53 +107,14 @@ class NormalBrowserPageViewHolder(
}
}
// Updates tabs into the search term group adapter.
if (searchTermTabGroupsAreEnabled && (
!focusGroupTabId.isNullOrEmpty() ||
selectedTab.isNormalTabActiveWithSearchTerm(maxActiveTime)
)
) {
val tabId = focusGroupTabId ?: selectedTab.id
tabGroupAdapter.observeFirstInsert {
// With a grouping, we need to use the list of the adapter that is already grouped
// together for the UI, so we know the final index of the grouping to scroll to.
//
// N.B: Why are we using currentList here and no where else? `currentList` is an API on top of
// `ListAdapter` which is updated when the [ListAdapter.submitList] is invoked. For our BrowserAdapter
// as an example, the updates are coming from [TabsFeature] which internally uses the internal
// [DiffUtil.calculateDiff] directly to submit a changed list which evades the `ListAdapter` from being
// notified of updates, so it therefore returns an empty list.
tabGroupAdapter.currentList.forEachIndexed { groupIndex, group ->
if (group.containsTabId(tabId)) {
// Index is based on tabs above (inactive) with our calculated index.
val indexToScrollTo = inactiveTabAdapter.itemCount + groupIndex
containerView.post { layoutManager.scrollToPosition(indexToScrollTo) }
if (focusGroupTabId != null) {
tabsTrayStore.dispatch(TabsTrayAction.ConsumeFocusGroupTabId)
}
return@observeFirstInsert
}
}
}
}
if (focusGroupTabId.isNullOrEmpty()) {
// Updates tabs into the normal browser tabs adapter.
browserAdapter.observeFirstInsert {
val activeTabsList = browserStore.state.getNormalTrayTabs(
searchTermTabGroupsAreEnabled,
inactiveTabsAreEnabled
)
val activeTabsList = browserStore.state.getNormalTrayTabs(inactiveTabsAreEnabled)
activeTabsList.forEachIndexed { tabIndex, trayTab ->
if (trayTab.id == selectedTab.id) {
// Index is based on tabs above (inactive + groups + header) with our calculated index.
val indexToScrollTo = inactiveTabAdapter.itemCount +
tabGroupAdapter.itemCount +
headerAdapter.itemCount + tabIndex
// Index is based on tabs above (inactive) with our calculated index.
val indexToScrollTo = inactiveTabAdapter.itemCount + tabIndex
containerView.post { layoutManager.scrollToPosition(indexToScrollTo) }
@ -196,17 +147,12 @@ class NormalBrowserPageViewHolder(
context: Context,
concatAdapter: ConcatAdapter
): GridLayoutManager {
val headerAdapter = concatAdapter.titleHeaderAdapter
val inactiveTabAdapter = concatAdapter.inactiveTabsAdapter
val tabGroupAdapter = concatAdapter.tabGroupAdapter
val numberOfColumns = containerView.context.defaultBrowserLayoutColumns
return GridLayoutManager(context, numberOfColumns).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return if (position >= inactiveTabAdapter.itemCount + tabGroupAdapter.itemCount +
headerAdapter.itemCount
) {
return if (position >= inactiveTabAdapter.itemCount) {
1
} else {
numberOfColumns

View File

@ -1,49 +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/. -->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_marginBottom="15dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/group_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/fx_mobile_icon_color_primary"
app:srcCompat="@drawable/ic_search" />
<TextView
android:id="@+id/tab_group_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="11dp"
android:ellipsize="end"
android:gravity="start"
android:maxLines="1"
android:textAppearance="@style/Header16TextStyle"
android:textColor="@color/fx_mobile_text_color_primary"
app:layout_constraintBottom_toBottomOf="@id/group_icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/group_icon"
app:layout_constraintTop_toTopOf="@id/group_icon"
tools:text="Cats" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tab_group_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tab_group_title" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/tab_tray_header_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:clickable="false"
android:clipToPadding="false"
android:ellipsize="end"
android:focusable="false"
android:gravity="start"
android:maxLines="1"
android:text="@string/tab_tray_header_title_1"
android:textAppearance="@style/Header16TextStyle"
android:textColor="@color/fx_mobile_text_color_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

View File

@ -722,7 +722,7 @@
<!-- Postfix for private WebApp titles, placeholder is replaced with app name -->
<string name="pwa_site_controls_title_private">%1$s (Private Mode)</string>
<!-- Title text for the normal tabs header in the tabs tray which are not part of any tab grouping. -->
<string name="tab_tray_header_title_1">Other tabs</string>
<string name="tab_tray_header_title_1" moz:removedIn="104" tools:ignore="UnusedResources">Other tabs</string>
<!-- History -->
<!-- Text for the button to search all history -->

View File

@ -4,10 +4,7 @@
package org.mozilla.fenix.tabstray
import io.mockk.every
import io.mockk.mockk
import mozilla.components.browser.state.state.TabGroup
import mozilla.components.browser.state.state.TabPartition
import mozilla.components.service.glean.testing.GleanTestRule
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.robolectric.testContext
@ -19,7 +16,6 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.GleanMetrics.Metrics
import org.mozilla.fenix.GleanMetrics.SearchTerms
import org.mozilla.fenix.GleanMetrics.TabsTray
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@ -41,45 +37,6 @@ class TabsTrayMiddlewareTest {
)
}
@Test
fun `WHEN search term groups are updated AND there is at least one group THEN report the average tabs per group`() {
assertNull(SearchTerms.averageTabsPerGroup.testGetValue())
store.dispatch(TabsTrayAction.UpdateTabPartitions(generateSearchTermTabGroupsForAverage()))
store.waitUntilIdle()
assertNotNull(SearchTerms.averageTabsPerGroup.testGetValue())
val event = SearchTerms.averageTabsPerGroup.testGetValue()!!
assertEquals(1, event.size)
assertEquals("5.0", event.single().extra!!["count"])
}
@Test
fun `WHEN search term groups are updated AND there is at least one group THEN report the distribution of tab sizes`() {
assertNull(SearchTerms.groupSizeDistribution.testGetValue())
store.dispatch(TabsTrayAction.UpdateTabPartitions(generateSearchTermTabGroupsForDistribution()))
store.waitUntilIdle()
assertNotNull(SearchTerms.groupSizeDistribution.testGetValue())
val event = SearchTerms.groupSizeDistribution.testGetValue()!!.values
// Verify the distribution correctly describes the tab group sizes
assertEquals(mapOf(0L to 0L, 1L to 1L, 2L to 1L, 3L to 1L, 4L to 1L), event)
}
@Test
fun `WHEN search term groups are updated THEN report the count of search term tab groups`() {
assertNull(SearchTerms.numberOfSearchTermGroup.testGetValue())
store.dispatch(TabsTrayAction.UpdateTabPartitions(null))
store.waitUntilIdle()
assertNotNull(SearchTerms.numberOfSearchTermGroup.testGetValue())
val event = SearchTerms.numberOfSearchTermGroup.testGetValue()!!
assertEquals(1, event.size)
assertEquals("0", event.single().extra!!["count"])
}
@Test
fun `WHEN inactive tabs are updated THEN report the count of inactive tabs`() {
@ -133,30 +90,4 @@ class TabsTrayMiddlewareTest {
assertEquals(1, snapshot.size)
assertEquals("true", snapshot.single().extra?.getValue("tab_selected"))
}
private fun generateSearchTermTabGroupsForAverage(): TabPartition {
val group1 = TabGroup("", "", mockk(relaxed = true))
val group2 = TabGroup("", "", mockk(relaxed = true))
val group3 = TabGroup("", "", mockk(relaxed = true))
every { group1.tabIds.size } returns 8
every { group2.tabIds.size } returns 4
every { group3.tabIds.size } returns 3
return TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(group1, group2, group3))
}
private fun generateSearchTermTabGroupsForDistribution(): TabPartition {
val group1 = TabGroup("", "", mockk(relaxed = true))
val group2 = TabGroup("", "", mockk(relaxed = true))
val group3 = TabGroup("", "", mockk(relaxed = true))
val group4 = TabGroup("", "", mockk(relaxed = true))
every { group1.tabIds.size } returns 8
every { group2.tabIds.size } returns 4
every { group3.tabIds.size } returns 2
every { group4.tabIds.size } returns 12
return TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(group1, group2, group3, group4))
}
}

View File

@ -1,82 +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 io.mockk.mockk
import mozilla.components.browser.state.state.TabGroup
import mozilla.components.browser.state.state.TabPartition
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.tabstray.SEARCH_TERM_TAB_GROUPS
import org.mozilla.fenix.tabstray.TabsTrayState
import org.mozilla.fenix.tabstray.TabsTrayStore
class OtherHeaderBindingTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
@Test
fun `WHEN there are no tabs THEN show no header`() {
val store = TabsTrayStore()
var result: Boolean? = null
val binding = OtherHeaderBinding(store) { result = it }
binding.start()
store.waitUntilIdle()
assertFalse(result!!)
}
@Test
fun `WHEN tabs for only groups THEN show no header`() {
val store = TabsTrayStore(TabsTrayState(searchTermPartition = mockk()))
var result: Boolean? = null
val binding = OtherHeaderBinding(store) { result = it }
binding.start()
store.waitUntilIdle()
assertFalse(result!!)
}
@Test
fun `WHEN tabs for only normal tabs THEN show no header`() {
val store = TabsTrayStore(TabsTrayState(normalTabs = listOf(mockk())))
var result: Boolean? = null
val binding = OtherHeaderBinding(store) { result = it }
binding.start()
store.waitUntilIdle()
assertFalse(result!!)
}
@Test
fun `WHEN normal tabs and groups exist THEN show header`() {
val tabGroup = TabGroup("test", "", listOf("1", "2"))
val store = TabsTrayStore(
TabsTrayState(
normalTabs = listOf(mockk()),
searchTermPartition = TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(tabGroup))
)
)
var result = false
val binding = OtherHeaderBinding(store) { result = it }
binding.start()
store.waitUntilIdle()
assertTrue(result)
}
}

View File

@ -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 mozilla.components.browser.state.state.TabGroup
import mozilla.components.browser.state.state.TabPartition
import mozilla.components.browser.state.state.createTab
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.tabstray.SEARCH_TERM_TAB_GROUPS
import org.mozilla.fenix.tabstray.TabsTrayAction
import org.mozilla.fenix.tabstray.TabsTrayStore
class TabGroupBindingTest {
val store = TabsTrayStore()
var captured: List<TabGroup>? = null
val binding = TabGroupBinding(store) { captured = it }
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
@After
fun teardown() {
binding.stop()
}
@Test
fun `WHEN the store is updated THEN notify the adapter`() {
val expectedTabGroups = listOf(TabGroup("cats", "name", listOf("1", "2")))
val tabPartition = TabPartition(SEARCH_TERM_TAB_GROUPS, expectedTabGroups)
assertNull(store.state.searchTermPartition?.tabGroups)
store.dispatch(TabsTrayAction.UpdateTabPartitions(tabPartition)).joinBlocking()
binding.start()
assertTrue(store.state.searchTermPartition?.tabGroups?.isNotEmpty() == true)
assertEquals(expectedTabGroups, captured)
}
@Test
fun `WHEN the store is updated with empty tab group THEN notify the adapter`() {
val expectedTabPartition = TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(TabGroup("cats", "name", emptyList())))
assertNull(store.state.searchTermPartition?.tabGroups)
store.dispatch(TabsTrayAction.UpdateTabPartitions(expectedTabPartition)).joinBlocking()
binding.start()
assertTrue(store.state.searchTermPartition?.tabGroups?.isNotEmpty() == true)
assertEquals(emptyList<TabGroup>(), captured)
}
@Test
fun `WHEN non-group tabs are updated THEN do not notify the adapter`() {
assertEquals(store.state.searchTermPartition?.tabGroups, null)
store.dispatch(TabsTrayAction.UpdatePrivateTabs(listOf(createTab("https://mozilla.org")))).joinBlocking()
binding.start()
assertNull(store.state.searchTermPartition?.tabGroups)
assertEquals(emptyList<TabGroup>(), captured)
}
}

View File

@ -6,14 +6,11 @@ package org.mozilla.fenix.tabstray.browser
import io.mockk.every
import io.mockk.mockk
import mozilla.components.browser.state.state.TabGroup
import mozilla.components.browser.state.state.TabPartition
import mozilla.components.browser.state.state.createTab
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.tabstray.SEARCH_TERM_TAB_GROUPS
import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.utils.Settings
@ -29,7 +26,7 @@ class TabSorterTest {
}
@Test
fun `WHEN updated with one normal tab THEN adapter have only one normal tab and no header`() {
fun `WHEN updated with one normal tab THEN adapter have only one normal tab`() {
val tabSorter = TabSorter(settings, tabsTrayStore)
tabSorter.updateTabs(
@ -43,52 +40,25 @@ class TabSorterTest {
tabsTrayStore.waitUntilIdle()
assertEquals(tabsTrayStore.state.inactiveTabs.size, 0)
assertEquals(tabsTrayStore.state.searchTermPartition?.tabGroups?.size, null)
assertEquals(tabsTrayStore.state.normalTabs.size, 1)
}
@Test
fun `WHEN updated with one normal tab and two search term tab THEN adapter have normal tab and a search group`() {
fun `WHEN updated with one normal tab and one inactive tab THEN adapter have normal tab and inactive tab`() {
val tabSorter = TabSorter(settings, tabsTrayStore)
val searchTab1 = createTab(url = "url", id = "tab2", lastAccess = System.currentTimeMillis(), searchTerms = "mozilla")
val searchTab2 = createTab(url = "url", id = "tab3", lastAccess = System.currentTimeMillis(), searchTerms = "mozilla")
tabSorter.updateTabs(
listOf(
createTab(url = "url", id = "tab1", lastAccess = System.currentTimeMillis()),
searchTab1, searchTab2
),
TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(TabGroup("mozilla", "", listOf(searchTab1.id, searchTab2.id)))),
selectedTabId = "tab1"
)
tabsTrayStore.waitUntilIdle()
assertEquals(tabsTrayStore.state.inactiveTabs.size, 0)
assertEquals(tabsTrayStore.state.searchTermPartition?.tabGroups?.size, 1)
assertEquals(tabsTrayStore.state.normalTabs.size, 1)
}
@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 tabSorter = TabSorter(settings, tabsTrayStore)
val searchTab1 = createTab(url = "url", id = "tab3", lastAccess = System.currentTimeMillis(), searchTerms = "mozilla")
val searchTab2 = createTab(url = "url", id = "tab4", lastAccess = System.currentTimeMillis(), searchTerms = "mozilla")
tabSorter.updateTabs(
listOf(
createTab(url = "url", id = "tab1", lastAccess = System.currentTimeMillis()),
createTab(url = "url", id = "tab2", lastAccess = inactiveTimestamp, createdAt = inactiveTimestamp),
searchTab1, searchTab2
),
TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(TabGroup("mozilla", "", listOf(searchTab1.id, searchTab2.id)))),
tabPartition = null,
selectedTabId = "tab1"
)
tabsTrayStore.waitUntilIdle()
assertEquals(tabsTrayStore.state.inactiveTabs.size, 1)
assertEquals(tabsTrayStore.state.searchTermPartition?.tabGroups?.size, 1)
assertEquals(tabsTrayStore.state.normalTabs.size, 1)
}
@ -106,70 +76,19 @@ class TabSorterTest {
lastAccess = inactiveTimestamp,
createdAt = inactiveTimestamp
),
createTab(
url = "url",
id = "tab3",
lastAccess = System.currentTimeMillis(),
searchTerms = "mozilla"
),
createTab(
url = "url",
id = "tab4",
lastAccess = System.currentTimeMillis(),
searchTerms = "mozilla"
)
),
TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(TabGroup("mozilla", "", listOf("tab3", "tab4")))),
tabPartition = null,
selectedTabId = "tab1"
)
tabsTrayStore.waitUntilIdle()
assertEquals(tabsTrayStore.state.inactiveTabs.size, 0)
assertEquals(tabsTrayStore.state.searchTermPartition?.tabGroups?.size, 1)
assertEquals(tabsTrayStore.state.normalTabs.size, 2)
}
@Test
fun `WHEN search term tabs is off THEN adapter have no search term group`() {
every { settings.searchTermTabGroupsAreEnabled }.answers { false }
val tabSorter = TabSorter(settings, tabsTrayStore)
tabSorter.updateTabs(
listOf(
createTab(url = "url", id = "tab1", lastAccess = System.currentTimeMillis()),
createTab(
url = "url",
id = "tab2",
lastAccess = inactiveTimestamp,
createdAt = inactiveTimestamp
),
createTab(
url = "url",
id = "tab3",
lastAccess = System.currentTimeMillis(),
searchTerms = "mozilla"
),
createTab(
url = "url",
id = "tab4",
lastAccess = System.currentTimeMillis(),
searchTerms = "mozilla"
)
),
TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(TabGroup("mozilla", "", listOf("tab3", "tab4")))),
selectedTabId = "tab1"
)
tabsTrayStore.waitUntilIdle()
assertEquals(tabsTrayStore.state.inactiveTabs.size, 1)
assertEquals(tabsTrayStore.state.searchTermPartition?.tabGroups?.size, 0)
assertEquals(tabsTrayStore.state.normalTabs.size, 3)
}
@Test
fun `WHEN both inactive tabs and search term tabs are off THEN adapter have only normal tabs`() {
fun `WHEN inactive tabs are disabled THEN adapter have only normal tabs`() {
every { settings.inactiveTabsAreEnabled }.answers { false }
every { settings.searchTermTabGroupsAreEnabled }.answers { false }
val tabSorter = TabSorter(settings, tabsTrayStore)
@ -195,67 +114,13 @@ class TabSorterTest {
searchTerms = "mozilla"
)
),
TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(TabGroup("mozilla", "", mockk()), TabGroup("mozilla", "", mockk()))),
tabPartition = null,
selectedTabId = "tab1"
)
tabsTrayStore.waitUntilIdle()
assertEquals(tabsTrayStore.state.inactiveTabs.size, 0)
assertEquals(tabsTrayStore.state.searchTermPartition?.tabGroups?.size, 0)
assertEquals(tabsTrayStore.state.normalTabs.size, 4)
}
@Test
fun `WHEN only one search term tab THEN there is no search group`() {
val tabSorter = TabSorter(settings, tabsTrayStore)
val tab1 =
createTab(
url = "url", id = "tab1", lastAccess = System.currentTimeMillis(),
searchTerms = "mozilla"
)
tabSorter.updateTabs(
listOf(tab1),
TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(TabGroup("mozilla", "", listOf(tab1.id)))),
selectedTabId = "tab1"
)
tabsTrayStore.waitUntilIdle()
assertEquals(tabsTrayStore.state.inactiveTabs.size, 0)
assertEquals(tabsTrayStore.state.searchTermPartition?.tabGroups?.size, 0)
assertEquals(tabsTrayStore.state.normalTabs.size, 1)
}
@Test
fun `WHEN remove second last one search term tab THEN search group is kept even if there's only one tab`() {
val tabSorter = TabSorter(settings, tabsTrayStore)
val tab1 = createTab(url = "url", id = "tab1", lastAccess = System.currentTimeMillis(), searchTerms = "mozilla")
val tab2 = createTab(url = "url", id = "tab2", lastAccess = System.currentTimeMillis(), searchTerms = "mozilla")
tabSorter.updateTabs(
listOf(tab1, tab2),
TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(TabGroup("mozilla", "", listOf(tab1.id, tab2.id)))),
selectedTabId = "tab1"
)
tabsTrayStore.waitUntilIdle()
assertEquals(tabsTrayStore.state.inactiveTabs.size, 0)
assertEquals(tabsTrayStore.state.searchTermPartition?.tabGroups?.size, 1)
assertEquals(tabsTrayStore.state.normalTabs.size, 0)
tabSorter.updateTabs(
listOf(tab1),
TabPartition(SEARCH_TERM_TAB_GROUPS, listOf(TabGroup("mozilla", "", listOf(tab1.id)))),
selectedTabId = "tab1"
)
tabsTrayStore.waitUntilIdle()
assertEquals(tabsTrayStore.state.inactiveTabs.size, 0)
assertEquals(tabsTrayStore.state.searchTermPartition?.tabGroups?.size, 1)
assertEquals(tabsTrayStore.state.normalTabs.size, 0)
}
}