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 32844de46..e33b709c2 100644
--- a/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt
+++ b/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt
@@ -40,7 +40,7 @@ class TrayPagerAdapter(
*/
private val normalAdapter by lazy {
ConcatAdapter(
- InactiveTabsAdapter(context, browserInteractor, interactor, INACTIVE_TABS_FEATURE_NAME),
+ InactiveTabsAdapter(context, browserInteractor, interactor, INACTIVE_TABS_FEATURE_NAME, context.settings()),
TabGroupAdapter(context, browserInteractor, tabsTrayStore, TAB_GROUP_FEATURE_NAME),
TitleHeaderAdapter(browserStore, context.settings()),
BrowserTabsAdapter(context, browserInteractor, tabsTrayStore, TABS_TRAY_FEATURE_NAME)
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
index ddc575173..503edc99f 100644
--- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolder.kt
+++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolder.kt
@@ -14,6 +14,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.InactiveFooterItemBinding
import org.mozilla.fenix.databinding.InactiveHeaderItemBinding
import org.mozilla.fenix.databinding.InactiveTabListItemBinding
+import org.mozilla.fenix.databinding.InactiveTabsAutoCloseBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.loadIntoView
import org.mozilla.fenix.ext.toShortUrl
@@ -72,6 +73,27 @@ sealed class InactiveTabViewHolder(itemView: View) : RecyclerView.ViewHolder(ite
}
}
+ class AutoCloseDialogHolder(
+ itemView: View,
+ interactor: InactiveTabsAutoCloseDialogInteractor
+ ) : InactiveTabViewHolder(itemView) {
+ private val binding = InactiveTabsAutoCloseBinding.bind(itemView)
+
+ init {
+ binding.closeButton.setOnClickListener {
+ interactor.onCloseClicked()
+ }
+
+ binding.action.setOnClickListener {
+ interactor.onEnabledAutoCloseClicked()
+ }
+ }
+
+ companion object {
+ const val LAYOUT_ID = R.layout.inactive_tabs_auto_close
+ }
+ }
+
/**
* A RecyclerView ViewHolder implementation for an inactive tab view.
*
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
index 473950a7e..8eb1fe01d 100644
--- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAdapter.kt
+++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAdapter.kt
@@ -15,10 +15,12 @@ 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.AutoCloseDialogHolder
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 org.mozilla.fenix.utils.Settings
import mozilla.components.support.base.observer.Observable as ComponentObservable
/**
@@ -44,16 +46,20 @@ class InactiveTabsAdapter(
private val browserTrayInteractor: BrowserTrayInteractor,
private val tabsTrayInteractor: TabsTrayInteractor,
private val featureName: String,
+ private val settings: Settings,
delegate: Observable = ObserverRegistry()
) : Adapter(DiffCallback), TabsTray, Observable by delegate {
internal lateinit var inactiveTabsInteractor: InactiveTabsInteractor
+ internal lateinit var inactiveTabsAutoCloseDialogInteractor: InactiveTabsAutoCloseDialogInteractor
+ internal var inActiveTabsCount: Int = 0
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InactiveTabViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(viewType, parent, false)
return when (viewType) {
+ AutoCloseDialogHolder.LAYOUT_ID -> AutoCloseDialogHolder(view, inactiveTabsAutoCloseDialogInteractor)
HeaderHolder.LAYOUT_ID -> HeaderHolder(view, inactiveTabsInteractor, tabsTrayInteractor)
TabViewHolder.LAYOUT_ID -> TabViewHolder(view, browserTrayInteractor, featureName)
FooterHolder.LAYOUT_ID -> FooterHolder(view)
@@ -71,7 +77,7 @@ class InactiveTabsAdapter(
val item = getItem(position) as Item.Footer
holder.bind(item.interval)
}
- is HeaderHolder -> {
+ is HeaderHolder, is AutoCloseDialogHolder -> {
// do nothing.
}
}
@@ -80,12 +86,19 @@ class InactiveTabsAdapter(
override fun getItemViewType(position: Int): Int {
return when (position) {
0 -> HeaderHolder.LAYOUT_ID
+ 1 -> if (settings.shouldShowInactiveTabsAutoCloseDialog(inActiveTabsCount)) {
+ AutoCloseDialogHolder.LAYOUT_ID
+ } else {
+ TabViewHolder.LAYOUT_ID
+ }
itemCount - 1 -> FooterHolder.LAYOUT_ID
else -> TabViewHolder.LAYOUT_ID
}
}
override fun updateTabs(tabs: Tabs) {
+ inActiveTabsCount = tabs.list.size
+
// Early return with an empty list to remove the header/footer items.
if (tabs.list.isEmpty()) {
submitList(emptyList())
@@ -100,8 +113,12 @@ class InactiveTabsAdapter(
val items = tabs.list.map { Item.Tab(it) }
val footer = Item.Footer(context.autoCloseInterval)
-
- submitList(listOf(Item.Header) + items + listOf(footer))
+ val headerItems = if (settings.shouldShowInactiveTabsAutoCloseDialog(items.size)) {
+ listOf(Item.Header, Item.AutoCloseMessage)
+ } else {
+ listOf(Item.Header)
+ }
+ submitList(headerItems + items + listOf(footer))
}
override fun isTabSelected(tabs: Tabs, position: Int): Boolean = false
@@ -136,6 +153,11 @@ class InactiveTabsAdapter(
*/
data class Tab(val tab: TabsTrayTab) : Item()
+ /**
+ * A dialog for when the inactive tabs section reach 20 tabs.
+ */
+ object AutoCloseMessage : Item()
+
/**
* A footer for the inactive tab section. This may be seen only
* when at least one inactive tab is present.
diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAutoCloseDialogController.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAutoCloseDialogController.kt
new file mode 100644
index 000000000..137d7d318
--- /dev/null
+++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAutoCloseDialogController.kt
@@ -0,0 +1,44 @@
+/* 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 androidx.annotation.VisibleForTesting
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.tabstray.TabsTray
+import mozilla.components.feature.tabs.ext.toTabs
+import org.mozilla.fenix.utils.Settings
+
+class InactiveTabsAutoCloseDialogController(
+ private val browserStore: BrowserStore,
+ private val settings: Settings,
+ private val tabFilter: (TabSessionState) -> Boolean,
+ private val tray: TabsTray
+) {
+ /**
+ * Dismiss the auto-close dialog.
+ */
+ fun close() {
+ settings.hasInactiveTabsAutoCloseDialogBeenDismissed = true
+ refeshInactiveTabsSecion()
+ }
+
+ /**
+ * Enable the auto-close feature with the after a month setting.
+ */
+ fun enableAutoClosed() {
+ settings.closeTabsAfterOneMonth = true
+ settings.closeTabsAfterOneWeek = false
+ settings.closeTabsAfterOneDay = false
+ settings.manuallyCloseTabs = false
+ refeshInactiveTabsSecion()
+ }
+
+ @VisibleForTesting
+ internal fun refeshInactiveTabsSecion() {
+ val tabs = browserStore.state.toTabs { tabFilter.invoke(it) }
+ tray.updateTabs(tabs)
+ }
+}
diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAutoCloseDialogInteractor.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAutoCloseDialogInteractor.kt
new file mode 100644
index 000000000..ec3d930d9
--- /dev/null
+++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAutoCloseDialogInteractor.kt
@@ -0,0 +1,22 @@
+/* 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
+
+interface InactiveTabsAutoCloseDialogInteractor {
+ fun onCloseClicked()
+ fun onEnabledAutoCloseClicked()
+}
+
+class DefaultInactiveTabsAutoCloseDialogInteractor(
+ private val controller: InactiveTabsAutoCloseDialogController
+) : InactiveTabsAutoCloseDialogInteractor {
+ override fun onCloseClicked() {
+ controller.close()
+ }
+
+ override fun onEnabledAutoCloseClicked() {
+ controller.enableAutoClosed()
+ }
+}
diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/NormalBrowserTrayList.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/NormalBrowserTrayList.kt
index f24c5ce37..0c3b39af2 100644
--- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/NormalBrowserTrayList.kt
+++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/NormalBrowserTrayList.kt
@@ -39,23 +39,35 @@ class NormalBrowserTrayList @JvmOverloads constructor(
private val swipeDelegate = SwipeToDeleteDelegate()
private val concatAdapter by lazy { adapter as ConcatAdapter }
private val tabSorter by lazy { TabSorter(context, concatAdapter, context.components.core.store) }
- private val inactiveTabsInteractor by lazy {
- val tabFilter: (TabSessionState) -> Boolean = filter@{
- if (!context.settings().inactiveTabsAreEnabled) {
- return@filter false
- }
- it.isNormalTabInactive(maxActiveTime)
+ private val inactiveTabsFilter: (TabSessionState) -> Boolean = filter@{
+ if (!context.settings().inactiveTabsAreEnabled) {
+ return@filter false
}
+ it.isNormalTabInactive(maxActiveTime)
+ }
+
+ private val inactiveTabsInteractor by lazy {
DefaultInactiveTabsInteractor(
InactiveTabsController(
context.components.core.store,
- tabFilter,
+ inactiveTabsFilter,
concatAdapter.inactiveTabsAdapter,
context.components.analytics.metrics
)
)
}
+ private val inactiveTabsAutoCloseInteractor by lazy {
+ DefaultInactiveTabsAutoCloseDialogInteractor(
+ InactiveTabsAutoCloseDialogController(
+ context.components.core.store,
+ context.settings(),
+ inactiveTabsFilter,
+ concatAdapter.inactiveTabsAdapter
+ )
+ )
+ }
+
override val tabsFeature by lazy {
TabsFeature(
tabSorter,
@@ -81,6 +93,7 @@ class NormalBrowserTrayList @JvmOverloads constructor(
super.onAttachedToWindow()
concatAdapter.inactiveTabsAdapter.inactiveTabsInteractor = inactiveTabsInteractor
+ concatAdapter.inactiveTabsAdapter.inactiveTabsAutoCloseDialogInteractor = inactiveTabsAutoCloseInteractor
tabsFeature.start()
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 1cf25e436..1c5a9c9d3 100644
--- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt
+++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt
@@ -67,6 +67,7 @@ class Settings(private val appContext: Context) : PreferencesHolder {
private const val CFR_COUNT_CONDITION_FOCUS_INSTALLED = 1
private const val CFR_COUNT_CONDITION_FOCUS_NOT_INSTALLED = 3
private const val APP_LAUNCHES_TO_SHOW_DEFAULT_BROWSER_CARD = 3
+ private const val INACTIVE_TAB_MINIMUM_TO_SHOW_AUTO_CLOSE_DIALOG = 20
const val FOUR_HOURS_MS = 60 * 60 * 4 * 1000L
const val ONE_DAY_MS = 60 * 60 * 24 * 1000L
@@ -838,6 +839,26 @@ class Settings(private val appContext: Context) : PreferencesHolder {
default = true
)
+ /**
+ * Indicates if the auto-close dialog for inactive tabs has been dismissed before.
+ */
+ var hasInactiveTabsAutoCloseDialogBeenDismissed by booleanPreference(
+ appContext.getPreferenceKey(R.string.pref_key_has_inactive_tabs_auto_close_dialog_dismissed),
+ default = false
+ )
+
+ /**
+ * Indicates if the auto-close dialog should be visible based on
+ * if the user has dismissed it before [hasInactiveTabsAutoCloseDialogBeenDismissed],
+ * if the minimum number of tabs has been accumulated [numbersOfTabs]
+ * and if the auto-close setting is already set to [closeTabsAfterOneMonth].
+ */
+ fun shouldShowInactiveTabsAutoCloseDialog(numbersOfTabs: Int): Boolean {
+ return !hasInactiveTabsAutoCloseDialogBeenDismissed &&
+ numbersOfTabs >= INACTIVE_TAB_MINIMUM_TO_SHOW_AUTO_CLOSE_DIALOG &&
+ !closeTabsAfterOneMonth
+ }
+
/**
* Indicates if the jump back in CRF should be shown.
*/
diff --git a/app/src/main/res/drawable/inactive_tab_auto_close_border_background.xml b/app/src/main/res/drawable/inactive_tab_auto_close_border_background.xml
new file mode 100644
index 000000000..bc80384e3
--- /dev/null
+++ b/app/src/main/res/drawable/inactive_tab_auto_close_border_background.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/inactive_tabs_auto_close.xml b/app/src/main/res/layout/inactive_tabs_auto_close.xml
new file mode 100644
index 000000000..e8a1a4133
--- /dev/null
+++ b/app/src/main/res/layout/inactive_tabs_auto_close.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml
index 659954185..96dbbdeae 100644
--- a/app/src/main/res/values/preference_keys.xml
+++ b/app/src/main/res/values/preference_keys.xml
@@ -227,6 +227,8 @@
pref_key_should_show_inactive_tabs_popup
+
+ pref_key_has_inactive_tabs_auto_close_dialog_dismissed
pref_key_should_show_jump_back_in_tabs_popup
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 82a24648b..7e814fe35 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -110,6 +110,14 @@
Tabs you haven’t viewed for two weeks get moved here.
Turn off in settings
+
+ Auto-close after one month?
+
+ Firefox can close tabs you haven’t viewed over the past month.
+
+ Close
+
+ Turn on auto close
diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/DefaultInactiveTabsAutoCloseDialogInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/DefaultInactiveTabsAutoCloseDialogInteractorTest.kt
new file mode 100644
index 000000000..110987efe
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/DefaultInactiveTabsAutoCloseDialogInteractorTest.kt
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.tabstray.browser
+
+import io.mockk.mockk
+import io.mockk.verify
+import org.junit.Test
+
+class DefaultInactiveTabsAutoCloseDialogInteractorTest {
+
+ @Test
+ fun `WHEN onCloseClicked THEN close`() {
+ val controller: InactiveTabsAutoCloseDialogController = mockk(relaxed = true)
+ val interactor = DefaultInactiveTabsAutoCloseDialogInteractor(controller)
+
+ interactor.onCloseClicked()
+
+ verify { controller.close() }
+ }
+
+ @Test
+ fun `WHEN onEnabledAutoCloseClicked THEN enableAutoClosed`() {
+ val controller: InactiveTabsAutoCloseDialogController = mockk(relaxed = true)
+ val interactor = DefaultInactiveTabsAutoCloseDialogInteractor(controller)
+
+ interactor.onEnabledAutoCloseClicked()
+
+ verify { controller.enableAutoClosed() }
+ }
+}
diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAutoCloseDialogControllerTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAutoCloseDialogControllerTest.kt
new file mode 100644
index 000000000..6629c7e98
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabsAutoCloseDialogControllerTest.kt
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.tabstray.browser
+
+import io.mockk.Runs
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.spyk
+import io.mockk.verify
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.tabstray.TabsTray
+import org.junit.Test
+import org.mozilla.fenix.utils.Settings
+
+class InactiveTabsAutoCloseDialogControllerTest {
+
+ @Test
+ fun `WHEN close THEN update settings and refresh`() {
+ val filter: (TabSessionState) -> Boolean = { !it.content.private }
+ val store = BrowserStore()
+ val settings: Settings = mockk(relaxed = true)
+ val tray: TabsTray = mockk(relaxed = true)
+ val controller = spyk(InactiveTabsAutoCloseDialogController(store, settings, filter, tray))
+
+ every { controller.refeshInactiveTabsSecion() } just Runs
+
+ controller.close()
+
+ verify { settings.hasInactiveTabsAutoCloseDialogBeenDismissed = true }
+ verify { controller.refeshInactiveTabsSecion() }
+ }
+
+ @Test
+ fun `WHEN enableAutoClosed THEN update closeTabsAfterOneMonth settings and refresh`() {
+ val filter: (TabSessionState) -> Boolean = { !it.content.private }
+ val store = BrowserStore()
+ val settings: Settings = mockk(relaxed = true)
+ val tray: TabsTray = mockk(relaxed = true)
+ val controller = spyk(InactiveTabsAutoCloseDialogController(store, settings, filter, tray))
+
+ every { controller.refeshInactiveTabsSecion() } just Runs
+
+ controller.enableAutoClosed()
+
+ verify { settings.closeTabsAfterOneMonth = true }
+ verify { settings.closeTabsAfterOneWeek = false }
+ verify { settings.closeTabsAfterOneDay = false }
+ verify { settings.manuallyCloseTabs = false }
+ verify { controller.refeshInactiveTabsSecion() }
+ }
+}
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 313c3861e..d2bc07c4a 100644
--- a/app/src/test/java/org/mozilla/fenix/utils/SettingsTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/utils/SettingsTest.kt
@@ -753,4 +753,34 @@ class SettingsTest {
// Then
assertTrue(settings.inactiveTabsAreEnabled)
}
+
+ @Test
+ fun `GIVEN shouldShowInactiveTabsAutoCloseDialog WHEN the dialog has been dismissed before THEN no show the dialog`() {
+ val settings = spyk(settings)
+ every { settings.hasInactiveTabsAutoCloseDialogBeenDismissed } returns true
+
+ assertFalse(settings.shouldShowInactiveTabsAutoCloseDialog(20))
+ }
+
+ @Test
+ fun `GIVEN shouldShowInactiveTabsAutoCloseDialog WHEN the inactive tabs are less than the minimum THEN no show the dialog`() {
+ assertFalse(settings.shouldShowInactiveTabsAutoCloseDialog(19))
+ }
+
+ @Test
+ fun `GIVEN shouldShowInactiveTabsAutoCloseDialog WHEN closeTabsAfterOneMonth is already selected THEN no show the dialog`() {
+ val settings = spyk(settings)
+ every { settings.closeTabsAfterOneMonth } returns true
+
+ assertFalse(settings.shouldShowInactiveTabsAutoCloseDialog(19))
+ }
+
+ @Test
+ fun `GIVEN shouldShowInactiveTabsAutoCloseDialog WHEN the dialog has not been dismissed, with more inactive tabs than the queried and closeTabsAfterOneMonth not set THEN show the dialog`() {
+ val settings = spyk(settings)
+ every { settings.closeTabsAfterOneMonth } returns false
+ every { settings.hasInactiveTabsAutoCloseDialogBeenDismissed } returns false
+
+ assertTrue(settings.shouldShowInactiveTabsAutoCloseDialog(20))
+ }
}