Inactive tabs changes (#21524)

* Issue mozilla-mobile#21319 - Moved inactive tabs to the top of the normal tabs tray.

* Issue mozilla-mobile#21319 - Added a delete icon to delete ALL inactive tabs.

* Issue mozilla-mobile#21319 - Changed default inactive time period to 14 days

* Issue mozilla-mobile#21319 - Hooked inactive tabs setting to UI code

Inactive tabs setting is also disabled when the user has selected the one day or week auto-close tab setting.

* Issue mozilla-mobile#21319 - File and Lint cleanup

* PR: Fixed bug causing grouped tabs to also show in "Other" when marked as inactive but inactive is OFF in Settings

* PR: Fixed lint warnings

* PR: Removed redundant feature check

* PR - Ignore test until search term tab groups switch is done
upstream-sync
Noah Bond 3 years ago committed by GitHub
parent 5358552a62
commit 25d0696101
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -8,12 +8,14 @@ import android.os.Bundle
import android.view.View
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.Event.TabViewSettingChanged
import org.mozilla.fenix.components.metrics.Event.TabViewSettingChanged.Type
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.utils.view.addToRadioGroup
@ -30,6 +32,8 @@ class TabsSettingsFragment : PreferenceFragmentCompat() {
private lateinit var startOnHomeRadioFourHours: RadioButtonPreference
private lateinit var startOnHomeRadioAlways: RadioButtonPreference
private lateinit var startOnHomeRadioNever: RadioButtonPreference
private lateinit var inactiveTabsCategory: PreferenceCategory
private lateinit var inactiveTabs: SwitchPreference
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.tabs_preferences, rootKey)
@ -67,9 +71,24 @@ class TabsSettingsFragment : PreferenceFragmentCompat() {
requirePreference<PreferenceCategory>(R.string.pref_key_start_on_home_category).isVisible =
FeatureFlags.showStartOnHomeSettings
inactiveTabs = requirePreference<SwitchPreference>(R.string.pref_key_inactive_tabs).also {
it.isChecked = it.context.settings().inactiveTabsAreEnabled
it.onPreferenceChangeListener = SharedPreferenceUpdater()
}
inactiveTabsCategory = requirePreference<PreferenceCategory>(R.string.pref_key_inactive_tabs_category).also {
it.isVisible = FeatureFlags.inactiveTabs
it.isEnabled = !(it.context.settings().closeTabsAfterOneDay || it.context.settings().closeTabsAfterOneWeek)
}
listRadioButton.onClickListener(::sendTabViewTelemetry)
gridRadioButton.onClickListener(::sendTabViewTelemetry)
radioManual.onClickListener(::enableInactiveTabsSetting)
radioOneDay.onClickListener(::disableInactiveTabsSetting)
radioOneWeek.onClickListener(::disableInactiveTabsSetting)
radioOneMonth.onClickListener(::enableInactiveTabsSetting)
setupRadioGroups()
}
@ -102,4 +121,18 @@ class TabsSettingsFragment : PreferenceFragmentCompat() {
metrics.track(TabViewSettingChanged(Type.GRID))
}
}
private fun enableInactiveTabsSetting() {
inactiveTabsCategory.apply {
isEnabled = true
}
}
private fun disableInactiveTabsSetting() {
inactiveTabsCategory.apply {
isEnabled = false
inactiveTabs.isChecked = false
context.settings().inactiveTabsAreEnabled = false
}
}
}

