For #21791 Adds tab auto-close prompt

upstream-sync
Arturo Mejia 3 years ago committed by mergify[bot]
parent 58e12b18e6
commit 08256ac68c

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

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

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

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

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

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

@ -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.
*/

@ -0,0 +1,14 @@
<?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/. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke
android:width="1dp"
android:color="?toolbarDivider" />
<corners android:radius="8dp" />
<solid android:color="?above" />
</shape>

@ -0,0 +1,79 @@
<?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/. -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:paddingHorizontal="1dp"
android:background="@color/photonLightGrey30">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?above"
android:clickable="false"
android:clipToPadding="false"
android:focusable="true"
android:padding="16dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/banner_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="false"
android:clipToPadding="false"
android:background="@drawable/inactive_tab_auto_close_border_background"
android:focusable="true"
android:padding="16dp">
<TextView
android:id="@+id/banner_info_message"
android:layout_height="wrap_content"
android:layout_width="0dp"
android:padding="8dp"
android:text="@string/tab_tray_inactive_auto_close_title"
android:textAppearance="@style/Header14TextStyle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/close_button"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/close_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/tab_tray_inactive_auto_close_button_content_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_close" />
<TextView
android:id="@+id/message"
android:layout_height="wrap_content"
android:layout_width="0dp"
android:padding="8dp"
android:text="@string/tab_tray_inactive_auto_close_body"
android:textAppearance="@style/Body14TextStyle"
app:layout_constraintTop_toBottomOf="@id/banner_info_message"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/action"
style="@style/DialogButtonStyleDark"
android:background="?android:attr/selectableItemBackground"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginEnd="3dp"
android:textAllCaps="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:text="@string/tab_tray_inactive_turn_on_auto_close_button"
app:layout_constraintTop_toBottomOf="@+id/message" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
</FrameLayout>

@ -227,6 +227,8 @@
<!-- A value of `true` means the Inactive tabs onboarding popup has not been shown yet -->
<string name="pref_key_should_show_inactive_tabs_popup" translatable="false">pref_key_should_show_inactive_tabs_popup</string>
<!-- A value of `true` means the Inactive tabs auto close dialog has been dismissed by the user -->
<string name="pref_key_has_inactive_tabs_auto_close_dialog_dismissed" translatable="false">pref_key_has_inactive_tabs_auto_close_dialog_dismissed</string>
<!-- A value of `true` means the jump back in onboarding popup has not been shown yet -->
<string name="pref_key_should_show_jump_back_in_tabs_popup" translatable="false">pref_key_should_show_jump_back_in_tabs_popup</string>

@ -110,6 +110,14 @@
<string name="tab_tray_inactive_onboarding_message">Tabs you havent viewed for two weeks get moved here.</string>
<!-- Text for the action link to go to Settings for inactive tabs. -->
<string name="tab_tray_inactive_onboarding_button_text">Turn off in settings</string>
<!-- Text for title for the auto-close dialog of the inactive tabs. -->
<string name="tab_tray_inactive_auto_close_title">Auto-close after one month?</string>
<!-- Text for the body for the auto-close dialog of the inactive tabs. -->
<string name="tab_tray_inactive_auto_close_body">Firefox can close tabs you havent viewed over the past month.</string>
<!-- Content description for close button in the auto-close dialog of the inactive tabs. -->
<string name="tab_tray_inactive_auto_close_button_content_description">Close</string>
<!-- Text for turn on auto close tabs button in the auto-close dialog of the inactive tabs. -->
<string name="tab_tray_inactive_turn_on_auto_close_button">Turn on auto close</string>
<!-- Home screen icons - Long press shortcuts -->
<!-- Shortcut action to open new tab -->

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

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

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

Loading…
Cancel
Save