Issue #20349: Add inactive tab grouping to tabs tray

upstream-sync
Jonathan Almeida 3 years ago committed by Sebastian Kaspari
parent f8945b3720
commit 69d630f46c

@ -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
}

@ -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<Tab>)
/**
* 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<Tab>,
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<Tab>, 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) {

@ -162,7 +162,6 @@ class TabsTrayFragment : AppCompatDialogFragment() {
tabsTrayInteractor,
tabsTrayController,
requireComponents.useCases.tabsUseCases.selectTab,
requireComponents.settings,
requireComponents.analytics.metrics
)

@ -29,6 +29,11 @@ interface TabsTrayInteractor {
* Invoked when [Tab]s need to be deleted.
*/
fun onDeleteTabs(tabs: Collection<Tab>)
/**
* Called when clicking the debug menu option for inactive tabs.
*/
fun onInactiveDebugClicked(tabs: Collection<Tab>)
}
/**
@ -54,4 +59,8 @@ class DefaultTabsTrayInteractor(
override fun onDeleteTabs(tabs: Collection<Tab>) {
controller.handleMultipleTabsDeletion(tabs)
}
override fun onInactiveDebugClicked(tabs: Collection<Tab>) {
controller.forceTabsAsInactive(tabs)
}
}

@ -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<AbstractPageViewHolder>() {
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 {

@ -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)
}
}

@ -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<Tab>, UserInteractionHandler {
@ -32,11 +26,6 @@ interface BrowserTrayInteractor : SelectionInteractor<Tab>, 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]
*/

@ -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)
}