@ -24,6 +24,7 @@ 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_ACTIVE_DAYS
import org.mozilla.fenix.tabstray.ext.inactiveTabs
import java.util.concurrent.TimeUnit
interface TabsTrayController {
@ -76,6 +77,11 @@ interface TabsTrayController {
tabs: Collection<Tab>,
numOfDays: Long = DEFAULT_ACTIVE_DAYS + 1
)
/**
* Deletes all inactive tabs.
*/
fun handleDeleteAllInactiveTabs()
}
class DefaultTabsTrayController(
@ -179,7 +185,7 @@ class DefaultTabsTrayController(
}
/**
* Marks all the [tabs] with the [TabSessionState.lastAccess] to 5 days; enough time to
* Marks all the [tabs] with the [TabSessionState.lastAccess] to 15 days; enough time to
* have a tab considered as inactive.
*
* DO NOT USE THIS OUTSIDE OF DEBUGGING/TESTING.
@ -211,4 +217,11 @@ class DefaultTabsTrayController(
dismissTray()
navigateToHomeAndDeleteSession(sessionId)
}
override fun handleDeleteAllInactiveTabs() {
browserStore.state.inactiveTabs.map { it.id }.let {
tabsUseCases.removeTabs(it)
}
showUndoSnackbarForTab(false)
}
}

@ -34,6 +34,11 @@ interface TabsTrayInteractor {
* Called when clicking the debug menu option for inactive tabs.
*/
fun onInactiveDebugClicked(tabs: Collection<Tab>)
/**
* Deletes all inactive tabs.
*/
fun onDeleteInactiveTabs()
}
/**
@ -63,4 +68,8 @@ class DefaultTabsTrayInteractor(
override fun onInactiveDebugClicked(tabs: Collection<Tab>) {
controller.forceTabsAsInactive(tabs)
}
override fun onDeleteInactiveTabs() {
controller.handleDeleteAllInactiveTabs()
}
}

@ -11,6 +11,7 @@ import androidx.annotation.VisibleForTesting
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.browser.state.store.BrowserStore
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.sync.SyncedTabsAdapter
import org.mozilla.fenix.tabstray.browser.BrowserTabsAdapter
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
@ -39,9 +40,9 @@ class TrayPagerAdapter(
*/
private val normalAdapter by lazy {
ConcatAdapter(
InactiveTabsAdapter(context, browserInteractor, INACTIVE_TABS_FEATURE_NAME),
InactiveTabsAdapter(context, browserInteractor, interactor, INACTIVE_TABS_FEATURE_NAME),
TabGroupAdapter(context, browserInteractor, tabsTrayStore, TAB_GROUP_FEATURE_NAME),
TitleHeaderAdapter(browserStore),
TitleHeaderAdapter(browserStore, context.settings()),
BrowserTabsAdapter(context, browserInteractor, tabsTrayStore, TABS_TRAY_FEATURE_NAME)
)
}

