From 69d630f46c35c8a062033edf071ef08c8a6834cb Mon Sep 17 00:00:00 2001 From: Jonathan Almeida Date: Tue, 13 Jul 2021 22:26:15 -0400 Subject: [PATCH] Issue #20349: Add inactive tab grouping to tabs tray --- .../java/org/mozilla/fenix/FeatureFlags.kt | 5 + .../fenix/tabstray/TabsTrayController.kt | 29 ++++ .../fenix/tabstray/TabsTrayFragment.kt | 1 - .../fenix/tabstray/TabsTrayInteractor.kt | 9 ++ .../fenix/tabstray/TrayPagerAdapter.kt | 11 +- .../browser/AbstractBrowserTrayList.kt | 68 +++++++++ .../tabstray/browser/BrowserTrayInteractor.kt | 33 +---- .../tabstray/browser/InactiveTabViewHolder.kt | 100 +++++++++++++ .../tabstray/browser/InactiveTabsAdapter.kt | 132 ++++++++++++++++++ .../tabstray/browser/NormalBrowserTrayList.kt | 101 ++++++++++++++ ...rTrayList.kt => PrivateBrowserTrayList.kt} | 42 +----- .../fenix/tabstray/browser/SelectionMenu.kt | 12 ++ .../browser/SelectionMenuIntegration.kt | 6 +- .../fenix/tabstray/browser/TabsAdapter.kt | 16 ++- .../fenix/tabstray/ext/ConcatAdapter.kt | 21 +++ .../org/mozilla/fenix/tabstray/ext/Context.kt | 28 ++++ .../fenix/tabstray/ext/TabSessionState.kt | 26 ++-- .../AbstractBrowserPageViewHolder.kt | 6 +- .../viewholders/AbstractPageViewHolder.kt | 3 +- .../NormalBrowserPageViewHolder.kt | 31 ++-- .../PrivateBrowserPageViewHolder.kt | 18 ++- .../viewholders/SyncedTabsPageViewHolder.kt | 6 +- .../java/org/mozilla/fenix/utils/Settings.kt | 4 +- .../main/res/layout/inactive_footer_item.xml | 42 ++++++ .../main/res/layout/inactive_header_item.xml | 35 +++++ .../res/layout/inactive_tab_list_item.xml | 15 ++ .../res/layout/normal_browser_tray_list.xml | 2 +- .../res/layout/private_browser_tray_list.xml | 2 +- app/src/main/res/values/static_strings.xml | 3 + app/src/main/res/values/strings.xml | 10 ++ ...Test.kt => AbstractBrowserTrayListTest.kt} | 4 +- .../DefaultBrowserTrayInteractorTest.kt | 101 -------------- .../fenix/tabstray/ext/ContextKtTest.kt | 39 ++++++ .../tabstray/ext/TabSessionStateKtTest.kt | 57 -------- .../AbstractBrowserPageViewHolderTest.kt | 15 +- .../org/mozilla/fenix/utils/SettingsTest.kt | 10 +- 36 files changed, 757 insertions(+), 286 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/tabstray/browser/AbstractBrowserTrayList.kt create mode 100644 app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolder.kt create mode 100644 app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAdapter.kt create mode 100644 app/src/main/java/org/mozilla/fenix/tabstray/browser/NormalBrowserTrayList.kt rename app/src/main/java/org/mozilla/fenix/tabstray/browser/{BrowserTrayList.kt => PrivateBrowserTrayList.kt} (59%) create mode 100644 app/src/main/java/org/mozilla/fenix/tabstray/ext/ConcatAdapter.kt create mode 100644 app/src/main/res/layout/inactive_footer_item.xml create mode 100644 app/src/main/res/layout/inactive_header_item.xml create mode 100644 app/src/main/res/layout/inactive_tab_list_item.xml rename app/src/test/java/org/mozilla/fenix/tabstray/browser/{BrowserTrayListTest.kt => AbstractBrowserTrayListTest.kt} (90%) delete mode 100644 app/src/test/java/org/mozilla/fenix/tabstray/browser/DefaultBrowserTrayInteractorTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/tabstray/ext/ContextKtTest.kt delete mode 100644 app/src/test/java/org/mozilla/fenix/tabstray/ext/TabSessionStateKtTest.kt diff --git a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt index ae0be437d3..94522769e4 100644 --- a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt +++ b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt @@ -53,4 +53,9 @@ object FeatureFlags { * Enables the recently saved bookmarks feature in the home screen. */ val recentBookmarksFeature = Config.channel.isNightlyOrDebug + + /** + * Identifies and separates the tabs list with a secondary section containing least used tabs. + */ + val inactiveTabs = Config.channel.isNightlyOrDebug } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt index 4ffdd86a76..b9450f0e82 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt @@ -7,8 +7,10 @@ package org.mozilla.fenix.tabstray import androidx.annotation.VisibleForTesting import androidx.navigation.NavController import kotlinx.coroutines.ExperimentalCoroutinesApi +import mozilla.components.browser.state.action.LastAccessAction import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.getNormalOrPrivateTabs +import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.base.profiler.Profiler import mozilla.components.concept.tabstray.Tab @@ -19,6 +21,8 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.home.HomeFragment +import org.mozilla.fenix.tabstray.browser.DEFAULT_INACTIVE_DAYS +import java.util.concurrent.TimeUnit interface TabsTrayController { @@ -53,6 +57,18 @@ interface TabsTrayController { * @param tabs List of [Tab]s (sessions) to be removed. */ fun handleMultipleTabsDeletion(tabs: Collection) + + /** + * Set the list of [tabs] into the inactive state. + * + * ⚠️ DO NOT USE THIS OUTSIDE OF DEBUGGING/TESTING. + * + * @param tabs List of [Tab]s to be removed. + */ + fun forceTabsAsInactive( + tabs: Collection, + numOfDays: Long = DEFAULT_INACTIVE_DAYS + 1 + ) } class DefaultTabsTrayController( @@ -144,6 +160,19 @@ class DefaultTabsTrayController( showUndoSnackbarForTab(isPrivate) } + /** + * Marks all the [tabs] with the [TabSessionState.lastAccess] to 5 days; enough time to + * have a tab considered as inactive. + * + * ⚠️ DO NOT USE THIS OUTSIDE OF DEBUGGING/TESTING. + */ + override fun forceTabsAsInactive(tabs: Collection, numOfDays: Long) { + tabs.forEach { tab -> + val daysSince = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(numOfDays) + browserStore.dispatch(LastAccessAction.UpdateLastAccessAction(tab.id, daysSince)) + } + } + @VisibleForTesting internal fun sendNewTabEvent(isPrivateModeSelected: Boolean) { val eventToSend = if (isPrivateModeSelected) { diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt index e3775a0c70..ac5eafb099 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt @@ -162,7 +162,6 @@ class TabsTrayFragment : AppCompatDialogFragment() { tabsTrayInteractor, tabsTrayController, requireComponents.useCases.tabsUseCases.selectTab, - requireComponents.settings, requireComponents.analytics.metrics ) diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInteractor.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInteractor.kt index f8223e4b6e..9923b0eb07 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInteractor.kt @@ -29,6 +29,11 @@ interface TabsTrayInteractor { * Invoked when [Tab]s need to be deleted. */ fun onDeleteTabs(tabs: Collection) + + /** + * Called when clicking the debug menu option for inactive tabs. + */ + fun onInactiveDebugClicked(tabs: Collection) } /** @@ -54,4 +59,8 @@ class DefaultTabsTrayInteractor( override fun onDeleteTabs(tabs: Collection) { controller.handleMultipleTabsDeletion(tabs) } + + override fun onInactiveDebugClicked(tabs: Collection) { + controller.forceTabsAsInactive(tabs) + } } 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 43d74280ed..cbeeb757b2 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt @@ -8,6 +8,7 @@ import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup import androidx.annotation.VisibleForTesting +import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.RecyclerView import mozilla.components.browser.state.selector.normalTabs import mozilla.components.browser.state.selector.privateTabs @@ -16,6 +17,7 @@ import mozilla.components.browser.state.store.BrowserStore 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.InactiveTabsAdapter import org.mozilla.fenix.tabstray.syncedtabs.TabClickDelegate import org.mozilla.fenix.tabstray.viewholders.AbstractPageViewHolder import org.mozilla.fenix.tabstray.viewholders.NormalBrowserPageViewHolder @@ -31,7 +33,12 @@ class TrayPagerAdapter( @VisibleForTesting internal val browserStore: BrowserStore ) : RecyclerView.Adapter() { - private val normalAdapter by lazy { BrowserTabsAdapter(context, browserInteractor, store) } + private val normalAdapter by lazy { + ConcatAdapter( + BrowserTabsAdapter(context, browserInteractor, store), + InactiveTabsAdapter(context, browserInteractor) + ) + } private val privateAdapter by lazy { BrowserTabsAdapter(context, browserInteractor, store) } private val syncedTabsAdapter by lazy { SyncedTabsAdapter(TabClickDelegate(navInteractor)) } @@ -74,7 +81,7 @@ class TrayPagerAdapter( POSITION_SYNCED_TABS -> syncedTabsAdapter else -> throw IllegalStateException("View type does not exist.") } - viewHolder.bind(adapter, browserInteractor.getLayoutManagerForPosition(context, position)) + viewHolder.bind(adapter) } override fun getItemViewType(position: Int): Int { diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/AbstractBrowserTrayList.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/AbstractBrowserTrayList.kt new file mode 100644 index 0000000000..6b40df5265 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/AbstractBrowserTrayList.kt @@ -0,0 +1,68 @@ +/* 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.util.AttributeSet +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.feature.tabs.tabstray.TabsFeature +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.tabstray.TabsTrayInteractor +import org.mozilla.fenix.tabstray.TabsTrayStore + +/** + * The base class for a tabs tray list that wants to display browser tabs. + */ +abstract class AbstractBrowserTrayList @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : RecyclerView(context, attrs, defStyleAttr) { + + lateinit var interactor: TabsTrayInteractor + lateinit var tabsTrayStore: TabsTrayStore + + /** + * A [TabsFeature] is required for each browser list to ensure one always exists for displaying + * tabs. + */ + abstract val tabsFeature: TabsFeature + + // NB: The use cases here are duplicated because there isn't a nicer + // way to share them without a better dependency injection solution. + protected val selectTabUseCase = SelectTabUseCaseWrapper( + context.components.analytics.metrics, + context.components.useCases.tabsUseCases.selectTab + ) { + interactor.onBrowserTabSelected() + } + + protected val removeTabUseCase = RemoveTabUseCaseWrapper( + context.components.analytics.metrics + ) { sessionId -> + interactor.onDeleteTab(sessionId) + } + + protected val swipeToDelete by lazy { + SwipeToDeleteBinding(tabsTrayStore) + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + + swipeToDelete.start() + + adapter?.onAttachedToRecyclerView(this) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + + swipeToDelete.stop() + + // Notify the adapter that it is released from the view preemptively. + adapter?.onDetachedFromRecyclerView(this) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTrayInteractor.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTrayInteractor.kt index ec03088f2b..d4cf648e07 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTrayInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTrayInteractor.kt @@ -4,9 +4,6 @@ package org.mozilla.fenix.tabstray.browser -import android.content.Context -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView import mozilla.components.concept.tabstray.Tab import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.support.base.feature.UserInteractionHandler @@ -15,14 +12,11 @@ import org.mozilla.fenix.tabstray.TabsTrayAction import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.tabstray.TabsTrayController import org.mozilla.fenix.tabstray.TabsTrayInteractor -import org.mozilla.fenix.tabstray.TrayPagerAdapter -import org.mozilla.fenix.tabstray.ext.numberOfGridColumns -import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.tabstray.TabsTrayState.Mode import org.mozilla.fenix.tabstray.TabsTrayStore /** - * For interacting with UI that is specifically for [BrowserTrayList] and other browser + * For interacting with UI that is specifically for [AbstractBrowserTrayList] and other browser * tab tray views. */ interface BrowserTrayInteractor : SelectionInteractor, UserInteractionHandler { @@ -32,11 +26,6 @@ interface BrowserTrayInteractor : SelectionInteractor, UserInteractionHandl */ fun close(tab: Tab) - /** - * Returns the appropriate [RecyclerView.LayoutManager] to be used at [position]. - */ - fun getLayoutManagerForPosition(context: Context, position: Int): RecyclerView.LayoutManager - /** * TabTray's Floating Action Button clicked. */ @@ -51,7 +40,6 @@ class DefaultBrowserTrayInteractor( private val trayInteractor: TabsTrayInteractor, private val controller: TabsTrayController, private val selectTab: TabsUseCases.SelectTabUseCase, - private val settings: Settings, private val metrics: MetricController ) : BrowserTrayInteractor { @@ -109,25 +97,6 @@ class DefaultBrowserTrayInteractor( return false } - override fun getLayoutManagerForPosition( - context: Context, - position: Int - ): RecyclerView.LayoutManager { - if (position == TrayPagerAdapter.POSITION_SYNCED_TABS) { - // Lists are just Grids with one column :) - return GridLayoutManager(context, 1) - } - - // Normal/Private tabs - val numberOfColumns = if (settings.gridTabView) { - context.numberOfGridColumns - } else { - 1 - } - - return GridLayoutManager(context, numberOfColumns) - } - /** * See [BrowserTrayInteractor.onFabClicked] */ diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolder.kt new file mode 100644 index 0000000000..b29012ce74 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolder.kt @@ -0,0 +1,100 @@ +/* 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.annotation.StringRes +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.browser.toolbar.MAX_URI_LENGTH +import mozilla.components.concept.tabstray.Tab +import org.mozilla.fenix.R +import org.mozilla.fenix.databinding.InactiveFooterItemBinding +import org.mozilla.fenix.databinding.InactiveTabListItemBinding +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.loadIntoView +import org.mozilla.fenix.ext.toShortUrl +import org.mozilla.fenix.tabstray.browser.AutoCloseInterval.Manual +import org.mozilla.fenix.tabstray.browser.AutoCloseInterval.OneDay +import org.mozilla.fenix.tabstray.browser.AutoCloseInterval.OneMonth +import org.mozilla.fenix.tabstray.browser.AutoCloseInterval.OneWeek + +sealed class InactiveTabViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + class HeaderHolder(itemView: View) : InactiveTabViewHolder(itemView) { + companion object { + const val LAYOUT_ID = R.layout.inactive_header_item + } + } + + class TabViewHolder( + itemView: View, + private val browserTrayInteractor: BrowserTrayInteractor + ) : InactiveTabViewHolder(itemView) { + + private val binding = InactiveTabListItemBinding.bind(itemView) + + fun bind(tab: Tab) { + val components = itemView.context.components + val makePrettyUrl: (String) -> String = { + it.toShortUrl(components.publicSuffixList).take(MAX_URI_LENGTH) + } + + itemView.setOnClickListener { + browserTrayInteractor.open(tab) + } + + binding.siteListItem.apply { + components.core.icons.loadIntoView(iconView, tab.url) + setText(tab.title, makePrettyUrl(tab.url)) + setSecondaryButton( + R.drawable.mozac_ic_close, + R.string.content_description_close_button + ) { + browserTrayInteractor.close(tab) + } + } + } + + companion object { + const val LAYOUT_ID = R.layout.inactive_tab_list_item + } + } + + class FooterHolder(itemView: View) : InactiveTabViewHolder(itemView) { + + val binding = InactiveFooterItemBinding.bind(itemView) + + fun bind(interval: AutoCloseInterval) { + val context = itemView.context + val stringRes = when (interval) { + Manual, OneDay -> { + binding.inactiveDescription.visibility = View.GONE + binding.topDivider.visibility = View.GONE + null + } + OneWeek -> { + context.getString(interval.description) + } + OneMonth -> { + context.getString(interval.description) + } + } + if (stringRes != null) { + binding.inactiveDescription.text = + context.getString(R.string.inactive_tabs_description, stringRes) + } + } + + companion object { + const val LAYOUT_ID = R.layout.inactive_footer_item + } + } +} + +enum class AutoCloseInterval(@StringRes val description: Int) { + Manual(0), + OneDay(0), + OneWeek(R.string.inactive_tabs_7_days), + OneMonth(R.string.inactive_tabs_30_days) +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAdapter.kt new file mode 100644 index 0000000000..cdf93cffee --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAdapter.kt @@ -0,0 +1,132 @@ +/* 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.concept.tabstray.Tab as TabsTrayTab +import mozilla.components.concept.tabstray.Tabs +import mozilla.components.concept.tabstray.TabsTray +import mozilla.components.support.base.observer.ObserverRegistry +import org.mozilla.fenix.tabstray.browser.InactiveTabViewHolder.FooterHolder +import org.mozilla.fenix.tabstray.browser.InactiveTabViewHolder.HeaderHolder +import org.mozilla.fenix.tabstray.browser.InactiveTabViewHolder.TabViewHolder +import org.mozilla.fenix.tabstray.ext.autoCloseInterval +import mozilla.components.support.base.observer.Observable as ComponentObservable + +/** + * A convenience alias for readability. + */ +private typealias Adapter = ListAdapter + +/** + * A convenience alias for readability. + */ +private typealias Observable = ComponentObservable + +/** + * The [ListAdapter] for displaying the list of inactive tabs. + */ +class InactiveTabsAdapter( + private val context: Context, + private val browserTrayInteractor: BrowserTrayInteractor, + delegate: Observable = ObserverRegistry() +) : Adapter(DiffCallback), TabsTray, Observable by delegate { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InactiveTabViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(viewType, parent, false) + + return when (viewType) { + HeaderHolder.LAYOUT_ID -> HeaderHolder(view) + TabViewHolder.LAYOUT_ID -> TabViewHolder(view, browserTrayInteractor) + FooterHolder.LAYOUT_ID -> FooterHolder(view) + else -> throw IllegalStateException("Unknown viewType: $viewType") + } + } + + override fun onBindViewHolder(holder: InactiveTabViewHolder, position: Int) { + when (holder) { + is TabViewHolder -> { + val item = getItem(position) as Item.Tab + holder.bind(item.tab) + } + is FooterHolder -> { + val item = getItem(position) as Item.Footer + holder.bind(item.interval) + } + is HeaderHolder -> { + // do nothing. + } + } + } + + override fun getItemViewType(position: Int): Int { + return when (position) { + 0 -> HeaderHolder.LAYOUT_ID + itemCount - 1 -> FooterHolder.LAYOUT_ID + else -> TabViewHolder.LAYOUT_ID + } + } + + override fun updateTabs(tabs: Tabs) { + if (tabs.list.isEmpty()) { + // Early return with an empty list to remove the header/footer items. + submitList(emptyList()) + return + } + + val items = tabs.list.map { Item.Tab(it) } + val footer = Item.Footer(context.autoCloseInterval) + + submitList(listOf(Item.Header) + items + listOf(footer)) + } + + override fun isTabSelected(tabs: Tabs, position: Int): Boolean = false + 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: Item, newItem: Item): Boolean { + return if (oldItem is Item.Tab && newItem is Item.Tab) { + oldItem.tab.id == newItem.tab.id + } else { + oldItem == newItem + } + } + + override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { + return oldItem == newItem + } + } + + /** + * The types of different data we can put into the [InactiveTabsAdapter]. + */ + sealed class Item { + + /** + * A title header for the inactive tab section. This may be seen only + * when at least one inactive tab is present. + */ + object Header : Item() + + /** + * A tab that is now considered inactive. + */ + data class Tab(val tab: TabsTrayTab) : Item() + + /** + * A footer for the inactive tab section. This may be seen only + * when at least one inactive tab is present. + */ + data class Footer(val interval: AutoCloseInterval) : Item() + } +} 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 new file mode 100644 index 0000000000..1bd9f6fb77 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/NormalBrowserTrayList.kt @@ -0,0 +1,101 @@ +/* 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.util.AttributeSet +import androidx.recyclerview.widget.ConcatAdapter +import mozilla.components.browser.tabstray.TabViewHolder +import mozilla.components.feature.tabs.tabstray.TabsFeature +import org.mozilla.fenix.FeatureFlags +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.isNormalTabInactive +import java.util.concurrent.TimeUnit + +/** + * The time until which a tab is considered in-active (in days). + */ +const val DEFAULT_INACTIVE_DAYS = 4L + +class NormalBrowserTrayList @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : AbstractBrowserTrayList(context, attrs, defStyleAttr) { + + /** + * The maximum time from when a tab was created or accessed until it is considered "inactive". + */ + var maxActiveTime = TimeUnit.DAYS.toMillis(DEFAULT_INACTIVE_DAYS) + + private val concatAdapter by lazy { adapter as ConcatAdapter } + + override val tabsFeature by lazy { + val tabsAdapter = concatAdapter.browserAdapter + + TabsFeature( + tabsAdapter, + context.components.core.store, + selectTabUseCase, + removeTabUseCase, + { state -> + if (!FeatureFlags.inactiveTabs) { + return@TabsFeature !state.content.private + } + state.isNormalTabActive(maxActiveTime) + }, + {} + ) + } + + private val inactiveFeature by lazy { + val tabsAdapter = concatAdapter.inactiveTabsAdapter + + TabsFeature( + tabsAdapter, + context.components.core.store, + selectTabUseCase, + removeTabUseCase, + { state -> + if (!FeatureFlags.inactiveTabs) { + return@TabsFeature false + } + state.isNormalTabInactive(maxActiveTime) + }, + {} + ) + } + + private val touchHelper by lazy { + TabsTouchHelper( + observable = concatAdapter.browserAdapter, + onViewHolderTouched = { + it is TabViewHolder && swipeToDelete.isSwipeable + }, + onViewHolderDraw = { context.components.settings.gridTabView.not() } + ) + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + + tabsFeature.start() + inactiveFeature.start() + + touchHelper.attachToRecyclerView(this) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + + tabsFeature.stop() + inactiveFeature.stop() + + touchHelper.attachToRecyclerView(null) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTrayList.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/PrivateBrowserTrayList.kt similarity index 59% rename from app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTrayList.kt rename to app/src/main/java/org/mozilla/fenix/tabstray/browser/PrivateBrowserTrayList.kt index 39f595a4cb..a99f71bf21 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTrayList.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/PrivateBrowserTrayList.kt @@ -7,58 +7,28 @@ package org.mozilla.fenix.tabstray.browser import android.content.Context import android.util.AttributeSet import androidx.annotation.VisibleForTesting -import androidx.recyclerview.widget.RecyclerView +import androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE import mozilla.components.feature.tabs.tabstray.TabsFeature import org.mozilla.fenix.ext.components -import org.mozilla.fenix.tabstray.TabsTrayInteractor -import org.mozilla.fenix.tabstray.TabsTrayStore -import org.mozilla.fenix.tabstray.ext.filterFromConfig -class BrowserTrayList @JvmOverloads constructor( +class PrivateBrowserTrayList @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -) : RecyclerView(context, attrs, defStyleAttr) { +) : AbstractBrowserTrayList(context, attrs, defStyleAttr) { - /** - * The browser tab types we would want to show. - */ - enum class BrowserTabType { NORMAL, PRIVATE } - - lateinit var browserTabType: BrowserTabType - lateinit var interactor: TabsTrayInteractor - lateinit var tabsTrayStore: TabsTrayStore - - private val tabsFeature by lazy { + override val tabsFeature by lazy { // NB: The use cases here are duplicated because there isn't a nicer // way to share them without a better dependency injection solution. - val selectTabUseCase = SelectTabUseCaseWrapper( - context.components.analytics.metrics, - context.components.useCases.tabsUseCases.selectTab - ) { - interactor.onBrowserTabSelected() - } - - val removeTabUseCase = RemoveTabUseCaseWrapper( - context.components.analytics.metrics - ) { sessionId -> - interactor.onDeleteTab(sessionId) - } - TabsFeature( adapter as TabsAdapter, context.components.core.store, selectTabUseCase, removeTabUseCase, - { it.filterFromConfig(browserTabType) }, + { it.content.private }, { } ) } - - private val swipeToDelete by lazy { - SwipeToDeleteBinding(tabsTrayStore) - } - private val touchHelper by lazy { TabsTouchHelper( observable = adapter as TabsAdapter, @@ -78,7 +48,7 @@ class BrowserTrayList @JvmOverloads constructor( touchHelper.attachToRecyclerView(this) } - @VisibleForTesting + @VisibleForTesting(otherwise = PACKAGE_PRIVATE) public override fun onDetachedFromWindow() { super.onDetachedFromWindow() diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionMenu.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionMenu.kt index 1a789acb67..a46063bbd3 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionMenu.kt @@ -8,6 +8,7 @@ import android.content.Context import mozilla.components.browser.menu.BrowserMenuBuilder import mozilla.components.browser.menu.item.SimpleBrowserMenuItem import org.mozilla.fenix.R +import org.mozilla.fenix.ext.components class SelectionMenu( private val context: Context, @@ -16,6 +17,7 @@ class SelectionMenu( sealed class Item { object BookmarkTabs : Item() object DeleteTabs : Item() + object MakeInactive : Item() } val menuBuilder by lazy { BrowserMenuBuilder(menuItems) } @@ -34,6 +36,16 @@ class SelectionMenu( textColorResource = R.color.primary_text_normal_theme ) { onItemTapped.invoke(Item.DeleteTabs) + }, + // This item is only visible for debugging. + SimpleBrowserMenuItem( + context.getString(R.string.inactive_tabs_menu_item), + textColorResource = R.color.primary_text_normal_theme + ) { + onItemTapped.invoke(Item.MakeInactive) + }.apply { + // We only want this menu option visible when in debug mode for testing. + visible = { context.components.settings.showSecretDebugMenuThisSession } } ) } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionMenuIntegration.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionMenuIntegration.kt index 40fee99e8d..f008a85c41 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionMenuIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionMenuIntegration.kt @@ -33,12 +33,14 @@ class SelectionMenuIntegration( Do exhaustive when (item) { is SelectionMenu.Item.BookmarkTabs -> { navInteractor.onSaveToBookmarks(store.state.mode.selectedTabs) - store.dispatch(TabsTrayAction.ExitSelectMode) } is SelectionMenu.Item.DeleteTabs -> { trayInteractor.onDeleteTabs(store.state.mode.selectedTabs) - store.dispatch(TabsTrayAction.ExitSelectMode) + } + is SelectionMenu.Item.MakeInactive -> { + trayInteractor.onInactiveDebugClicked(store.state.mode.selectedTabs) } } + store.dispatch(TabsTrayAction.ExitSelectMode) } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsAdapter.kt index 8e4820cd12..77aa6509ca 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsAdapter.kt @@ -5,9 +5,11 @@ package org.mozilla.fenix.tabstray.browser import androidx.annotation.CallSuper -import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter import mozilla.components.browser.tabstray.TabViewHolder import mozilla.components.browser.tabstray.TabsTrayStyling +import mozilla.components.concept.tabstray.Tab import mozilla.components.concept.tabstray.Tabs import mozilla.components.concept.tabstray.TabsTray import mozilla.components.support.base.observer.Observable @@ -26,7 +28,7 @@ import mozilla.components.support.base.observer.ObserverRegistry */ abstract class TabsAdapter( delegate: Observable = ObserverRegistry() -) : RecyclerView.Adapter(), TabsTray, Observable by delegate { +) : ListAdapter(DiffCallback), TabsTray, Observable by delegate { protected var tabs: Tabs? = null protected var styling: TabsTrayStyling = TabsTrayStyling() @@ -61,4 +63,14 @@ abstract class TabsAdapter( final override fun onTabsRemoved(position: Int, count: Int) = notifyItemRangeRemoved(position, count) + + 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/ext/ConcatAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/ext/ConcatAdapter.kt new file mode 100644 index 0000000000..5b052f0951 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/ext/ConcatAdapter.kt @@ -0,0 +1,21 @@ +/* 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.ext + +import androidx.recyclerview.widget.ConcatAdapter +import org.mozilla.fenix.tabstray.browser.BrowserTabsAdapter +import org.mozilla.fenix.tabstray.browser.InactiveTabsAdapter + +/** + * A convenience binding for retrieving the [BrowserTabsAdapter] from the [ConcatAdapter]. + */ +internal val ConcatAdapter.browserAdapter + get() = adapters.find { it is BrowserTabsAdapter } as BrowserTabsAdapter + +/** + * A convenience binding for retrieving the [InactiveTabsAdapter] from the [ConcatAdapter]. + */ +internal val ConcatAdapter.inactiveTabsAdapter + get() = adapters.find { it is InactiveTabsAdapter } as InactiveTabsAdapter 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 1beff26775..663968364b 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 @@ -5,6 +5,8 @@ package org.mozilla.fenix.tabstray.ext 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 @@ -17,3 +19,29 @@ internal val Context.numberOfGridColumns: Int val screenWidthDp = displayMetrics.widthPixels / displayMetrics.density return (screenWidthDp / MIN_COLUMN_WIDTH_DP).toInt().coerceAtLeast(2) } + +/** + * Returns the default number of columns a browser tray list should display based + * on user preferences. + */ +internal val Context.defaultBrowserLayoutColumns: Int + get() { + return if (components.settings.gridTabView) { + numberOfGridColumns + } else { + 1 + } + } + +/** + * Returns the appropriate [AutoCloseInterval] based on user preferences. + */ +internal val Context.autoCloseInterval: AutoCloseInterval + get() = with(components.settings) { + when { + closeTabsAfterOneDay -> AutoCloseInterval.OneDay + closeTabsAfterOneWeek -> AutoCloseInterval.OneWeek + closeTabsAfterOneMonth -> AutoCloseInterval.OneMonth + else -> AutoCloseInterval.Manual + } + } 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 663fb6e216..2fc8c09f61 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 @@ -5,18 +5,22 @@ package org.mozilla.fenix.tabstray.ext import mozilla.components.browser.state.state.TabSessionState -import org.mozilla.fenix.tabstray.Page -import org.mozilla.fenix.tabstray.browser.BrowserTrayList.BrowserTabType -import org.mozilla.fenix.tabstray.browser.BrowserTrayList.BrowserTabType.PRIVATE -fun TabSessionState.filterFromConfig(type: BrowserTabType): Boolean { - val isPrivate = type == PRIVATE +private fun TabSessionState.isActive(maxActiveTime: Long): Boolean { + val now = System.currentTimeMillis() + return (now - lastAccess <= maxActiveTime) +} - return content.private == isPrivate +/** + * Returns true if a [TabSessionState] is considered active based on the [maxActiveTime]. + */ +internal fun TabSessionState.isNormalTabActive(maxActiveTime: Long): Boolean { + return isActive(maxActiveTime) && !content.private } -fun TabSessionState.getTrayPosition(): Int = - when (content.private) { - true -> Page.PrivateTabs.ordinal - false -> Page.NormalTabs.ordinal - } +/** + * Returns true if a [TabSessionState] is considered active based on the [maxActiveTime]. + */ +internal fun TabSessionState.isNormalTabInactive(maxActiveTime: Long): Boolean { + return !isActive(maxActiveTime) && !content.private +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/AbstractBrowserPageViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/AbstractBrowserPageViewHolder.kt index 4ca9eed336..071c2a335a 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/AbstractBrowserPageViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/AbstractBrowserPageViewHolder.kt @@ -13,7 +13,7 @@ import androidx.recyclerview.widget.RecyclerView import org.mozilla.fenix.R import org.mozilla.fenix.tabstray.TabsTrayInteractor import org.mozilla.fenix.tabstray.TabsTrayStore -import org.mozilla.fenix.tabstray.browser.BrowserTrayList +import org.mozilla.fenix.tabstray.browser.AbstractBrowserTrayList /** * A shared view holder for browser tabs tray list. @@ -25,7 +25,7 @@ abstract class AbstractBrowserPageViewHolder( private val currentTabIndex: Int ) : AbstractPageViewHolder(containerView) { - protected val trayList: BrowserTrayList = itemView.findViewById(R.id.tray_list_item) + private val trayList: AbstractBrowserTrayList = itemView.findViewById(R.id.tray_list_item) private val emptyList: TextView = itemView.findViewById(R.id.tab_tray_empty_view) abstract val emptyStringText: String @@ -36,7 +36,7 @@ abstract class AbstractBrowserPageViewHolder( } @CallSuper - override fun bind( + protected fun bind( adapter: RecyclerView.Adapter, layoutManager: RecyclerView.LayoutManager ) { diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/AbstractPageViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/AbstractPageViewHolder.kt index 1819364024..870b6ab321 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/AbstractPageViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/AbstractPageViewHolder.kt @@ -16,7 +16,6 @@ abstract class AbstractPageViewHolder constructor( ) : RecyclerView.ViewHolder(containerView) { abstract fun bind( - adapter: RecyclerView.Adapter, - layoutManager: RecyclerView.LayoutManager + adapter: RecyclerView.Adapter ) } 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 9cff6fdb58..a6c370cfb3 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 @@ -5,14 +5,16 @@ package org.mozilla.fenix.tabstray.viewholders import android.view.View +import androidx.recyclerview.widget.ConcatAdapter +import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import mozilla.components.concept.tabstray.Tab import org.mozilla.fenix.R import org.mozilla.fenix.selection.SelectionHolder import org.mozilla.fenix.tabstray.TabsTrayInteractor import org.mozilla.fenix.tabstray.TabsTrayStore -import org.mozilla.fenix.tabstray.browser.BrowserTrayList.BrowserTabType.NORMAL -import org.mozilla.fenix.tabstray.browser.BrowserTabsAdapter +import org.mozilla.fenix.tabstray.ext.browserAdapter +import org.mozilla.fenix.tabstray.ext.defaultBrowserLayoutColumns /** * View holder for the normal tabs tray list. @@ -30,10 +32,6 @@ class NormalBrowserPageViewHolder( ), SelectionHolder { - init { - trayList.browserTabType = NORMAL - } - /** * Holds the list of selected tabs. * @@ -47,12 +45,25 @@ class NormalBrowserPageViewHolder( get() = itemView.resources.getString(R.string.no_open_tabs_description) override fun bind( - adapter: RecyclerView.Adapter, - layoutManager: RecyclerView.LayoutManager + adapter: RecyclerView.Adapter ) { - (adapter as BrowserTabsAdapter).selectionHolder = this + val browserAdapter = (adapter as ConcatAdapter).browserAdapter + browserAdapter.selectionHolder = this + + val number = containerView.context.defaultBrowserLayoutColumns + val manager = GridLayoutManager(containerView.context, number).apply { + spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + return if (position >= browserAdapter.itemCount) { + number + } else { + 1 + } + } + } + } - super.bind(adapter, layoutManager) + super.bind(adapter, manager) } companion object { diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/PrivateBrowserPageViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/PrivateBrowserPageViewHolder.kt index c213ee5121..7e83b5903a 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/PrivateBrowserPageViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/PrivateBrowserPageViewHolder.kt @@ -5,10 +5,12 @@ package org.mozilla.fenix.tabstray.viewholders import android.view.View +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView import org.mozilla.fenix.R import org.mozilla.fenix.tabstray.TabsTrayInteractor import org.mozilla.fenix.tabstray.TabsTrayStore -import org.mozilla.fenix.tabstray.browser.BrowserTrayList.BrowserTabType.PRIVATE +import org.mozilla.fenix.tabstray.ext.defaultBrowserLayoutColumns /** * View holder for the private tabs tray list. @@ -25,13 +27,19 @@ class PrivateBrowserPageViewHolder( currentTabIndex ) { - init { - trayList.browserTabType = PRIVATE - } - override val emptyStringText: String get() = itemView.resources.getString(R.string.no_private_tabs_description) + override fun bind( + adapter: RecyclerView.Adapter + ) { + val context = containerView.context + val columns = context.defaultBrowserLayoutColumns + val manager = GridLayoutManager(context, columns) + + super.bind(adapter, manager) + } + companion object { const val LAYOUT_ID = R.layout.private_browser_tray_list } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/SyncedTabsPageViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/SyncedTabsPageViewHolder.kt index a13820d6ad..68c2cea3e9 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/SyncedTabsPageViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/SyncedTabsPageViewHolder.kt @@ -5,6 +5,7 @@ package org.mozilla.fenix.tabstray.viewholders import android.view.View +import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import org.mozilla.fenix.R import org.mozilla.fenix.databinding.ComponentSyncTabsTrayLayoutBinding @@ -16,12 +17,11 @@ class SyncedTabsPageViewHolder( ) : AbstractPageViewHolder(containerView) { override fun bind( - adapter: RecyclerView.Adapter, - layoutManager: RecyclerView.LayoutManager + adapter: RecyclerView.Adapter ) { val binding = ComponentSyncTabsTrayLayoutBinding.bind(containerView) - binding.syncedTabsList.layoutManager = layoutManager + binding.syncedTabsList.layoutManager = GridLayoutManager(containerView.context, 1) binding.syncedTabsList.adapter = adapter binding.syncedTabsTrayLayout.tabsTrayStore = tabsTrayStore diff --git a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt index f0da0bad7c..d5455b65f7 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -332,7 +332,7 @@ class Settings(private val appContext: Context) : PreferencesHolder { var manuallyCloseTabs by booleanPreference( appContext.getPreferenceKey(R.string.pref_key_close_tabs_manually), - default = true + default = FeatureFlags.inactiveTabs.not() ) var closeTabsAfterOneDay by booleanPreference( @@ -347,7 +347,7 @@ class Settings(private val appContext: Context) : PreferencesHolder { var closeTabsAfterOneMonth by booleanPreference( appContext.getPreferenceKey(R.string.pref_key_close_tabs_after_one_month), - default = false + default = FeatureFlags.inactiveTabs ) var allowThirdPartyRootCerts by booleanPreference( diff --git a/app/src/main/res/layout/inactive_footer_item.xml b/app/src/main/res/layout/inactive_footer_item.xml new file mode 100644 index 0000000000..b6342dde36 --- /dev/null +++ b/app/src/main/res/layout/inactive_footer_item.xml @@ -0,0 +1,42 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/inactive_header_item.xml b/app/src/main/res/layout/inactive_header_item.xml new file mode 100644 index 0000000000..f23f5892b2 --- /dev/null +++ b/app/src/main/res/layout/inactive_header_item.xml @@ -0,0 +1,35 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/inactive_tab_list_item.xml b/app/src/main/res/layout/inactive_tab_list_item.xml new file mode 100644 index 0000000000..6d004fb3ec --- /dev/null +++ b/app/src/main/res/layout/inactive_tab_list_item.xml @@ -0,0 +1,15 @@ + + + diff --git a/app/src/main/res/layout/normal_browser_tray_list.xml b/app/src/main/res/layout/normal_browser_tray_list.xml index fd2dde665a..003e706622 100644 --- a/app/src/main/res/layout/normal_browser_tray_list.xml +++ b/app/src/main/res/layout/normal_browser_tray_list.xml @@ -3,7 +3,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - Use Nimbus Preview Collection (requires restart) + + Make inactive + Show Synced Tabs in the tabs tray diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6445a0dc40..f5ba7dea17 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1765,6 +1765,16 @@ Cancel + + + Inactive tabs + + Tabs are available here for %s. After that time, tabs will be automatically closed. + + 30 days + + 1 week + Set links from websites, emails, and messages to open automatically in Firefox. diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/BrowserTrayListTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/AbstractBrowserTrayListTest.kt similarity index 90% rename from app/src/test/java/org/mozilla/fenix/tabstray/browser/BrowserTrayListTest.kt rename to app/src/test/java/org/mozilla/fenix/tabstray/browser/AbstractBrowserTrayListTest.kt index b3422ee588..e9d826799f 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/browser/BrowserTrayListTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/AbstractBrowserTrayListTest.kt @@ -13,11 +13,11 @@ import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.tabstray.TabsTrayStore @RunWith(FenixRobolectricTestRunner::class) -class BrowserTrayListTest { +class AbstractBrowserTrayListTest { @Test fun `WHEN recyclerview detaches from window THEN notify adapter`() { - val trayList = BrowserTrayList(testContext) + val trayList = PrivateBrowserTrayList(testContext) val adapter = mockk(relaxed = true) trayList.adapter = adapter diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/DefaultBrowserTrayInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/DefaultBrowserTrayInteractorTest.kt deleted file mode 100644 index cbb916abd7..0000000000 --- a/app/src/test/java/org/mozilla/fenix/tabstray/browser/DefaultBrowserTrayInteractorTest.kt +++ /dev/null @@ -1,101 +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.content.res.Resources -import android.util.DisplayMetrics -import androidx.recyclerview.widget.GridLayoutManager -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkStatic -import io.mockk.spyk -import io.mockk.unmockkStatic -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test -import org.mozilla.fenix.tabstray.TrayPagerAdapter -import org.mozilla.fenix.tabstray.ext.numberOfGridColumns -import org.mozilla.fenix.utils.Settings - -class DefaultBrowserTrayInteractorTest { - - @Before - fun setup() { - mockkStatic("org.mozilla.fenix.tabstray.ext.ContextKt") - } - - @After - fun shutdown() { - unmockkStatic("org.mozilla.fenix.tabstray.ext.ContextKt") - } - - @Test - fun `WHEN pager position is synced tabs THEN return a list layout manager`() { - val interactor = - DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), mockk(), mockk(), mockk()) - - val result = interactor.getLayoutManagerForPosition( - mockk(), - TrayPagerAdapter.POSITION_SYNCED_TABS - ) - - assertEquals(1, (result as GridLayoutManager).spanCount) - } - - @Test - fun `WHEN setting is grid view THEN return grid layout manager`() { - val context = mockk() - val settings = mockk() - val interactor = - DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), mockk(), settings, mockk()) - - every { context.numberOfGridColumns }.answers { 4 } - every { settings.gridTabView }.answers { true } - - val result = interactor.getLayoutManagerForPosition( - context, - TrayPagerAdapter.POSITION_NORMAL_TABS - ) - - assertEquals(4, (result as GridLayoutManager).spanCount) - } - - @Test - fun `WHEN setting is list view THEN return list layout manager`() { - val context = mockk() - val settings = mockk() - val interactor = - DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), mockk(), settings, mockk()) - - every { context.numberOfGridColumns }.answers { 4 } - every { settings.gridTabView }.answers { false } - - val result = interactor.getLayoutManagerForPosition( - context, - TrayPagerAdapter.POSITION_NORMAL_TABS - ) - - // Should NOT be 4. - assertEquals(1, (result as GridLayoutManager).spanCount) - } - - @Test - fun `WHEN screen density is very low THEN numberOfGridColumns will still be a minimum of 2`() { - val context = mockk() - val resources = mockk() - val displayMetrics = spyk { - widthPixels = 1 - density = 1f - } - every { context.resources } returns resources - every { resources.displayMetrics } returns displayMetrics - - val result = context.numberOfGridColumns - - assertEquals(2, result) - } -} diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/ext/ContextKtTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/ext/ContextKtTest.kt new file mode 100644 index 0000000000..3e6ad82a4b --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/tabstray/ext/ContextKtTest.kt @@ -0,0 +1,39 @@ +/* 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.ext + +import android.content.Context +import android.content.res.Resources +import android.util.DisplayMetrics +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.spyk +import io.mockk.unmockkStatic +import org.junit.Assert.assertEquals +import org.junit.Test + +class ContextKtTest { + + @Test + fun `WHEN screen density is very low THEN numberOfGridColumns will still be a minimum of 2`() { + mockkStatic("org.mozilla.fenix.tabstray.ext.ContextKt") + + val context = mockk() + val resources = mockk() + val displayMetrics = spyk { + widthPixels = 1 + density = 1f + } + every { context.resources } returns resources + every { resources.displayMetrics } returns displayMetrics + + val result = context.numberOfGridColumns + + assertEquals(2, result) + + unmockkStatic("org.mozilla.fenix.tabstray.ext.ContextKt") + } +} diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/ext/TabSessionStateKtTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/ext/TabSessionStateKtTest.kt deleted file mode 100644 index d8efb4e564..0000000000 --- a/app/src/test/java/org/mozilla/fenix/tabstray/ext/TabSessionStateKtTest.kt +++ /dev/null @@ -1,57 +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.ext - -import io.mockk.every -import io.mockk.mockk -import mozilla.components.browser.state.state.ContentState -import mozilla.components.browser.state.state.TabSessionState -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test -import org.mozilla.fenix.tabstray.browser.BrowserTrayList.BrowserTabType.NORMAL -import org.mozilla.fenix.tabstray.browser.BrowserTrayList.BrowserTabType.PRIVATE - -class TabSessionStateKtTest { - - @Test - fun `WHEN configuration is private THEN return true`() { - val contentState = mockk() - val state = TabSessionState(content = contentState) - val config = PRIVATE - - every { contentState.private } returns true - - assertTrue(state.filterFromConfig(config)) - } - - @Test - fun `WHEN configuration is normal THEN return false`() { - val contentState = mockk() - val state = TabSessionState(content = contentState) - val config = NORMAL - - every { contentState.private } returns false - - assertTrue(state.filterFromConfig(config)) - } - - @Test - fun `WHEN configuration does not match THEN return false`() { - val contentState = mockk() - val state = TabSessionState(content = contentState) - val config = NORMAL - - every { contentState.private } returns true - - assertFalse(state.filterFromConfig(config)) - - val config2 = PRIVATE - - every { contentState.private } returns false - - assertFalse(state.filterFromConfig(config2)) - } -} diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/viewholders/AbstractBrowserPageViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/viewholders/AbstractBrowserPageViewHolderTest.kt index 6598682ce2..aef750d17a 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/viewholders/AbstractBrowserPageViewHolderTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/viewholders/AbstractBrowserPageViewHolderTest.kt @@ -8,7 +8,6 @@ import android.view.LayoutInflater import android.view.View.GONE import android.view.View.VISIBLE import android.widget.TextView -import androidx.recyclerview.widget.LinearLayoutManager import io.mockk.mockk import mozilla.components.concept.tabstray.Tabs import mozilla.components.support.test.robolectric.testContext @@ -19,7 +18,7 @@ import org.mozilla.fenix.R import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.tabstray.TabsTrayInteractor import org.mozilla.fenix.tabstray.TabsTrayStore -import org.mozilla.fenix.tabstray.browser.BrowserTrayList +import org.mozilla.fenix.tabstray.browser.AbstractBrowserTrayList import org.mozilla.fenix.tabstray.browser.BrowserTabsAdapter import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor import org.mozilla.fenix.tabstray.browser.createTab @@ -35,11 +34,11 @@ class AbstractBrowserPageViewHolderTest { fun `WHEN tabs inserted THEN show tray`() { val itemView = LayoutInflater.from(testContext).inflate(R.layout.normal_browser_tray_list, null) - val viewHolder = NormalBrowserPageViewHolder(itemView, store, interactor, 5) - val trayList: BrowserTrayList = itemView.findViewById(R.id.tray_list_item) + val viewHolder = PrivateBrowserPageViewHolder(itemView, store, interactor, 5) + val trayList: AbstractBrowserTrayList = itemView.findViewById(R.id.tray_list_item) val emptyList: TextView = itemView.findViewById(R.id.tab_tray_empty_view) - viewHolder.bind(adapter, LinearLayoutManager(testContext)) + viewHolder.bind(adapter) adapter.updateTabs( Tabs( @@ -59,11 +58,11 @@ class AbstractBrowserPageViewHolderTest { fun `WHEN no tabs THEN show empty view`() { val itemView = LayoutInflater.from(testContext).inflate(R.layout.normal_browser_tray_list, null) - val viewHolder = NormalBrowserPageViewHolder(itemView, store, interactor, 5) - val trayList: BrowserTrayList = itemView.findViewById(R.id.tray_list_item) + val viewHolder = PrivateBrowserPageViewHolder(itemView, store, interactor, 5) + val trayList: AbstractBrowserTrayList = itemView.findViewById(R.id.tray_list_item) val emptyList: TextView = itemView.findViewById(R.id.tab_tray_empty_view) - viewHolder.bind(adapter, LinearLayoutManager(testContext)) + viewHolder.bind(adapter) adapter.updateTabs( Tabs( diff --git a/app/src/test/java/org/mozilla/fenix/utils/SettingsTest.kt b/app/src/test/java/org/mozilla/fenix/utils/SettingsTest.kt index d4298043fc..4f8934261c 100644 --- a/app/src/test/java/org/mozilla/fenix/utils/SettingsTest.kt +++ b/app/src/test/java/org/mozilla/fenix/utils/SettingsTest.kt @@ -225,21 +225,21 @@ class SettingsTest { fun shouldManuallyCloseTabs() { // When just created // Then - assertTrue(settings.manuallyCloseTabs) + assertFalse(settings.manuallyCloseTabs) // When - settings.manuallyCloseTabs = false + settings.manuallyCloseTabs = true // Then - assertFalse(settings.shouldUseLightTheme) + assertTrue(settings.manuallyCloseTabs) } @Test fun getTabTimeout() { // When just created // Then - assertTrue(settings.manuallyCloseTabs) - assertEquals(Long.MAX_VALUE, settings.getTabTimeout()) + assertTrue(settings.closeTabsAfterOneMonth) + assertEquals(Settings.ONE_MONTH_MS, settings.getTabTimeout()) // When settings.manuallyCloseTabs = false