diff --git a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt index 1e3374658302..58f4aa76ace4 100644 --- a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt +++ b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt @@ -63,4 +63,9 @@ object FeatureFlags { * Enables customizing the home screen */ val customizeHome = Config.channel.isNightlyOrDebug + + /** + * Identifies and separates the tabs list with a group containing search term tabs. + */ + val tabGroupFeature = Config.channel.isDebug } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt index da04b280982a..3a0f038db6db 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt @@ -14,10 +14,13 @@ import mozilla.components.browser.state.selector.normalTabs import mozilla.components.browser.state.selector.privateTabs import mozilla.components.browser.state.selector.selectedTab import mozilla.components.browser.state.store.BrowserStore +import org.mozilla.fenix.R import org.mozilla.fenix.sync.SyncedTabsAdapter import org.mozilla.fenix.tabstray.browser.BrowserTabsAdapter import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor +import org.mozilla.fenix.tabstray.browser.TitleHeaderAdapter import org.mozilla.fenix.tabstray.browser.InactiveTabsAdapter +import org.mozilla.fenix.tabstray.browser.TabGroupAdapter import org.mozilla.fenix.tabstray.syncedtabs.TabClickDelegate import org.mozilla.fenix.tabstray.viewholders.AbstractPageViewHolder import org.mozilla.fenix.tabstray.viewholders.NormalBrowserPageViewHolder @@ -35,8 +38,10 @@ class TrayPagerAdapter( private val normalAdapter by lazy { ConcatAdapter( - BrowserTabsAdapter(context, browserInteractor, store, TABS_TRAY_FEATURE_NAME), - InactiveTabsAdapter(context, browserInteractor, INACTIVE_TABS_FEATURE_NAME) + InactiveTabsAdapter(context, browserInteractor, INACTIVE_TABS_FEATURE_NAME), + TabGroupAdapter(context, browserInteractor, store, TAB_GROUP_FEATURE_NAME), + TitleHeaderAdapter(context.getString(R.string.tab_tray_header_title)), + BrowserTabsAdapter(context, browserInteractor, store, TABS_TRAY_FEATURE_NAME) ) } private val privateAdapter by lazy { BrowserTabsAdapter(context, browserInteractor, store, TABS_TRAY_FEATURE_NAME) } @@ -100,6 +105,7 @@ 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 diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabGridViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabGridViewHolder.kt deleted file mode 100644 index 5ca700e27c82..000000000000 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabGridViewHolder.kt +++ /dev/null @@ -1,69 +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.appcompat.content.res.AppCompatResources -import androidx.appcompat.widget.AppCompatImageButton -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 -import kotlin.math.max -import org.mozilla.fenix.selection.SelectionHolder -import org.mozilla.fenix.tabstray.TabsTrayStore - -/** - * A RecyclerView ViewHolder implementation for "tab" items with grid layout. - * - * @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 itemView [View] that displays a "tab". - * @param featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting. - */ -class BrowserTabGridViewHolder( - imageLoader: ImageLoader, - override val browserTrayInteractor: BrowserTrayInteractor, - store: TabsTrayStore, - selectionHolder: SelectionHolder? = null, - itemView: View, - featureName: String -) : AbstractBrowserTabViewHolder(itemView, imageLoader, store, selectionHolder, featureName) { - - private val closeButton: AppCompatImageButton = itemView.findViewById(R.id.mozac_browser_tabstray_close) - - override val thumbnailSize: Int - get() = max( - itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_grid_item_thumbnail_height), - itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_grid_item_thumbnail_width) - ) - - override fun updateSelectedTabIndicator(showAsSelected: Boolean) { - val binding = TabTrayGridItemBinding.bind(itemView) - binding.tabTrayGridItem.background = if (showAsSelected) { - AppCompatResources.getDrawable(itemView.context, R.drawable.tab_tray_grid_item_selected_border) - } else { - null - } - return - } - - override fun bind( - tab: Tab, - isSelected: Boolean, - styling: TabsTrayStyling, - observable: Observable - ) { - super.bind(tab, isSelected, styling, observable) - - closeButton.increaseTapArea(GRID_ITEM_CLOSE_BUTTON_EXTRA_DPS) - } -} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabListViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabListViewHolder.kt deleted file mode 100644 index 9e217c29c23c..000000000000 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabListViewHolder.kt +++ /dev/null @@ -1,53 +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.core.content.ContextCompat -import mozilla.components.concept.base.images.ImageLoader -import mozilla.components.concept.tabstray.Tab -import org.mozilla.fenix.R -import org.mozilla.fenix.selection.SelectionHolder -import org.mozilla.fenix.tabstray.TabsTrayStore -import kotlin.math.max - -/** - * A RecyclerView ViewHolder implementation for "tab" items with list layout. - * - * @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 itemView [View] that displays a "tab". - * @param featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting. -*/ -class BrowserTabListViewHolder( - imageLoader: ImageLoader, - override val browserTrayInteractor: BrowserTrayInteractor, - store: TabsTrayStore, - selectionHolder: SelectionHolder? = null, - itemView: View, - featureName: String -) : AbstractBrowserTabViewHolder(itemView, imageLoader, store, selectionHolder, featureName) { - override val thumbnailSize: Int - get() = max( - itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_list_item_thumbnail_height), - itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_list_item_thumbnail_width) - ) - - override fun updateSelectedTabIndicator(showAsSelected: Boolean) { - val color = if (showAsSelected) { - R.color.tab_tray_item_selected_background_normal_theme - } else { - R.color.tab_tray_item_background_normal_theme - } - itemView.setBackgroundColor( - ContextCompat.getColor( - itemView.context, - color - ) - ) - } -} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabViewHolder.kt new file mode 100644 index 000000000000..654dcf65b28f --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabViewHolder.kt @@ -0,0 +1,120 @@ +/* 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.appcompat.content.res.AppCompatResources +import androidx.appcompat.widget.AppCompatImageButton +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +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 +import kotlin.math.max +import org.mozilla.fenix.selection.SelectionHolder +import org.mozilla.fenix.tabstray.TabsTrayStore + +sealed class BrowserTabViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + /** + * A RecyclerView ViewHolder implementation for "tab" items with grid layout. + * + * @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 itemView [View] that displays a "tab". + * @param featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting. + */ + class GridViewHolder( + imageLoader: ImageLoader, + override val browserTrayInteractor: BrowserTrayInteractor, + store: TabsTrayStore, + selectionHolder: SelectionHolder? = null, + itemView: View, + featureName: String + ) : AbstractBrowserTabViewHolder(itemView, imageLoader, store, selectionHolder, featureName) { + + private val closeButton: AppCompatImageButton = itemView.findViewById(R.id.mozac_browser_tabstray_close) + + override val thumbnailSize: Int + get() = max( + itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_grid_item_thumbnail_height), + itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_grid_item_thumbnail_width) + ) + + override fun updateSelectedTabIndicator(showAsSelected: Boolean) { + val binding = TabTrayGridItemBinding.bind(itemView) + binding.tabTrayGridItem.background = if (showAsSelected) { + AppCompatResources.getDrawable(itemView.context, R.drawable.tab_tray_grid_item_selected_border) + } else { + null + } + return + } + + override fun bind( + tab: Tab, + isSelected: Boolean, + styling: TabsTrayStyling, + observable: Observable + ) { + super.bind(tab, isSelected, styling, observable) + + closeButton.increaseTapArea(GRID_ITEM_CLOSE_BUTTON_EXTRA_DPS) + } + + companion object { + const val LAYOUT_ID = R.layout.tab_tray_grid_item + } + } + + /** + * A RecyclerView ViewHolder implementation for "tab" items with list layout. + * + * @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 itemView [View] that displays a "tab". + * @param featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting. + */ + class ListViewHolder( + imageLoader: ImageLoader, + override val browserTrayInteractor: BrowserTrayInteractor, + store: TabsTrayStore, + selectionHolder: SelectionHolder? = null, + itemView: View, + featureName: String + ) : AbstractBrowserTabViewHolder(itemView, imageLoader, store, selectionHolder, featureName) { + override val thumbnailSize: Int + get() = max( + itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_list_item_thumbnail_height), + itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_list_item_thumbnail_width) + ) + + override fun updateSelectedTabIndicator(showAsSelected: Boolean) { + val color = if (showAsSelected) { + R.color.tab_tray_item_selected_background_normal_theme + } else { + R.color.tab_tray_item_background_normal_theme + } + itemView.setBackgroundColor( + ContextCompat.getColor( + itemView.context, + color + ) + ) + } + + companion object { + const val LAYOUT_ID = R.layout.tab_tray_item + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapter.kt index 9e89a2123fc7..a4e47a5a704d 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapter.kt @@ -16,7 +16,6 @@ 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.R import org.mozilla.fenix.components.Components import org.mozilla.fenix.databinding.TabTrayGridItemBinding import org.mozilla.fenix.databinding.TabTrayItemBinding @@ -45,8 +44,8 @@ class BrowserTabsAdapter( * The layout types for the tabs. */ enum class ViewType(val layoutRes: Int) { - LIST(R.layout.tab_tray_item), - GRID(R.layout.tab_tray_grid_item) + LIST(BrowserTabViewHolder.ListViewHolder.LAYOUT_ID), + GRID(BrowserTabViewHolder.GridViewHolder.LAYOUT_ID) } /** @@ -58,10 +57,13 @@ class BrowserTabsAdapter( private val imageLoader = ThumbnailLoader(context.components.core.thumbnailStorage) override fun getItemViewType(position: Int): Int { - return if (context.components.settings.gridTabView) { - ViewType.GRID.layoutRes - } else { - ViewType.LIST.layoutRes + return when { + context.components.settings.gridTabView -> { + ViewType.GRID.layoutRes + } + else -> { + ViewType.LIST.layoutRes + } } } @@ -70,9 +72,9 @@ class BrowserTabsAdapter( return when (viewType) { ViewType.GRID.layoutRes -> - BrowserTabGridViewHolder(imageLoader, interactor, store, selectionHolder, view, featureName) + BrowserTabViewHolder.GridViewHolder(imageLoader, interactor, store, selectionHolder, view, featureName) else -> - BrowserTabListViewHolder(imageLoader, interactor, store, selectionHolder, view, featureName) + BrowserTabViewHolder.ListViewHolder(imageLoader, interactor, store, selectionHolder, view, featureName) } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsInteractor.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsInteractor.kt index 08d01debb270..0dfb886ea6c5 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsInteractor.kt @@ -22,5 +22,5 @@ class DefaultInactiveTabsInteractor( * TODO This should be replaced with the AppStore. */ object InactiveTabsState { - var isExpanded = true + var isExpanded = false } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/NormalBrowserTrayList.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/NormalBrowserTrayList.kt index 3b4cd2e4ebca..0b4f3fc13adc 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/NormalBrowserTrayList.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/NormalBrowserTrayList.kt @@ -15,7 +15,10 @@ import org.mozilla.fenix.ext.components import org.mozilla.fenix.tabstray.ext.browserAdapter import org.mozilla.fenix.tabstray.ext.inactiveTabsAdapter import org.mozilla.fenix.tabstray.ext.isNormalTabActive +import org.mozilla.fenix.tabstray.ext.isNormalTabActiveWithoutSearchTerm import org.mozilla.fenix.tabstray.ext.isNormalTabInactive +import org.mozilla.fenix.tabstray.ext.isNormalTabWithSearchTerm +import org.mozilla.fenix.tabstray.ext.tabGroupAdapter import java.util.concurrent.TimeUnit /** @@ -32,7 +35,7 @@ class NormalBrowserTrayList @JvmOverloads constructor( /** * The maximum time from when a tab was created or accessed until it is considered "inactive". */ - var maxActiveTime = TimeUnit.DAYS.toMillis(DEFAULT_ACTIVE_DAYS) + private var maxActiveTime = TimeUnit.DAYS.toMillis(DEFAULT_ACTIVE_DAYS) private val concatAdapter by lazy { adapter as ConcatAdapter } @@ -48,12 +51,37 @@ class NormalBrowserTrayList @JvmOverloads constructor( if (!FeatureFlags.inactiveTabs) { return@TabsFeature !state.content.private } - state.isNormalTabActive(maxActiveTime) + + if (!FeatureFlags.tabGroupFeature) { + state.isNormalTabActive(maxActiveTime) + } else { + state.isNormalTabActiveWithoutSearchTerm(maxActiveTime) + } }, {} ) } + private val searchTermFeature by lazy { + val store = context.components.core.store + val tabFilter: (TabSessionState) -> Boolean = filter@{ + if (!FeatureFlags.tabGroupFeature) { + return@filter false + } + it.isNormalTabWithSearchTerm(maxActiveTime) + } + val tabsAdapter = concatAdapter.tabGroupAdapter + + TabsFeature( + tabsAdapter, + store, + selectTabUseCase, + removeTabUseCase, + tabFilter, + {} + ) + } + /** * NB: The setup for this feature is a bit complicated without a better dependency injection * solution to scope it down to just this view. @@ -96,6 +124,7 @@ class NormalBrowserTrayList @JvmOverloads constructor( super.onAttachedToWindow() tabsFeature.start() + searchTermFeature.start() inactiveFeature.start() touchHelper.attachToRecyclerView(this) @@ -105,6 +134,7 @@ class NormalBrowserTrayList @JvmOverloads constructor( super.onDetachedFromWindow() tabsFeature.stop() + searchTermFeature.stop() inactiveFeature.stop() touchHelper.attachToRecyclerView(null) diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupAdapter.kt new file mode 100644 index 000000000000..d5bcd20ecb71 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupAdapter.kt @@ -0,0 +1,118 @@ +/* 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 TabGroupViewHolder +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView.HORIZONTAL +import androidx.recyclerview.widget.RecyclerView.VERTICAL +import mozilla.components.concept.tabstray.Tabs +import mozilla.components.concept.tabstray.Tab as TabsTrayTab +import mozilla.components.concept.tabstray.TabsTray +import mozilla.components.support.base.observer.ObserverRegistry +import org.mozilla.fenix.components.Components +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.tabstray.TabsTrayStore +import kotlin.math.max +import mozilla.components.support.base.observer.Observable as ComponentObservable + +/** + * 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 delegate [Observable]<[TabsTray.Observer]> for observing tabs tray changes. Defaults to [ObserverRegistry]. + */ +class TabGroupAdapter( + private val context: Context, + private val browserTrayInteractor: BrowserTrayInteractor, + private val store: TabsTrayStore, + private val featureName: String, + delegate: ComponentObservable = ObserverRegistry() +) : ListAdapter(DiffCallback), + TabsTray, + ComponentObservable by delegate { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabGroupViewHolder { + val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) + + return when { + context.components.settings.gridTabView -> { + TabGroupViewHolder(view, HORIZONTAL) + } + else -> { + TabGroupViewHolder(view, VERTICAL) + } + } + } + + override fun onBindViewHolder(holder: TabGroupViewHolder, position: Int) { + val group = getItem(position) + holder.bind(group, browserTrayInteractor, store, this) + } + + override fun getItemViewType(position: Int): Int { + return TabGroupViewHolder.LAYOUT_ID + } + + override fun updateTabs(tabs: Tabs) { + val data = tabs.list.groupBy { it.searchTerm.lowercase() } + + val grouping = data.map { mapEntry -> + val searchTerm = mapEntry.key.replaceFirstChar(Char::uppercase) + val groupTabs = mapEntry.value + val groupMax = groupTabs.fold(0L) { acc, tab -> + max(tab.lastAccess, acc) + } + + Group( + title = searchTerm, + tabs = groupTabs, + lastAccess = groupMax + ) + }.sortedBy { it.lastAccess } + + submitList(grouping) + } + + data class Group( + /** + * A title for the tab group. + */ + val title: String, + + /** + * The list of tabs belonging to this tab group. + */ + val tabs: List, + + /** + * The last time tabs in this group was accessed. + */ + val lastAccess: Long + ) + + override fun isTabSelected(tabs: Tabs, position: Int): Boolean = + tabs.selectedIndex == position + override fun onTabsChanged(position: Int, count: Int) = Unit + override fun onTabsInserted(position: Int, count: Int) = Unit + override fun onTabsMoved(fromPosition: Int, toPosition: Int) = Unit + override fun onTabsRemoved(position: Int, count: Int) = Unit + + private object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Group, newItem: Group): Boolean { + return oldItem.title == newItem.title + } + + override fun areContentsTheSame(oldItem: Group, newItem: Group): Boolean { + return oldItem == newItem + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupListAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupListAdapter.kt new file mode 100644 index 000000000000..58127b77581e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupListAdapter.kt @@ -0,0 +1,91 @@ +/* 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.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +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 +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.dpToPx +import org.mozilla.fenix.tabstray.TabsTrayStore +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 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 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, + private val featureName: String, +) : ListAdapter(DiffCallback) { + + private val imageLoader = ThumbnailLoader(context.components.core.thumbnailStorage) + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): AbstractBrowserTabViewHolder { + val view = 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()) + view + } + else -> { + LayoutInflater.from(parent.context).inflate(R.layout.tab_tray_item, parent, false) + } + } + + return BrowserTabViewHolder.ListViewHolder(imageLoader, interactor, store, null, view, featureName) + } + + override fun onBindViewHolder(holder: AbstractBrowserTabViewHolder, position: Int) { + val tab = getItem(position) + holder.bind(tab, false, TabsTrayStyling(), delegate) + 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) + } + } + } + } + } + + private object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Tab, newItem: Tab): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: Tab, newItem: Tab): Boolean { + return oldItem == newItem + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupViewHolder.kt new file mode 100644 index 000000000000..7566c57022c4 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabGroupViewHolder.kt @@ -0,0 +1,52 @@ +/* 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/. */ + +import android.view.View +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.concept.tabstray.TabsTray +import mozilla.components.support.base.observer.Observable +import org.mozilla.fenix.R +import org.mozilla.fenix.databinding.TabGroupItemBinding +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.TabGroupListAdapter + +/** + * 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 + */ +class TabGroupViewHolder( + itemView: View, + val orientation: Int +) : RecyclerView.ViewHolder(itemView) { + private val binding = TabGroupItemBinding.bind(itemView) + + fun bind( + group: TabGroupAdapter.Group, + interactor: BrowserTrayInteractor, + store: TabsTrayStore, + observable: Observable + ) { + // bind title + binding.tabGroupTitle.text = group.title + // bind recyclerview for search term adapter + binding.tabGroupList.apply { + val groupListAdapter = TabGroupListAdapter( + itemView.context, interactor, store, observable, TrayPagerAdapter.TAB_GROUP_FEATURE_NAME + ) + layoutManager = LinearLayoutManager(itemView.context, orientation, false) + adapter = groupListAdapter + + groupListAdapter.submitList(group.tabs) + } + } + companion object { + const val LAYOUT_ID = R.layout.tab_group_item + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TitleHeaderAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TitleHeaderAdapter.kt new file mode 100644 index 000000000000..75f87bb5bd8e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TitleHeaderAdapter.kt @@ -0,0 +1,57 @@ +/* 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.RecyclerView +import org.mozilla.fenix.FeatureFlags +import org.mozilla.fenix.R +import org.mozilla.fenix.databinding.TabTrayTitleHeaderItemBinding + +/** + * A [RecyclerView.Adapter] for tab header. + * + * @param title [String] used for the title + */ +class TitleHeaderAdapter(val title: String) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder { + val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) + return HeaderViewHolder(view, title) + } + + override fun getItemViewType(position: Int): Int { + return HeaderViewHolder.LAYOUT_ID + } + + override fun getItemCount(): Int { + return if (FeatureFlags.tabGroupFeature) { + 1 + } else { + 0 + } + } + + override fun onBindViewHolder(holder: HeaderViewHolder, position: Int) { + /* Do nothing */ + } + + class HeaderViewHolder( + itemView: View, + val title: String + ) : RecyclerView.ViewHolder(itemView) { + private val binding = TabTrayTitleHeaderItemBinding.bind(itemView) + + fun bind() { + binding.tabTrayHeaderTitle.text = title + } + + companion object { + const val LAYOUT_ID = R.layout.tab_tray_title_header_item + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/ext/ConcatAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/ext/ConcatAdapter.kt index 5b052f0951de..ff9cf3d77efa 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/ext/ConcatAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/ext/ConcatAdapter.kt @@ -6,7 +6,9 @@ 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]. @@ -19,3 +21,15 @@ 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 diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/ext/Context.kt b/app/src/main/java/org/mozilla/fenix/tabstray/ext/Context.kt index 663968364ba8..92ad59cbfb95 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/ext/Context.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/ext/Context.kt @@ -8,7 +8,7 @@ import android.content.Context import org.mozilla.fenix.ext.components import org.mozilla.fenix.tabstray.browser.AutoCloseInterval -private const val MIN_COLUMN_WIDTH_DP = 180 +const val MIN_COLUMN_WIDTH_DP = 180 /** * Returns the number of grid columns we can fit on the screen in the tabs tray. diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSessionState.kt b/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSessionState.kt index 495539c4349b..55f2873b6bfb 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSessionState.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSessionState.kt @@ -12,6 +12,13 @@ private fun TabSessionState.isActive(maxActiveTime: Long): Boolean { return (now - lastActiveTime <= maxActiveTime) } +/** + * Returns true if a [TabSessionState] has a search term. + */ +private fun TabSessionState.hasSearchTerm(): Boolean { + return content.searchTerms.isNotEmpty() || !historyMetadata?.searchTerm.isNullOrBlank() +} + /** * Returns true if a [TabSessionState] is considered active based on the [maxActiveTime]. */ @@ -19,9 +26,24 @@ internal fun TabSessionState.isNormalTabActive(maxActiveTime: Long): Boolean { return isActive(maxActiveTime) && !content.private } +/** + * Returns true if a [TabSessionState] is considered active based on the [maxActiveTime] and + * does not have a search term + */ +internal fun TabSessionState.isNormalTabActiveWithoutSearchTerm(maxActiveTime: Long): Boolean { + return isNormalTabActive(maxActiveTime) && !hasSearchTerm() +} + /** * Returns true if a [TabSessionState] is considered active based on the [maxActiveTime]. */ internal fun TabSessionState.isNormalTabInactive(maxActiveTime: Long): Boolean { return !isActive(maxActiveTime) && !content.private } + +/** + * Returns true if a [TabSessionState] have a search term. + */ +internal fun TabSessionState.isNormalTabWithSearchTerm(maxActiveTime: Long): Boolean { + return isNormalTabActive(maxActiveTime) && hasSearchTerm() +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/NormalBrowserPageViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/NormalBrowserPageViewHolder.kt index a6c370cfb377..6f3f3085479b 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/NormalBrowserPageViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/NormalBrowserPageViewHolder.kt @@ -15,6 +15,9 @@ import org.mozilla.fenix.tabstray.TabsTrayInteractor import org.mozilla.fenix.tabstray.TabsTrayStore import org.mozilla.fenix.tabstray.ext.browserAdapter import org.mozilla.fenix.tabstray.ext.defaultBrowserLayoutColumns +import org.mozilla.fenix.tabstray.ext.titleHeaderAdapter +import org.mozilla.fenix.tabstray.ext.inactiveTabsAdapter +import org.mozilla.fenix.tabstray.ext.tabGroupAdapter /** * View holder for the normal tabs tray list. @@ -47,17 +50,23 @@ class NormalBrowserPageViewHolder( override fun bind( adapter: RecyclerView.Adapter ) { - val browserAdapter = (adapter as ConcatAdapter).browserAdapter + val concatAdapter = adapter as ConcatAdapter + val headerAdapter = concatAdapter.titleHeaderAdapter + val browserAdapter = concatAdapter.browserAdapter + val inactiveTabAdapter = concatAdapter.inactiveTabsAdapter + val tabGroupAdapter = concatAdapter.tabGroupAdapter browserAdapter.selectionHolder = this - val number = containerView.context.defaultBrowserLayoutColumns - val manager = GridLayoutManager(containerView.context, number).apply { + val numberOfColumns = containerView.context.defaultBrowserLayoutColumns + val manager = GridLayoutManager(containerView.context, numberOfColumns).apply { spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { - return if (position >= browserAdapter.itemCount) { - number - } else { + return if (position >= inactiveTabAdapter.itemCount + tabGroupAdapter.itemCount + + headerAdapter.itemCount + ) { 1 + } else { + numberOfColumns } } } diff --git a/app/src/main/res/layout/tab_group_item.xml b/app/src/main/res/layout/tab_group_item.xml new file mode 100644 index 000000000000..fe82d9ed9baf --- /dev/null +++ b/app/src/main/res/layout/tab_group_item.xml @@ -0,0 +1,34 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/tab_tray_title_header_item.xml b/app/src/main/res/layout/tab_tray_title_header_item.xml new file mode 100644 index 000000000000..a44b7cd51356 --- /dev/null +++ b/app/src/main/res/layout/tab_tray_title_header_item.xml @@ -0,0 +1,31 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 189a812b7043..deaac7bdb27a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -740,6 +740,8 @@ %1$s (Private Mode) Save + + Other diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapterTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapterTest.kt index 3217584294d5..736270c81e06 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapterTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapterTest.kt @@ -56,7 +56,7 @@ class BrowserTabsAdapterTest { val adapter = BrowserTabsAdapter(context, interactor, store, "Test") val binding = TabTrayItemBinding.inflate(LayoutInflater.from(testContext)) val holder = spyk( - BrowserTabListViewHolder( + BrowserTabViewHolder.ListViewHolder( imageLoader = mockk(), browserTrayInteractor = interactor, store = store,