@ -19,6 +19,7 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.loadIntoView
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.dpToPx
import org.mozilla.fenix.tabstray.TabsTrayInteractor
import org.mozilla.fenix.tabstray.browser.AutoCloseInterval.Manual
import org.mozilla.fenix.tabstray.browser.AutoCloseInterval.OneDay
import org.mozilla.fenix.tabstray.browser.AutoCloseInterval.OneMonth
@ -28,7 +29,8 @@ sealed class InactiveTabViewHolder(itemView: View) : RecyclerView.ViewHolder(ite
class HeaderHolder(
itemView: View,
interactor: InactiveTabsInteractor
inactiveTabsInteractor: InactiveTabsInteractor,
tabsTrayInteractor: TabsTrayInteractor,
) : InactiveTabViewHolder(itemView) {
private val binding = InactiveHeaderItemBinding.bind(itemView)
@ -40,7 +42,7 @@ sealed class InactiveTabViewHolder(itemView: View) : RecyclerView.ViewHolder(ite
setOnClickListener {
val newState = !it.isActivated
interactor.onHeaderClicked(newState)
inactiveTabsInteractor.onHeaderClicked(newState)
it.isActivated = newState
binding.chevron.rotation = ROTATION_DEGREE
@ -50,6 +52,10 @@ sealed class InactiveTabViewHolder(itemView: View) : RecyclerView.ViewHolder(ite
bottom = binding.root.context.dpToPx(if (it.isActivated) 0f else 1f)
)
}
binding.delete.setOnClickListener {
tabsTrayInteractor.onDeleteInactiveTabs()
}
}
}

@ -14,6 +14,7 @@ import mozilla.components.concept.tabstray.Tabs
import mozilla.components.concept.tabstray.TabsTray
import mozilla.components.support.base.observer.ObserverRegistry
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.tabstray.TabsTrayInteractor
import org.mozilla.fenix.tabstray.browser.InactiveTabViewHolder.FooterHolder
import org.mozilla.fenix.tabstray.browser.InactiveTabViewHolder.HeaderHolder
import org.mozilla.fenix.tabstray.browser.InactiveTabViewHolder.RecentlyClosedHolder
@ -42,6 +43,7 @@ private typealias Observable = ComponentObservable<TabsTray.Observer>
class InactiveTabsAdapter(
private val context: Context,
private val browserTrayInteractor: BrowserTrayInteractor,
private val tabsTrayInteractor: TabsTrayInteractor,
private val featureName: String,
delegate: Observable = ObserverRegistry()
) : Adapter(DiffCallback), TabsTray, Observable by delegate {
@ -53,7 +55,7 @@ class InactiveTabsAdapter(
.inflate(viewType, parent, false)
return when (viewType) {
HeaderHolder.LAYOUT_ID -> HeaderHolder(view, inactiveTabsInteractor)
HeaderHolder.LAYOUT_ID -> HeaderHolder(view, inactiveTabsInteractor, tabsTrayInteractor)
TabViewHolder.LAYOUT_ID -> TabViewHolder(view, browserTrayInteractor, featureName)
FooterHolder.LAYOUT_ID -> FooterHolder(view)
RecentlyClosedHolder.LAYOUT_ID -> RecentlyClosedHolder(view, browserTrayInteractor)

@ -12,19 +12,22 @@ 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.ext.settings
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.isNormalTabActiveWithSearchTerm
import org.mozilla.fenix.tabstray.ext.isNormalTabActiveWithoutSearchTerm
import org.mozilla.fenix.tabstray.ext.isNormalTabWithoutSearchTerm
import org.mozilla.fenix.tabstray.ext.isNormalTabWithSearchTerm
import org.mozilla.fenix.tabstray.ext.isNormalTabInactive
import org.mozilla.fenix.tabstray.ext.isNormalTabActiveWithSearchTerm
import org.mozilla.fenix.tabstray.ext.tabGroupAdapter
import java.util.concurrent.TimeUnit
/**
* The time until which a tab is considered in-active (in days).
*/
const val DEFAULT_ACTIVE_DAYS = 4L
const val DEFAULT_ACTIVE_DAYS = 14L
/**
* The maximum time from when a tab was created or accessed until it is considered "inactive".
@ -41,34 +44,41 @@ class NormalBrowserTrayList @JvmOverloads constructor(
override val tabsFeature by lazy {
val tabsAdapter = concatAdapter.browserAdapter
val inactiveTabsEnabled = context.settings().inactiveTabsAreEnabled
val tabFilter: (TabSessionState) -> Boolean = {
when {
FeatureFlags.tabGroupFeature && inactiveTabsEnabled ->
it.isNormalTabActiveWithoutSearchTerm(maxActiveTime)
inactiveTabsEnabled -> it.isNormalTabActive(maxActiveTime)
FeatureFlags.tabGroupFeature -> it.isNormalTabWithoutSearchTerm()
else -> !it.content.private
}
}
TabsFeature(
tabsAdapter,
context.components.core.store,
selectTabUseCase,
removeTabUseCase,
{ state ->
if (!FeatureFlags.inactiveTabs) {
return@TabsFeature !state.content.private
}
if (!FeatureFlags.tabGroupFeature) {
state.isNormalTabActive(maxActiveTime)
} else {
state.isNormalTabActiveWithoutSearchTerm(maxActiveTime)
}
},
tabFilter,
{}
)
}
private val searchTermFeature by lazy {
val store = context.components.core.store
val tabFilter: (TabSessionState) -> Boolean = filter@{
if (!FeatureFlags.tabGroupFeature) {
return@filter false
val inactiveTabsEnabled = context.settings().inactiveTabsAreEnabled
val tabFilter: (TabSessionState) -> Boolean = {
when {
FeatureFlags.tabGroupFeature && inactiveTabsEnabled -> it.isNormalTabActiveWithSearchTerm(maxActiveTime)
FeatureFlags.tabGroupFeature -> it.isNormalTabWithSearchTerm()
else -> false
}
it.isNormalTabActiveWithSearchTerm(maxActiveTime)
}
val tabsAdapter = concatAdapter.tabGroupAdapter
@ -89,7 +99,7 @@ class NormalBrowserTrayList @JvmOverloads constructor(
private val inactiveFeature by lazy {
val store = context.components.core.store
val tabFilter: (TabSessionState) -> Boolean = filter@{
if (!FeatureFlags.inactiveTabs) {
if (!context.settings().inactiveTabsAreEnabled) {
return@filter false
}
it.isNormalTabInactive(maxActiveTime)

@ -13,17 +13,19 @@ import androidx.recyclerview.widget.RecyclerView
import mozilla.components.browser.state.store.BrowserStore
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.TabTrayTitleHeaderItemBinding
import org.mozilla.fenix.utils.Settings
/**
* A [RecyclerView.Adapter] for tab header.
*/
class TitleHeaderAdapter(
browserStore: BrowserStore
browserStore: BrowserStore,
settings: Settings
) : ListAdapter<TitleHeaderAdapter.Header, TitleHeaderAdapter.HeaderViewHolder>(DiffCallback) {
class Header
private val normalTabsHeaderBinding = TitleHeaderBinding(browserStore, ::handleListChanges)
private val normalTabsHeaderBinding = TitleHeaderBinding(browserStore, settings, ::handleListChanges)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)

@ -12,7 +12,8 @@ import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.lib.state.helpers.AbstractBinding
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.tabstray.ext.normalTrayTabs
import org.mozilla.fenix.tabstray.ext.getNormalTrayTabs
import org.mozilla.fenix.utils.Settings
/**
* A binding class to notify an observer to show a title if there is at least one tab available.
@ -20,10 +21,11 @@ import org.mozilla.fenix.tabstray.ext.normalTrayTabs
@OptIn(ExperimentalCoroutinesApi::class)
class TitleHeaderBinding(
store: BrowserStore,
private val settings: Settings,
private val showHeader: (Boolean) -> Unit
) : AbstractBinding<BrowserState>(store) {
override suspend fun onState(flow: Flow<BrowserState>) {
flow.map { it.normalTrayTabs }
flow.map { it.getNormalTrayTabs(settings.inactiveTabsAreEnabled) }
.ifChanged { it.size }
.collect {
if (it.isEmpty()) {

@ -41,17 +41,16 @@ val BrowserState.inactiveTabs: List<TabSessionState>
/**
* The list of normal tabs in the tabs tray filtered appropriately based on feature flags.
*/
val BrowserState.normalTrayTabs: List<TabSessionState>
get() {
return normalTabs.run {
if (FeatureFlags.tabGroupFeature && FeatureFlags.inactiveTabs) {
filter { it.isNormalTabActiveWithoutSearchTerm(maxActiveTime) }
} else if (FeatureFlags.inactiveTabs) {
filter { it.isNormalTabActive(maxActiveTime) }
} else if (FeatureFlags.tabGroupFeature) {
filter { it.isNormalTabWithSearchTerm() }
} else {
this
}
fun BrowserState.getNormalTrayTabs(inactiveTabsEnabled: Boolean): List<TabSessionState> {
return normalTabs.run {
if (FeatureFlags.tabGroupFeature && inactiveTabsEnabled) {
filter { it.isNormalTabActiveWithoutSearchTerm(maxActiveTime) }
} else if (inactiveTabsEnabled) {
filter { it.isNormalTabActive(maxActiveTime) }
} else if (FeatureFlags.tabGroupFeature) {
filter { it.isNormalTabWithSearchTerm() }
} else {
this
}
}
}

@ -48,6 +48,13 @@ internal fun TabSessionState.isNormalTabWithSearchTerm(): Boolean {
return hasSearchTerm() && !content.private
}
/**
* Returns true if the [TabSessionState] has a search term but may or may not be active.
*/
internal fun TabSessionState.isNormalTabWithoutSearchTerm(): Boolean {
return !hasSearchTerm() && !content.private
}
/**
* Returns true if the [TabSessionState] is considered active based on the [maxActiveTime].
*/

@ -14,20 +14,21 @@ import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.tabstray.Tab
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.selection.SelectionHolder
import org.mozilla.fenix.tabstray.TabsTrayInteractor
import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.tabstray.browser.InactiveTabsState
import org.mozilla.fenix.tabstray.browser.containsTabId
import org.mozilla.fenix.tabstray.browser.InactiveTabsState
import org.mozilla.fenix.tabstray.browser.maxActiveTime
import org.mozilla.fenix.tabstray.ext.browserAdapter
import org.mozilla.fenix.tabstray.ext.defaultBrowserLayoutColumns
import org.mozilla.fenix.tabstray.ext.getNormalTrayTabs
import org.mozilla.fenix.tabstray.ext.inactiveTabs
import org.mozilla.fenix.tabstray.ext.titleHeaderAdapter
import org.mozilla.fenix.tabstray.ext.inactiveTabsAdapter
import org.mozilla.fenix.tabstray.ext.isNormalTabInactive
import org.mozilla.fenix.tabstray.ext.isNormalTabActiveWithSearchTerm
import org.mozilla.fenix.tabstray.ext.normalTrayTabs
import org.mozilla.fenix.tabstray.ext.isNormalTabInactive
import org.mozilla.fenix.tabstray.ext.observeFirstInsert
import org.mozilla.fenix.tabstray.ext.tabGroupAdapter
@ -60,7 +61,6 @@ class NormalBrowserPageViewHolder(
val browserAdapter = concatAdapter.browserAdapter
val tabGroupAdapter = concatAdapter.tabGroupAdapter
val manager = setupLayoutManager(containerView.context, concatAdapter)
browserAdapter.selectionHolder = this
tabGroupAdapter.selectionHolder = this
@ -79,11 +79,12 @@ class NormalBrowserPageViewHolder(
val browserAdapter = concatAdapter.browserAdapter
val inactiveTabAdapter = concatAdapter.inactiveTabsAdapter
val tabGroupAdapter = concatAdapter.tabGroupAdapter
val inactiveTabsAreEnabled = containerView.context.settings().inactiveTabsAreEnabled
val selectedTab = browserStore.state.selectedNormalTab ?: return
// Update tabs into the inactive adapter.
if (FeatureFlags.inactiveTabs && selectedTab.isNormalTabInactive(maxActiveTime)) {
if (inactiveTabsAreEnabled && selectedTab.isNormalTabInactive(maxActiveTime)) {
val inactiveTabsList = browserStore.state.inactiveTabs
// We want to expand the inactive section first before we want to fire our scroll observer.
InactiveTabsState.isExpanded = true
@ -126,7 +127,7 @@ class NormalBrowserPageViewHolder(
// Updates tabs into the normal browser tabs adapter.
browserAdapter.observeFirstInsert {
val activeTabsList = browserStore.state.normalTrayTabs
val activeTabsList = browserStore.state.getNormalTrayTabs(inactiveTabsAreEnabled)
activeTabsList.forEachIndexed { tabIndex, trayTab ->
if (trayTab.id == selectedTab.id) {

@ -412,6 +412,15 @@ class Settings(private val appContext: Context) : PreferencesHolder {
}
}
/**
* Indicates if the user has enabled the inactive tabs feature.
*/
var inactiveTabsAreEnabled by featureFlagPreference(
appContext.getPreferenceKey(R.string.pref_key_inactive_tabs),
default = FeatureFlags.inactiveTabs,
featureFlag = FeatureFlags.inactiveTabs
)
@VisibleForTesting
internal fun timeNowInMillis(): Long = System.currentTimeMillis()

@ -25,8 +25,7 @@
android:clipToPadding="false"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
android:paddingStart="16dp"
android:paddingEnd="16dp">
android:paddingStart="16dp">
<TextView
android:id="@+id/inactive_title"
@ -45,14 +44,28 @@
<ImageView
android:id="@+id/chevron"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/tab_menu"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:rotation="180"
android:contentDescription="@string/tab_menu"
app:layout_constraintBottom_toBottomOf="@id/inactive_title"
app:layout_constraintStart_toEndOf="@id/inactive_title"
app:layout_constraintTop_toTopOf="@id/inactive_title"
app:srcCompat="@drawable/ic_chevron" />
<ImageView
android:id="@+id/delete"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:paddingHorizontal="16dp"
android:paddingVertical="12dp"
android:contentDescription="@string/inactive_tabs_delete_all"
android:foreground="?android:attr/selectableItemBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_chevron" />
app:srcCompat="@drawable/ic_delete" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

@ -268,6 +268,8 @@
<string name="pref_key_start_on_home_never" translatable="false">pref_key_start_on_home_never</string>
<string name="pref_key_start_on_home_category" translatable="false">pref_key_start_on_home_category</string>
<string name="pref_key_camera_permissions_needed" translatable="false">pref_key_camera_permissions_needed</string>
<string name="pref_key_inactive_tabs_category" translatable="false">pref_key_inactive_tabs_category</string>
<string name="pref_key_inactive_tabs" translatable="false">pref_key_inactive_tabs</string>
<string name="pref_key_return_to_browser" translatable="false">pref_key_return_to_browser</string>

@ -668,6 +668,14 @@
<!-- Summary for tabs preference when auto closing tabs setting is set to auto close tabs after one month-->
<string name="close_tabs_after_one_month_summary">Close after one month</string>
<!-- Inactive tabs -->
<!-- Category header of a preference that allows a user to enable or disable the inactive tabs feature -->
<string name="preferences_inactive_tabs">Inactive tabs on tab tray</string>
<!-- Title for inactive tabs preference -->
<string name="preferences_inactive_tabs_title">Make tabs inactive after 14 days</string>
<!-- Summary of inactive tabs preference -->
<string name="preferences_inactive_tabs_summary">To improve speed, tabs will be moved into the inactive tabs section on your tab tray</string>
<!-- Studies -->
<!-- Title of the remove studies button -->
<string name="studies_remove">Remove</string>
@ -1889,6 +1897,8 @@
<!-- 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>
<!-- Content description for closing all inactive tabs -->
<string name="inactive_tabs_delete_all">Close all inactive tabs</string>
<!-- A description below the section of "inactive" tabs to notify the user when those tabs will be closed, if appropriate. See strings inactive_tabs_30_days and inactive_tabs_7_days for placeholders options. -->
<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 -->

@ -69,4 +69,18 @@
android:key="@string/pref_key_start_on_home_never"
android:title="@string/start_on_home_never" />
</androidx.preference.PreferenceCategory>
<androidx.preference.PreferenceCategory
android:layout="@layout/preference_cat_style"
android:title="@string/preferences_inactive_tabs"
android:key="@string/pref_key_inactive_tabs_category"
app:allowDividerAbove="true"
app:iconSpaceReserved="false"
app:isPreferenceVisible="false">
<SwitchPreference
android:defaultValue="true"
android:key="@string/pref_key_inactive_tabs"
android:title="@string/preferences_inactive_tabs_title"
android:summary="@string/preferences_inactive_tabs_summary"/>
</androidx.preference.PreferenceCategory>
</androidx.preference.PreferenceScreen>

@ -34,6 +34,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.maxActiveTime
import org.mozilla.fenix.tabstray.ext.inactiveTabs
class DefaultTabsTrayControllerTest {
@MockK(relaxed = true)
@ -379,7 +381,7 @@ class DefaultTabsTrayControllerTest {
}
@Test
fun `WHEN dismissTabsTrayAndNavigateHome is called with a spefic tab id THEN tray is dismissed and navigates home is opened to delete that tab`() {
fun `WHEN dismissTabsTrayAndNavigateHome is called with a specific tab id THEN tray is dismissed and navigates home is opened to delete that tab`() {
var dismissTrayInvoked = false
var navigateToHomeAndDeleteSessionInvoked = false
createController(
@ -396,6 +398,39 @@ class DefaultTabsTrayControllerTest {
assertTrue(navigateToHomeAndDeleteSessionInvoked)
}
@ExperimentalCoroutinesApi
@Test
fun `WHEN deleteAllInactiveTabs is called THEN that it uses tabsUseCases#removeTabs and shows an undo snackbar`() {
var showUndoSnackbarForTabInvoked = false
val controller = spyk(
createController(
showUndoSnackbarForTab = {
showUndoSnackbarForTabInvoked = true
}
)
)
val inactiveTab: TabSessionState = mockk {
every { lastAccess } returns maxActiveTime
every { createdAt } returns 0
every { id } returns "24"
every { content } returns mockk {
every { private } returns false
}
}
every { browserStore.state } returns mockk()
try {
mockkStatic("mozilla.components.browser.state.selector.SelectorsKt")
every { browserStore.state.inactiveTabs } returns listOf(inactiveTab)
controller.handleDeleteAllInactiveTabs()
verify { tabsUseCases.removeTabs(listOf("24")) }
assertTrue(showUndoSnackbarForTabInvoked)
} finally {
unmockkStatic("mozilla.components.browser.state.selector.SelectorsKt")
}
}
private fun createController(
navigateToHomeAndDeleteSession: (String) -> Unit = { },
selectTabPosition: (Int, Boolean) -> Unit = { _, _ -> },

@ -42,4 +42,11 @@ class DefaultTabsTrayInteractorTest {
verifySequence { controller.handleMultipleTabsDeletion(tabsToDelete) }
}
@Test
fun `GIVEN user selecting delete all inactive tabs WHEN onDeleteTabs is called THEN the Interactor delegates the controller`() {
trayInteractor.onDeleteInactiveTabs()
verifySequence { controller.handleDeleteAllInactiveTabs() }
}
}

@ -12,6 +12,7 @@ import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.tabstray.TabsTrayInteractor
import org.mozilla.fenix.tabstray.browser.InactiveTabViewHolder.HeaderHolder
@RunWith(FenixRobolectricTestRunner::class)
@ -20,7 +21,8 @@ class InactiveTabViewHolderTest {
fun `HeaderHolder - WHEN clicked THEN notify the interactor`() {
val view = LayoutInflater.from(testContext).inflate(HeaderHolder.LAYOUT_ID, null)
val interactor: InactiveTabsInteractor = mockk(relaxed = true)
val viewHolder = HeaderHolder(view, interactor)
val tabsTrayInteractor: TabsTrayInteractor = mockk(relaxed = true)
val viewHolder = HeaderHolder(view, interactor, tabsTrayInteractor)
val initialActivatedState = view.isActivated

@ -4,6 +4,8 @@
package org.mozilla.fenix.tabstray.browser
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.state.action.TabListAction
@ -16,8 +18,10 @@ import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.utils.Settings
@ExperimentalCoroutinesApi
class TitleHeaderBindingTest {
@ -29,7 +33,10 @@ class TitleHeaderBindingTest {
fun `WHEN normal tabs are added to the list THEN return true`() = runBlockingTest {
var result = false
val store = BrowserStore()
val binding = TitleHeaderBinding(store) { result = it }
val settings: Settings = mockk(relaxed = true)
val binding = TitleHeaderBinding(store, settings) { result = it }
every { settings.inactiveTabsAreEnabled } returns true
binding.start()
@ -40,11 +47,15 @@ class TitleHeaderBindingTest {
assertTrue(result)
}
@Ignore // To be fixed with https://github.com/mozilla-mobile/fenix/issues/21360
@Test
fun `WHEN grouped tabs are added to the list THEN return false`() = runBlockingTest {
var result = false
val store = BrowserStore()
val binding = TitleHeaderBinding(store) { result = it }
val settings: Settings = mockk(relaxed = true)
val binding = TitleHeaderBinding(store, settings) { result = it }
every { settings.inactiveTabsAreEnabled } returns true
binding.start()
@ -73,7 +84,8 @@ class TitleHeaderBindingTest {
tabs = listOf(createTab("https://getpocket.com", id = "123"))
)
)
val binding = TitleHeaderBinding(store) { result = it }
val settings: Settings = mockk(relaxed = true)
val binding = TitleHeaderBinding(store, settings) { result = it }
binding.start()

@ -746,4 +746,11 @@ class SettingsTest {
localSetting.defaultBrowserNotificationDisplayed = true
assertFalse(localSetting.shouldShowDefaultBrowserNotification())
}
@Test
fun inactiveTabsAreEnabled() {
// When just created
// Then
assertTrue(settings.inactiveTabsAreEnabled)
}
}

Loading…
Cancel
Save