@ -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<InactiveTabsAdapter.Item, InactiveTabViewHolder>
/**
* A convenience alias for readability.
*/
private typealias Observable = ComponentObservable<TabsTray.Observer>
/**
* 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<Item>() {
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()
}
}

@ -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)
}
}

@ -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()

@ -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 }
}
)
}

@ -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)
}
}

@ -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<T : TabViewHolder>(
delegate: Observable<TabsTray.Observer> = ObserverRegistry()
) : RecyclerView.Adapter<T>(), TabsTray, Observable<TabsTray.Observer> by delegate {
) : ListAdapter<Tab, T>(DiffCallback), TabsTray, Observable<TabsTray.Observer> by delegate {
protected var tabs: Tabs? = null
protected var styling: TabsTrayStyling = TabsTrayStyling()
@ -61,4 +63,14 @@ abstract class TabsAdapter<T : TabViewHolder>(
final override fun onTabsRemoved(position: Int, count: Int) =
notifyItemRangeRemoved(position, count)
private object DiffCallback : DiffUtil.ItemCallback<Tab>() {
override fun areItemsTheSame(oldItem: Tab, newItem: Tab): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Tab, newItem: Tab): Boolean {
return oldItem == newItem
}
}
}

@ -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

@ -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
}
}

@ -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
}

@ -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<out RecyclerView.ViewHolder>,
layoutManager: RecyclerView.LayoutManager
) {

@ -16,7 +16,6 @@ abstract class AbstractPageViewHolder constructor(
) : RecyclerView.ViewHolder(containerView) {
abstract fun bind(
adapter: RecyclerView.Adapter<out RecyclerView.ViewHolder>,
layoutManager: RecyclerView.LayoutManager
adapter: RecyclerView.Adapter<out RecyclerView.ViewHolder>
)
}

@ -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<Tab> {
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<out RecyclerView.ViewHolder>,
layoutManager: RecyclerView.LayoutManager
adapter: RecyclerView.Adapter<out RecyclerView.ViewHolder>
) {
(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 {

@ -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<out RecyclerView.ViewHolder>
) {
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
}

@ -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<out RecyclerView.ViewHolder>,
layoutManager: RecyclerView.LayoutManager
adapter: RecyclerView.Adapter<out RecyclerView.ViewHolder>
) {
val binding = ComponentSyncTabsTrayLayoutBinding.bind(containerView)
binding.syncedTabsList.layoutManager = layoutManager
binding.syncedTabsList.layoutManager = GridLayoutManager(containerView.context, 1)
binding.syncedTabsList.adapter = adapter
binding.syncedTabsTrayLayout.tabsTrayStore = tabsTrayStore

@ -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(

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?><!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:background="@drawable/rounded_bottom_corners"
android:elevation="@dimen/home_item_elevation"
android:paddingBottom="8dp">
<View
android:id="@+id/top_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?neutralFaded"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/inactive_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp"
android:textColor="?secondaryText"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/top_divider"
tools:text="@string/inactive_tabs_description" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?><!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="16dp"
android:background="@drawable/rounded_top_corners"
android:clickable="false"
android:clipToPadding="false"
android:elevation="@dimen/home_item_elevation"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<TextView
android:id="@+id/inactive_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="start"
android:maxLines="1"
android:minLines="1"
android:text="@string/inactive_tabs_title"
android:textAppearance="@style/Header16TextStyle"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Inactive tabs" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<mozilla.components.ui.widgets.WidgetSiteItemView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/site_list_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="?above"
android:elevation="@dimen/home_item_elevation"
android:foreground="?android:attr/selectableItemBackground"
android:minHeight="@dimen/mozac_widget_site_item_height" />

@ -3,7 +3,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.mozilla.fenix.tabstray.browser.BrowserTrayList
<org.mozilla.fenix.tabstray.browser.NormalBrowserTrayList
android:id="@+id/tray_list_item"
android:layout_width="match_parent"
android:layout_height="match_parent"

@ -3,7 +3,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.mozilla.fenix.tabstray.browser.BrowserTrayList
<org.mozilla.fenix.tabstray.browser.PrivateBrowserTrayList
android:id="@+id/tray_list_item"
android:layout_width="match_parent"
android:layout_height="match_parent"

@ -50,6 +50,9 @@
<!-- Label for using the nimbus collections preview -->
<string name="preferences_nimbus_use_preview_collection">Use Nimbus Preview Collection (requires restart)</string>
<!-- A secret menu option in the tabs tray for making a tab inactive for testing. -->
<string name="inactive_tabs_menu_item">Make inactive</string>
<!-- Label for showing Synced Tabs in the tabs tray -->
<string name="preferences_debug_synced_tabs_tabs_tray">Show Synced Tabs in the tabs tray</string>

@ -1765,6 +1765,16 @@
<!-- Dialog button text for canceling the rename top site prompt. -->
<string name="top_sites_rename_dialog_cancel">Cancel</string>
<!-- In-activate tabs in the tabs tray -->
<!-- Title text displayed in the tabs tray when a tab has been unused for 4 days. -->
<string name="inactive_tabs_title">Inactive tabs</string>
<!-- A description below the section of "inactive" tabs to notify the user when those tabs will be closed, if appropriate. -->
<string name="inactive_tabs_description">Tabs are available here for %s. After that time, tabs will be automatically closed.</string>
<!-- The amount of time until a tab in the "inactive" section of the tabs tray will be closed. See string inactive_tabs_description as well -->
<string name="inactive_tabs_30_days">30 days</string>
<!-- The amount of time until a tab in the "inactive" section of the tabs tray will be closed. See string inactive_tabs_description as well -->
<string name="inactive_tabs_7_days">1 week</string>
<!-- Default browser experiment -->
<string name="default_browser_experiment_card_text">Set links from websites, emails, and messages to open automatically in Firefox.</string>

@ -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<BrowserTabsAdapter>(relaxed = true)
trayList.adapter = adapter

@ -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<Context>()
val settings = mockk<Settings>()
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<Context>()
val settings = mockk<Settings>()
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<Context>()
val resources = mockk<Resources>()
val displayMetrics = spyk<DisplayMetrics> {
widthPixels = 1
density = 1f
}
every { context.resources } returns resources
every { resources.displayMetrics } returns displayMetrics
val result = context.numberOfGridColumns
assertEquals(2, result)
}
}

@ -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<Context>()
val resources = mockk<Resources>()
val displayMetrics = spyk<DisplayMetrics> {
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")
}
}

@ -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<ContentState>()
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<ContentState>()
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<ContentState>()
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))
}
}

@ -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(

@ -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

Loading…
Cancel
Save