From 08a84f835393164addde57ce01297b6a93ba5876 Mon Sep 17 00:00:00 2001 From: Noah Bond Date: Thu, 2 Jun 2022 14:59:47 -0700 Subject: [PATCH] For #21897 - Convert inactive tabs to compose --- .../fenix/tabstray/TabsTrayFragment.kt | 29 +- .../fenix/tabstray/TrayPagerAdapter.kt | 31 +- .../tabstray/browser/InactiveTabViewHolder.kt | 222 +++++--------- .../tabstray/browser/InactiveTabsAdapter.kt | 160 +++------- .../tabstray/browser/InactiveTabsBinding.kt | 31 -- .../browser/InactiveTabsController.kt | 51 ++-- .../browser/InactiveTabsInteractor.kt | 32 +- .../tabstray/browser/NormalBrowserTrayList.kt | 21 -- .../fenix/tabstray/ext/TabSelectors.kt | 4 +- .../fenix/tabstray/ext/TabSessionState.kt | 6 + .../tabstray/inactivetabs/InactiveTabs.kt | 278 ++++++++++++++++++ .../AbstractBrowserPageViewHolder.kt | 27 +- .../NormalBrowserPageViewHolder.kt | 45 ++- .../res/drawable/card_list_row_background.xml | 8 - ...ctive_tab_auto_close_border_background.xml | 14 - .../res/drawable/rounded_bottom_corners.xml | 9 - .../main/res/drawable/rounded_top_corners.xml | 8 - .../main/res/layout/inactive_footer_item.xml | 25 -- .../main/res/layout/inactive_header_item.xml | 70 ----- .../res/layout/inactive_tab_list_item.xml | 22 -- .../res/layout/inactive_tabs_auto_close.xml | 78 ----- app/src/main/res/values/strings.xml | 6 +- .../fenix/tabstray/TabsTrayFragmentTest.kt | 11 +- .../DefaultInactiveTabsInteractorTest.kt | 34 +++ .../browser/InactiveTabViewHolderTest.kt | 39 --- .../browser/InactiveTabsBindingTest.kt | 58 ---- .../browser/InactiveTabsControllerTest.kt | 120 ++++---- 27 files changed, 652 insertions(+), 787 deletions(-) delete mode 100644 app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsBinding.kt create mode 100644 app/src/main/java/org/mozilla/fenix/tabstray/inactivetabs/InactiveTabs.kt delete mode 100644 app/src/main/res/drawable/card_list_row_background.xml delete mode 100644 app/src/main/res/drawable/inactive_tab_auto_close_border_background.xml delete mode 100644 app/src/main/res/drawable/rounded_bottom_corners.xml delete mode 100644 app/src/main/res/drawable/rounded_top_corners.xml delete mode 100644 app/src/main/res/layout/inactive_footer_item.xml delete mode 100644 app/src/main/res/layout/inactive_header_item.xml delete mode 100644 app/src/main/res/layout/inactive_tab_list_item.xml delete mode 100644 app/src/main/res/layout/inactive_tabs_auto_close.xml delete mode 100644 app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolderTest.kt delete mode 100644 app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabsBindingTest.kt diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt index 5ffe0d9c4..3a1daaa7f 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt @@ -17,6 +17,7 @@ import androidx.appcompat.app.AppCompatDialogFragment import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.fragment.app.setFragmentResultListener +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs @@ -228,11 +229,12 @@ class TabsTrayFragment : AppCompatDialogFragment() { setupMenu(navigationInteractor) setupPager( - view.context, - tabsTrayStore, - tabsTrayInteractor, - browserTrayInteractor, - navigationInteractor + context = view.context, + lifecycleOwner = viewLifecycleOwner, + store = tabsTrayStore, + trayInteractor = tabsTrayInteractor, + browserInteractor = browserTrayInteractor, + navigationInteractor = navigationInteractor, ) setupBackgroundDismissalListener { @@ -467,8 +469,10 @@ class TabsTrayFragment : AppCompatDialogFragment() { } @VisibleForTesting + @Suppress("LongParameterList") internal fun setupPager( context: Context, + lifecycleOwner: LifecycleOwner, store: TabsTrayStore, trayInteractor: TabsTrayInteractor, browserInteractor: BrowserTrayInteractor, @@ -476,13 +480,14 @@ class TabsTrayFragment : AppCompatDialogFragment() { ) { tabsTrayBinding.tabsTray.apply { adapter = TrayPagerAdapter( - context, - store, - browserInteractor, - navigationInteractor, - trayInteractor, - requireComponents.core.store, - requireComponents.appStore, + context = context, + lifecycleOwner = lifecycleOwner, + tabsTrayStore = store, + browserInteractor = browserInteractor, + navInteractor = navigationInteractor, + tabsTrayInteractor = trayInteractor, + browserStore = requireComponents.core.store, + appStore = requireComponents.appStore, ) isUserInputEnabled = false } 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 3317d408d..daec9e2c2 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt @@ -9,14 +9,18 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.annotation.VisibleForTesting import androidx.compose.ui.platform.ComposeView +import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.RecyclerView import mozilla.components.browser.state.store.BrowserStore import org.mozilla.fenix.components.AppStore +import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings import org.mozilla.fenix.tabstray.browser.BrowserTabsAdapter import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor +import org.mozilla.fenix.tabstray.browser.DefaultInactiveTabsInteractor import org.mozilla.fenix.tabstray.browser.InactiveTabsAdapter +import org.mozilla.fenix.tabstray.browser.InactiveTabsController import org.mozilla.fenix.tabstray.browser.TabGroupAdapter import org.mozilla.fenix.tabstray.browser.TitleHeaderAdapter import org.mozilla.fenix.tabstray.viewholders.AbstractPageViewHolder @@ -27,10 +31,11 @@ import org.mozilla.fenix.tabstray.viewholders.SyncedTabsPageViewHolder @Suppress("LongParameterList") class TrayPagerAdapter( @VisibleForTesting internal val context: Context, + @VisibleForTesting internal val lifecycleOwner: LifecycleOwner, @VisibleForTesting internal val tabsTrayStore: TabsTrayStore, @VisibleForTesting internal val browserInteractor: BrowserTrayInteractor, @VisibleForTesting internal val navInteractor: NavigationInteractor, - @VisibleForTesting internal val interactor: TabsTrayInteractor, + @VisibleForTesting internal val tabsTrayInteractor: TabsTrayInteractor, @VisibleForTesting internal val browserStore: BrowserStore, @VisibleForTesting internal val appStore: AppStore ) : RecyclerView.Adapter() { @@ -42,12 +47,29 @@ class TrayPagerAdapter( */ private val normalAdapter by lazy { ConcatAdapter( - InactiveTabsAdapter(context, browserInteractor, interactor, INACTIVE_TABS_FEATURE_NAME, context.settings()), + InactiveTabsAdapter( + lifecycleOwner = lifecycleOwner, + tabsTrayStore = tabsTrayStore, + tabsTrayInteractor = tabsTrayInteractor, + inactiveTabsInteractor = inactiveTabsInteractor, + featureName = INACTIVE_TABS_FEATURE_NAME, + ), TabGroupAdapter(context, browserInteractor, tabsTrayStore, TAB_GROUP_FEATURE_NAME), TitleHeaderAdapter(), BrowserTabsAdapter(context, browserInteractor, tabsTrayStore, TABS_TRAY_FEATURE_NAME) ) } + + private val inactiveTabsInteractor by lazy { + DefaultInactiveTabsInteractor( + InactiveTabsController( + appStore = context.components.appStore, + settings = context.settings(), + browserInteractor = browserInteractor, + ) + ) + } + private val privateAdapter by lazy { BrowserTabsAdapter( context, @@ -62,10 +84,11 @@ class TrayPagerAdapter( NormalBrowserPageViewHolder.LAYOUT_ID -> { NormalBrowserPageViewHolder( LayoutInflater.from(parent.context).inflate(viewType, parent, false), + lifecycleOwner, tabsTrayStore, browserStore, appStore, - interactor + tabsTrayInteractor ) } PrivateBrowserPageViewHolder.LAYOUT_ID -> { @@ -73,7 +96,7 @@ class TrayPagerAdapter( LayoutInflater.from(parent.context).inflate(viewType, parent, false), tabsTrayStore, browserStore, - interactor + tabsTrayInteractor ) } SyncedTabsPageViewHolder.LAYOUT_ID -> { 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 b45be254d..5e96c75de 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 @@ -5,165 +5,95 @@ package org.mozilla.fenix.tabstray.browser import android.view.View -import androidx.core.view.updatePadding -import androidx.recyclerview.widget.RecyclerView -import mozilla.components.browser.state.state.TabSessionState -import mozilla.components.browser.tabstray.TabsTray -import mozilla.components.browser.toolbar.MAX_URI_LENGTH +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.ComposeView +import androidx.lifecycle.LifecycleOwner +import mozilla.components.lib.state.ext.observeAsComposableState import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar -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 -import org.mozilla.fenix.home.topsites.dpToPx -import org.mozilla.fenix.GleanMetrics.TabsTray as TabsTrayMetrics +import org.mozilla.fenix.components.components +import org.mozilla.fenix.compose.ComposeViewHolder import org.mozilla.fenix.tabstray.TabsTrayFragment import org.mozilla.fenix.tabstray.TabsTrayInteractor +import org.mozilla.fenix.tabstray.TabsTrayState +import org.mozilla.fenix.tabstray.TabsTrayStore +import org.mozilla.fenix.tabstray.TrayPagerAdapter +import org.mozilla.fenix.tabstray.inactivetabs.InactiveTabsList +import org.mozilla.fenix.GleanMetrics.TabsTray as TabsTrayMetrics -sealed class InactiveTabViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - - class HeaderHolder( - itemView: View, - inactiveTabsInteractor: InactiveTabsInteractor, - tabsTrayInteractor: TabsTrayInteractor, - ) : InactiveTabViewHolder(itemView) { - - private val binding = InactiveHeaderItemBinding.bind(itemView) - - init { - itemView.apply { - isActivated = itemView.context.components.appStore.state.inactiveTabsExpanded - - correctHeaderBorder(isActivated) - - setOnClickListener { - val newState = !it.isActivated - - inactiveTabsInteractor.onHeaderClicked(newState) - - it.isActivated = newState - - correctHeaderBorder(isActivated) - } - - binding.delete.setOnClickListener { - tabsTrayInteractor.onDeleteInactiveTabs() - } - } - } - - /** - * When the header is collapsed we use its bottom border instead of the footer's - */ - private fun correctHeaderBorder(isActivated: Boolean) { - binding.inactiveHeaderBorder.updatePadding( - bottom = binding.root.context.dpToPx(if (isActivated) 0f else 1f) - ) - } - - companion object { - const val LAYOUT_ID = R.layout.inactive_header_item - } - } - - class AutoCloseDialogHolder( - itemView: View, - interactor: InactiveTabsAutoCloseDialogInteractor - ) : InactiveTabViewHolder(itemView) { - private val binding = InactiveTabsAutoCloseBinding.bind(itemView) - - init { +/** + * The [ComposeViewHolder] for displaying the section of inactive tabs in [TrayPagerAdapter]. + * + * @param composeView [ComposeView] which will be populated with Jetpack Compose UI content. + * @param lifecycleOwner [LifecycleOwner] to which this Composable will be tied to. + * @param tabsTrayStore [TabsTrayStore] used to listen for changes to [TabsTrayState.inactiveTabs]. + * @param tabsTrayInteractor [TabsTrayInteractor] used to handle deleting all inactive tabs. + * @param inactiveTabsInteractor [InactiveTabsInteractor] used to respond to interactions with the inactive tabs header + * and the auto close dialog. + */ +@Suppress("LongParameterList") +class InactiveTabViewHolder( + composeView: ComposeView, + lifecycleOwner: LifecycleOwner, + private val tabsTrayStore: TabsTrayStore, + private val tabsTrayInteractor: TabsTrayInteractor, + private val inactiveTabsInteractor: InactiveTabsInteractor, +) : ComposeViewHolder(composeView, lifecycleOwner) { + + @Composable + override fun Content() { + val expanded = components.appStore + .observeAsComposableState { state -> state.inactiveTabsExpanded }.value ?: false + val inactiveTabs = tabsTrayStore + .observeAsComposableState { state -> state.inactiveTabs }.value ?: emptyList() + val showInactiveTabsAutoCloseDialog = + components.settings.shouldShowInactiveTabsAutoCloseDialog(inactiveTabs.size) + var showAutoClosePrompt by remember { mutableStateOf(showInactiveTabsAutoCloseDialog) } + + if (showInactiveTabsAutoCloseDialog) { TabsTrayMetrics.autoCloseSeen.record(NoExtras()) - - binding.message.text = with(binding.root.context) { - getString( - R.string.tab_tray_inactive_auto_close_body_2, - getString(R.string.app_name) - ) - } - binding.closeButton.setOnClickListener { - interactor.onCloseClicked() - } - - binding.action.setOnClickListener { - interactor.onEnabledAutoCloseClicked() - showConfirmationSnackbar() - } } - private fun showConfirmationSnackbar() { - val context = binding.root.context - val view = binding.root - val text = context.getString(R.string.inactive_tabs_auto_close_message_snackbar) - val snackbar = FenixSnackbar.make( - view = view, - duration = FenixSnackbar.LENGTH_SHORT, - isDisplayedWithBrowserToolbar = true - ).setText(text) - snackbar.view.elevation = TabsTrayFragment.ELEVATION - snackbar.show() - } - - companion object { - const val LAYOUT_ID = R.layout.inactive_tabs_auto_close + if (inactiveTabs.isNotEmpty()) { + InactiveTabsList( + inactiveTabs = inactiveTabs, + expanded = expanded, + showAutoCloseDialog = showAutoClosePrompt, + onHeaderClick = { inactiveTabsInteractor.onHeaderClicked(!expanded) }, + onDeleteAllButtonClick = tabsTrayInteractor::onDeleteInactiveTabs, + onAutoCloseDismissClick = { + inactiveTabsInteractor.onCloseClicked() + showAutoClosePrompt = !showAutoClosePrompt + }, + onEnableAutoCloseClick = { + inactiveTabsInteractor.onEnabledAutoCloseClicked() + showAutoClosePrompt = !showAutoClosePrompt + showConfirmationSnackbar() + }, + onTabClick = inactiveTabsInteractor::onTabClicked, + onTabCloseClick = inactiveTabsInteractor::onTabClosed, + ) } } - /** - * A RecyclerView ViewHolder implementation for an inactive tab view. - * - * @param itemView the inactive tab [View]. - * @param featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting. - */ - class TabViewHolder( - itemView: View, - private val delegate: TabsTray.Delegate, - private val featureName: String - ) : InactiveTabViewHolder(itemView) { - - private val binding = InactiveTabListItemBinding.bind(itemView) - - fun bind(tab: TabSessionState) { - val components = itemView.context.components - val title = tab.content.title.ifEmpty { tab.content.url.take(MAX_URI_LENGTH) } - val url = tab.content.url.toShortUrl(components.publicSuffixList).take(MAX_URI_LENGTH) - - itemView.setOnClickListener { - TabsTrayMetrics.openInactiveTab.add() - delegate.onTabSelected(tab, featureName) - } - - binding.siteListItem.apply { - components.core.icons.loadIntoView(iconView, tab.content.url) - setText(title, url) - setSecondaryButton( - R.drawable.mozac_ic_close, - R.string.content_description_close_button - ) { - TabsTrayMetrics.closeInactiveTab.add() - delegate.onTabClosed(tab, featureName) - } - } - } - - companion object { - const val LAYOUT_ID = R.layout.inactive_tab_list_item - } + private fun showConfirmationSnackbar() { + val context = composeView.context + val text = context.getString(R.string.inactive_tabs_auto_close_message_snackbar) + val snackbar = FenixSnackbar.make( + view = composeView, + duration = FenixSnackbar.LENGTH_SHORT, + isDisplayedWithBrowserToolbar = true + ).setText(text) + snackbar.view.elevation = TabsTrayFragment.ELEVATION + snackbar.show() } - class FooterHolder(itemView: View) : InactiveTabViewHolder(itemView) { - - init { - InactiveFooterItemBinding.bind(itemView) - } - - companion object { - const val LAYOUT_ID = R.layout.inactive_footer_item - } + companion object { + val LAYOUT_ID = View.generateViewId() } } 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 38433efad..54d62ef9f 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 @@ -4,149 +4,57 @@ 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.browser.state.state.TabPartition -import mozilla.components.browser.state.state.TabSessionState -import mozilla.components.browser.tabstray.TabsTray -import org.mozilla.fenix.components.Components -import org.mozilla.fenix.ext.components +import androidx.compose.ui.platform.ComposeView +import androidx.lifecycle.LifecycleOwner +import androidx.recyclerview.widget.RecyclerView 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.utils.Settings +import org.mozilla.fenix.tabstray.TabsTrayState +import org.mozilla.fenix.tabstray.TabsTrayStore /** - * A convenience alias for readability. - */ -private typealias Adapter = ListAdapter - -/** - * The [ListAdapter] for displaying the list of inactive tabs. + * The adapter for displaying the section of inactive tabs. * - * @param context [Context] used for various platform interactions or accessing [Components] - * @param browserTrayInteractor [BrowserTrayInteractor] handling tabs interactions in a tab tray. - * @param featureName [String] representing the name of the feature displaying tabs. Used in telemetry reporting. + * @param lifecycleOwner [LifecycleOwner] to which the Composable will be tied to. + * @param tabsTrayStore [TabsTrayStore] used to listen for changes to [TabsTrayState.inactiveTabs]. + * @param tabsTrayInteractor [TabsTrayInteractor] used to handle deleting all inactive tabs. + * @param inactiveTabsInteractor [InactiveTabsInteractor] used to respond to interactions with the inactive tabs header + * and the auto close dialog. + * @param featureName [String] representing the name of the inactive tabs feature for telemetry reporting. */ +@Suppress("LongParameterList") class InactiveTabsAdapter( - private val context: Context, - private val browserTrayInteractor: BrowserTrayInteractor, + private val lifecycleOwner: LifecycleOwner, + private val tabsTrayStore: TabsTrayStore, private val tabsTrayInteractor: TabsTrayInteractor, + private val inactiveTabsInteractor: InactiveTabsInteractor, override val featureName: String, - private val settings: Settings, -) : Adapter(DiffCallback), TabsTray, FeatureNameHolder { +) : RecyclerView.Adapter(), FeatureNameHolder { - internal lateinit var inactiveTabsInteractor: InactiveTabsInteractor - private var inActiveTabsCount: Int = 0 + override fun getItemCount(): Int = 1 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, inactiveTabsInteractor) - HeaderHolder.LAYOUT_ID -> HeaderHolder(view, inactiveTabsInteractor, tabsTrayInteractor) - TabViewHolder.LAYOUT_ID -> TabViewHolder(view, browserTrayInteractor, featureName) - FooterHolder.LAYOUT_ID -> FooterHolder(view) - else -> throw IllegalStateException("Unknown viewType: $viewType") - } + return InactiveTabViewHolder( + composeView = ComposeView(parent.context), + lifecycleOwner = lifecycleOwner, + tabsTrayStore = tabsTrayStore, + inactiveTabsInteractor = inactiveTabsInteractor, + tabsTrayInteractor = tabsTrayInteractor, + ) } override fun onBindViewHolder(holder: InactiveTabViewHolder, position: Int) { - when (holder) { - is TabViewHolder -> { - val item = getItem(position) as Item.Tab - holder.bind(item.tab) - } - - is FooterHolder, is HeaderHolder, is AutoCloseDialogHolder -> { - // do nothing. - } - } - } - - 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: List, tabPartition: TabPartition?, selectedTabId: String?) { - inActiveTabsCount = tabs.size - - // Early return with an empty list to remove the header/footer items. - if (tabs.isEmpty()) { - submitList(emptyList()) - return - } - - // If we have items, but we should be in a collapsed state. - if (!context.components.appStore.state.inactiveTabsExpanded) { - submitList(listOf(Item.Header)) - return - } - - val items = tabs.map { Item.Tab(it) } - val footer = Item.Footer - val headerItems = if (settings.shouldShowInactiveTabsAutoCloseDialog(items.size)) { - listOf(Item.Header, Item.AutoCloseMessage) - } else { - listOf(Item.Header) - } - submitList(headerItems + items + listOf(footer)) + // no-op. This ViewHolder receives the TabsTrayStore as argument and will observe that + // without the need for us to manually update here for the data to be displayed. } - private object DiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean { - return if (oldItem is Item.Tab && newItem is Item.Tab) { - oldItem.tab.id == newItem.tab.id - } else { - oldItem == newItem - } - } - - override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { - return oldItem == newItem - } - } - - /** - * The types of different data we can put into the [InactiveTabsAdapter]. - */ - sealed class Item { - - /** - * A title header for the inactive tab section. This may be seen only - * when at least one inactive tab is present. - */ - object Header : Item() - - /** - * A tab that is now considered inactive. - */ - data class Tab(val tab: TabSessionState) : Item() - - /** - * A dialog for when the inactive tabs section reach 20 tabs. - */ - object AutoCloseMessage : Item() + override fun getItemViewType(position: Int): Int = InactiveTabViewHolder.LAYOUT_ID - /** - * A footer for the inactive tab section. This may be seen only - * when at least one inactive tab is present. - */ - object Footer : Item() + override fun onViewRecycled(holder: InactiveTabViewHolder) { + // no op + // This previously called "composeView.disposeComposition" which would have the + // entire Composable destroyed and recreated when this View is scrolled off or on screen again. + // This View already listens and maps store updates. Avoid creating and binding new Views. + // The composition will live until the ViewTreeLifecycleOwner to which it's attached to is destroyed. } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsBinding.kt deleted file mode 100644 index 20cb8b6c2..000000000 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsBinding.kt +++ /dev/null @@ -1,31 +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 kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.map -import mozilla.components.browser.tabstray.TabsTray -import mozilla.components.lib.state.helpers.AbstractBinding -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged -import org.mozilla.fenix.tabstray.TabsTrayState -import org.mozilla.fenix.tabstray.TabsTrayStore - -/** - * An inactive tabs observer that updates the provided [TabsTray]. - */ -class InactiveTabsBinding( - store: TabsTrayStore, - private val tray: TabsTray -) : AbstractBinding(store) { - override suspend fun onState(flow: Flow) { - flow.map { it.inactiveTabs } - .ifChanged() - .collect { - // We pass null for the selected tab id here, because inactive tabs doesn't care. - tray.updateTabs(it, null, null) - } - } -} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsController.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsController.kt index 263cefd6c..030119427 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsController.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsController.kt @@ -4,31 +4,50 @@ package org.mozilla.fenix.tabstray.browser -import androidx.annotation.VisibleForTesting -import mozilla.components.browser.tabstray.TabsTray +import mozilla.components.browser.state.state.TabSessionState import mozilla.telemetry.glean.private.NoExtras -import org.mozilla.fenix.GleanMetrics.TabsTray as TabsTrayMetrics import org.mozilla.fenix.components.AppStore +import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.appstate.AppAction.UpdateInactiveExpanded -import org.mozilla.fenix.tabstray.TabsTrayStore +import org.mozilla.fenix.tabstray.TrayPagerAdapter.Companion.INACTIVE_TABS_FEATURE_NAME import org.mozilla.fenix.utils.Settings +import org.mozilla.fenix.GleanMetrics.TabsTray as TabsTrayMetrics +/** + * Default behavior for handling all user interactions with the Inactive Tabs feature. + * + * @param appStore [AppStore] used to dispatch any [AppAction]. + * @param settings [Settings] used to update any user preferences. + * @param browserInteractor [BrowserTrayInteractor] used to respond to interactions with specific inactive tabs. + */ class InactiveTabsController( - private val tabsTrayStore: TabsTrayStore, private val appStore: AppStore, - private val tray: TabsTray, - private val settings: Settings + private val settings: Settings, + private val browserInteractor: BrowserTrayInteractor, ) { + + /** + * Opens the given inactive tab. + */ + fun openInactiveTab(tab: TabSessionState) { + TabsTrayMetrics.openInactiveTab.add() + browserInteractor.onTabSelected(tab, INACTIVE_TABS_FEATURE_NAME) + } + + /** + * Closes the given inactive tab. + */ + fun closeInactiveTab(tab: TabSessionState) { + TabsTrayMetrics.closeInactiveTab.add() + browserInteractor.onTabClosed(tab, INACTIVE_TABS_FEATURE_NAME) + } + /** * Updates the inactive card to be expanded to display all the tabs, or collapsed with only * the title showing. */ fun updateCardExpansion(isExpanded: Boolean) { - appStore.dispatch(UpdateInactiveExpanded(isExpanded)).invokeOnCompletion { - // To avoid racing, we read the list of inactive tabs only after we have updated - // the expanded state. - refreshInactiveTabsSection() - } + appStore.dispatch(UpdateInactiveExpanded(isExpanded)) when (isExpanded) { true -> TabsTrayMetrics.inactiveTabsExpanded.record(NoExtras()) @@ -41,7 +60,6 @@ class InactiveTabsController( */ fun close() { markDialogAsShown() - refreshInactiveTabsSection() TabsTrayMetrics.autoCloseDimissed.record(NoExtras()) } @@ -54,7 +72,6 @@ class InactiveTabsController( settings.closeTabsAfterOneWeek = false settings.closeTabsAfterOneDay = false settings.manuallyCloseTabs = false - refreshInactiveTabsSection() TabsTrayMetrics.autoCloseTurnOnClicked.record(NoExtras()) } @@ -64,10 +81,4 @@ class InactiveTabsController( private fun markDialogAsShown() { settings.hasInactiveTabsAutoCloseDialogBeenDismissed = true } - - @VisibleForTesting - internal fun refreshInactiveTabsSection() { - val tabs = tabsTrayStore.state.inactiveTabs - tray.updateTabs(tabs, null, null) - } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsInteractor.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsInteractor.kt index 27bcde07d..30e8794d8 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/InactiveTabsInteractor.kt @@ -4,16 +4,32 @@ package org.mozilla.fenix.tabstray.browser +import mozilla.components.browser.state.state.TabSessionState + /** * Interactor for all things related to inactive tabs in the tabs tray. */ interface InactiveTabsInteractor : InactiveTabsAutoCloseDialogInteractor { /** - * Invoked when the header is tapped on. + * Invoked when the header is clicked. * * @param activated true when the tap should expand the inactive section. */ fun onHeaderClicked(activated: Boolean) + + /** + * Invoked when an inactive tab is clicked. + * + * @param tab [TabSessionState] that was clicked. + */ + fun onTabClicked(tab: TabSessionState) + + /** + * Invoked when an inactive tab is closed. + * + * @param tab [TabSessionState] that was closed. + */ + fun onTabClosed(tab: TabSessionState) } /** @@ -56,4 +72,18 @@ class DefaultInactiveTabsInteractor( override fun onEnabledAutoCloseClicked() { controller.enableAutoClosed() } + + /** + * See [InactiveTabsInteractor.onTabClicked]. + */ + override fun onTabClicked(tab: TabSessionState) { + controller.openInactiveTab(tab) + } + + /** + * See [InactiveTabsInteractor.onTabClosed]. + */ + override fun onTabClosed(tab: TabSessionState) { + controller.closeInactiveTab(tab) + } } 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 cf8cbb352..22da1d2b8 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 @@ -9,9 +9,7 @@ import android.util.AttributeSet import androidx.recyclerview.widget.ConcatAdapter import mozilla.components.browser.tabstray.TabViewHolder 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.tabGroupAdapter import org.mozilla.fenix.tabstray.ext.titleHeaderAdapter @@ -23,10 +21,6 @@ class NormalBrowserTrayList @JvmOverloads constructor( private val concatAdapter by lazy { adapter as ConcatAdapter } - private val inactiveTabsBinding by lazy { - InactiveTabsBinding(tabsTrayStore, concatAdapter.inactiveTabsAdapter) - } - private val normalTabsBinding by lazy { NormalTabsBinding(tabsTrayStore, context.components.core.store, concatAdapter.browserAdapter) } @@ -39,17 +33,6 @@ class NormalBrowserTrayList @JvmOverloads constructor( TabGroupBinding(tabsTrayStore) { concatAdapter.tabGroupAdapter.submitList(it) } } - private val inactiveTabsInteractor by lazy { - DefaultInactiveTabsInteractor( - InactiveTabsController( - tabsTrayStore, - context.components.appStore, - concatAdapter.inactiveTabsAdapter, - context.settings() - ) - ) - } - private val touchHelper by lazy { TabsTouchHelper( interactionDelegate = concatAdapter.browserAdapter.interactor, @@ -64,9 +47,6 @@ class NormalBrowserTrayList @JvmOverloads constructor( override fun onAttachedToWindow() { super.onAttachedToWindow() - concatAdapter.inactiveTabsAdapter.inactiveTabsInteractor = inactiveTabsInteractor - - inactiveTabsBinding.start() normalTabsBinding.start() titleHeaderBinding.start() tabGroupBinding.start() @@ -77,7 +57,6 @@ class NormalBrowserTrayList @JvmOverloads constructor( override fun onDetachedFromWindow() { super.onDetachedFromWindow() - inactiveTabsBinding.stop() normalTabsBinding.stop() titleHeaderBinding.stop() tabGroupBinding.stop() diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSelectors.kt b/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSelectors.kt index f34848104..817913b62 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSelectors.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSelectors.kt @@ -43,11 +43,11 @@ fun BrowserState.getNormalTrayTabs( ): List { val tabGroupsTabIds = getTabGroups()?.flatMap { it.tabIds } ?: emptyList() return normalTabs.run { - if (searchTermTabGroupsAreEnabled && inactiveTabsEnabled) { + if (searchTermTabGroupsAreEnabled && tabGroupsTabIds.isNotEmpty() && inactiveTabsEnabled) { filter { it.isNormalTabActive(maxActiveTime) }.filter { tabGroupsTabIds.contains(it.id) } } else if (inactiveTabsEnabled) { filter { it.isNormalTabActive(maxActiveTime) } - } else if (searchTermTabGroupsAreEnabled) { + } else if (searchTermTabGroupsAreEnabled && tabGroupsTabIds.isNotEmpty()) { filter { it.isNormalTab() }.filter { tabGroupsTabIds.contains(it.id) } } else { this diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSessionState.kt b/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSessionState.kt index 089d10fd5..216ce4955 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSessionState.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSessionState.kt @@ -5,6 +5,7 @@ package org.mozilla.fenix.tabstray.ext import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.browser.toolbar.MAX_URI_LENGTH fun TabSessionState.isActive(maxActiveTime: Long): Boolean { val lastActiveTime = maxOf(lastAccess, createdAt) @@ -53,3 +54,8 @@ internal fun TabSessionState.isNormalTabInactive(maxActiveTime: Long): Boolean { internal fun TabSessionState.isNormalTab(): Boolean { return !content.private } + +/** + * Returns a [String] for displaying a [TabSessionState]'s title or its url when a title is not available. + */ +fun TabSessionState.toDisplayTitle(): String = content.title.ifEmpty { content.url.take(MAX_URI_LENGTH) } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/inactivetabs/InactiveTabs.kt b/app/src/main/java/org/mozilla/fenix/tabstray/inactivetabs/InactiveTabs.kt new file mode 100644 index 000000000..b45365f67 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/inactivetabs/InactiveTabs.kt @@ -0,0 +1,278 @@ +/* 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/. */ + +@file:Suppress("TooManyFunctions") + +package org.mozilla.fenix.tabstray.inactivetabs + +import android.content.res.Configuration +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import mozilla.components.browser.state.state.ContentState +import mozilla.components.browser.state.state.TabSessionState +import org.mozilla.fenix.R +import org.mozilla.fenix.compose.PrimaryText +import org.mozilla.fenix.compose.SecondaryText +import org.mozilla.fenix.compose.button.TextButton +import org.mozilla.fenix.compose.list.ExpandableListHeader +import org.mozilla.fenix.compose.list.FaviconListItem +import org.mozilla.fenix.ext.toShortUrl +import org.mozilla.fenix.tabstray.ext.toDisplayTitle +import org.mozilla.fenix.theme.FirefoxTheme +import org.mozilla.fenix.theme.Theme + +private val ROUNDED_CORNER_SHAPE = RoundedCornerShape(8.dp) + +/** + * Top-level list for displaying an expandable section of Inactive Tabs. + * + * @param inactiveTabs List of [TabSessionState] to display. + * @param expanded Whether to show the inactive tabs section expanded or collapsed. + * @param showAutoCloseDialog Whether to show the auto close inactive tabs dialog. + * @param onHeaderClick Called when the user clicks on the inactive tabs section header. + * @param onDeleteAllButtonClick Called when the user clicks on the delete all inactive tabs button. + * @param onAutoCloseDismissClick Called when the user clicks on the auto close dialog's dismiss button. + * @param onEnableAutoCloseClick Called when the user clicks on the auto close dialog's enable button. + * @param onTabClick Called when the user clicks on a specific inactive tab. + * @param onTabCloseClick Called when the user clicks on a specific inactive tab's close button. + */ +@Composable +@Suppress("LongParameterList") +fun InactiveTabsList( + inactiveTabs: List, + expanded: Boolean, + showAutoCloseDialog: Boolean, + onHeaderClick: () -> Unit, + onDeleteAllButtonClick: () -> Unit, + onAutoCloseDismissClick: () -> Unit, + onEnableAutoCloseClick: () -> Unit, + onTabClick: (TabSessionState) -> Unit, + onTabCloseClick: (TabSessionState) -> Unit, +) { + Card( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + shape = ROUNDED_CORNER_SHAPE, + backgroundColor = FirefoxTheme.colors.layer2, + border = BorderStroke( + width = 1.dp, + color = FirefoxTheme.colors.borderPrimary, + ), + ) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + InactiveTabsHeader( + expanded = expanded, + onClick = onHeaderClick, + onDeleteAllClick = onDeleteAllButtonClick, + ) + + if (expanded) { + if (showAutoCloseDialog) { + InactiveTabsAutoClosePrompt( + onDismissClick = onAutoCloseDismissClick, + onEnableAutoCloseClick = onEnableAutoCloseClick, + ) + } + + Column { + inactiveTabs.forEach { tab -> + val tabUrl = tab.content.url.toShortUrl() + + FaviconListItem( + label = tab.toDisplayTitle(), + description = tabUrl, + onClick = { onTabClick(tab) }, + url = tabUrl, + iconPainter = painterResource(R.drawable.mozac_ic_close), + iconDescription = stringResource(R.string.content_description_close_button), + onIconClick = { onTabCloseClick(tab) }, + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + } + } + } +} + +/** + * Collapsible header for the Inactive Tabs section. + * + * @param expanded Whether the section is expanded. + * @param onClick Called when the user clicks on the header. + * @param onDeleteAllClick Called when the user clicks on the delete all button. + */ +@Composable +private fun InactiveTabsHeader( + expanded: Boolean, + onClick: () -> Unit, + onDeleteAllClick: () -> Unit, +) { + ExpandableListHeader( + headerText = stringResource(R.string.inactive_tabs_title), + expanded = expanded, + expandActionContentDescription = stringResource(R.string.inactive_tabs_expand_content_description), + collapseActionContentDescription = stringResource(R.string.inactive_tabs_collapse_content_description), + onClick = onClick, + ) { + IconButton( + onClick = onDeleteAllClick, + modifier = Modifier.padding(horizontal = 4.dp), + ) { + Icon( + painter = painterResource(R.drawable.ic_delete), + contentDescription = stringResource(R.string.inactive_tabs_delete_all), + tint = FirefoxTheme.colors.iconPrimary, + ) + } + } +} + +/** + * Inactive Tabs auto close dialog. + * + * @param onDismissClick Called when the user clicks on the auto close dialog's dismiss button. + * @param onEnableAutoCloseClick Called when the user clicks on the auto close dialog's enable button. + */ +@Composable +private fun InactiveTabsAutoClosePrompt( + onDismissClick: () -> Unit, + onEnableAutoCloseClick: () -> Unit, +) { + Card( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + shape = ROUNDED_CORNER_SHAPE, + backgroundColor = FirefoxTheme.colors.layer2, + border = BorderStroke( + width = 1.dp, + color = FirefoxTheme.colors.borderPrimary, + ), + ) { + Column( + modifier = Modifier.padding(horizontal = 12.dp), + horizontalAlignment = Alignment.End, + ) { + Spacer(modifier = Modifier.height(12.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + PrimaryText( + text = stringResource(R.string.tab_tray_inactive_auto_close_title), + modifier = Modifier.weight(1f), + fontSize = 14.sp, + fontFamily = FontFamily(Font(R.font.metropolis_semibold)), + ) + + IconButton( + onClick = onDismissClick, + modifier = Modifier.size(20.dp) + ) { + Icon( + painter = painterResource(R.drawable.mozac_ic_close_20), + contentDescription = + stringResource(R.string.tab_tray_inactive_auto_close_button_content_description), + tint = FirefoxTheme.colors.iconPrimary, + ) + } + } + + SecondaryText( + text = stringResource( + R.string.tab_tray_inactive_auto_close_body_2, + stringResource(R.string.app_name) + ), + modifier = Modifier.fillMaxWidth(), + fontSize = 14.sp, + ) + + TextButton( + text = stringResource(R.string.tab_tray_inactive_turn_on_auto_close_button_2), + onClick = onEnableAutoCloseClick, + ) + } + } +} + +@Composable +@Preview(name = "Auto close dialog dark", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(name = "Auto close dialog light", uiMode = Configuration.UI_MODE_NIGHT_NO) +private fun InactiveTabsAutoClosePromptPreview() { + FirefoxTheme(theme = Theme.getTheme(isPrivate = false)) { + Box(Modifier.background(FirefoxTheme.colors.layer1)) { + InactiveTabsAutoClosePrompt( + onDismissClick = {}, + onEnableAutoCloseClick = {}, + ) + } + } +} + +@Composable +@Preview(name = "Full preview dark", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(name = "Full preview light", uiMode = Configuration.UI_MODE_NIGHT_NO) +private fun InactiveTabsListPreview() { + var expanded by remember { mutableStateOf(true) } + var showAutoClosePrompt by remember { mutableStateOf(true) } + + FirefoxTheme(theme = Theme.getTheme(isPrivate = false)) { + Box(Modifier.background(FirefoxTheme.colors.layer1)) { + InactiveTabsList( + inactiveTabs = generateFakeInactiveTabsList(), + expanded = expanded, + showAutoCloseDialog = showAutoClosePrompt, + onHeaderClick = { expanded = !expanded }, + onDeleteAllButtonClick = {}, + onAutoCloseDismissClick = { showAutoClosePrompt = !showAutoClosePrompt }, + onEnableAutoCloseClick = { showAutoClosePrompt = !showAutoClosePrompt }, + onTabClick = {}, + onTabCloseClick = {}, + ) + } + } +} + +private fun generateFakeInactiveTabsList(): List = + listOf( + TabSessionState( + id = "tabId", + content = ContentState( + url = "www.mozilla.com", + ) + ), + TabSessionState( + id = "tabId", + content = ContentState( + url = "www.google.com", + ) + ), + ) diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/AbstractBrowserPageViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/AbstractBrowserPageViewHolder.kt index e3a5db8a0..8593f0183 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/AbstractBrowserPageViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/AbstractBrowserPageViewHolder.kt @@ -5,10 +5,9 @@ package org.mozilla.fenix.tabstray.viewholders import android.view.View -import android.view.View.GONE -import android.view.View.VISIBLE import android.widget.TextView import androidx.annotation.CallSuper +import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import org.mozilla.fenix.R import org.mozilla.fenix.tabstray.TabsTrayInteractor @@ -72,11 +71,11 @@ abstract class AbstractBrowserPageViewHolder( adapterRef?.let { adapter -> adapterObserver = object : RecyclerView.AdapterDataObserver() { override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { - updateTrayVisibility(adapter.itemCount) + updateTrayVisibility(showTrayList(adapter)) } override fun onItemRangeRemoved(positionstart: Int, itemcount: Int) { - updateTrayVisibility(adapter.itemCount) + updateTrayVisibility(showTrayList(adapter)) } } adapterObserver?.let { @@ -97,14 +96,18 @@ abstract class AbstractBrowserPageViewHolder( adapterObserver = null } } + /** + * A way for an implementor of [AbstractBrowserPageViewHolder] to define their own behavior of + * when to show/hide the tray list and empty list UI. + */ + open fun showTrayList(adapter: RecyclerView.Adapter): Boolean = + adapter.itemCount > 0 - private fun updateTrayVisibility(size: Int) { - if (size == 0) { - trayList.visibility = GONE - emptyList.visibility = VISIBLE - } else { - trayList.visibility = VISIBLE - emptyList.visibility = GONE - } + /** + * Helper function used to toggle the visibility of the tabs tray lists and the empty list message. + */ + fun updateTrayVisibility(showTrayList: Boolean) { + trayList.isVisible = showTrayList + emptyList.isVisible = !showTrayList } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/NormalBrowserPageViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/NormalBrowserPageViewHolder.kt index 28d798df3..222ae1b82 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/NormalBrowserPageViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/NormalBrowserPageViewHolder.kt @@ -6,44 +6,51 @@ package org.mozilla.fenix.tabstray.viewholders import android.content.Context import android.view.View +import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.flow.map import mozilla.components.browser.state.selector.selectedNormalTab import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.lib.state.ext.flowScoped +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.R -import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.AppStore +import org.mozilla.fenix.components.appstate.AppAction +import org.mozilla.fenix.ext.maxActiveTime +import org.mozilla.fenix.ext.potentialInactiveTabs import org.mozilla.fenix.ext.settings import org.mozilla.fenix.selection.SelectionHolder import org.mozilla.fenix.tabstray.TabsTrayAction import org.mozilla.fenix.tabstray.TabsTrayInteractor import org.mozilla.fenix.tabstray.TabsTrayStore import org.mozilla.fenix.tabstray.browser.containsTabId -import org.mozilla.fenix.ext.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.ext.potentialInactiveTabs -import org.mozilla.fenix.tabstray.ext.titleHeaderAdapter import org.mozilla.fenix.tabstray.ext.inactiveTabsAdapter import org.mozilla.fenix.tabstray.ext.isNormalTabActiveWithSearchTerm import org.mozilla.fenix.tabstray.ext.isNormalTabInactive import org.mozilla.fenix.tabstray.ext.observeFirstInsert import org.mozilla.fenix.tabstray.ext.tabGroupAdapter +import org.mozilla.fenix.tabstray.ext.titleHeaderAdapter /** * View holder for the normal tabs tray list. */ class NormalBrowserPageViewHolder( containerView: View, + private val lifecycleOwner: LifecycleOwner, private val tabsTrayStore: TabsTrayStore, private val browserStore: BrowserStore, private val appStore: AppStore, interactor: TabsTrayInteractor, ) : AbstractBrowserPageViewHolder(containerView, tabsTrayStore, interactor), SelectionHolder { + private var inactiveTabsSize = 0 + /** * Holds the list of selected tabs. * @@ -66,6 +73,8 @@ class NormalBrowserPageViewHolder( browserAdapter.selectionHolder = this tabGroupAdapter.selectionHolder = this + observeTabsTrayInactiveTabsState(adapter) + super.bind(adapter, manager) } @@ -98,11 +107,9 @@ class NormalBrowserPageViewHolder( appStore.dispatch(AppAction.UpdateInactiveExpanded(true)) inactiveTabAdapter.observeFirstInsert { - inactiveTabsList.forEachIndexed { tabIndex, item -> + inactiveTabsList.forEach { item -> if (item.id == selectedTab.id) { - // Inactive Tabs are first + inactive header item. - val indexToScrollTo = tabIndex + 1 - layoutManager.scrollToPosition(indexToScrollTo) + containerView.post { layoutManager.scrollToPosition(0) } return@observeFirstInsert } @@ -132,7 +139,7 @@ class NormalBrowserPageViewHolder( // Index is based on tabs above (inactive) with our calculated index. val indexToScrollTo = inactiveTabAdapter.itemCount + groupIndex - layoutManager.scrollToPosition(indexToScrollTo) + containerView.post { layoutManager.scrollToPosition(indexToScrollTo) } if (focusGroupTabId != null) { tabsTrayStore.dispatch(TabsTrayAction.ConsumeFocusGroupTabId) @@ -158,7 +165,7 @@ class NormalBrowserPageViewHolder( tabGroupAdapter.itemCount + headerAdapter.itemCount + tabIndex - layoutManager.scrollToPosition(indexToScrollTo) + containerView.post { layoutManager.scrollToPosition(indexToScrollTo) } return@observeFirstInsert } @@ -167,6 +174,24 @@ class NormalBrowserPageViewHolder( } } + // Temporary hack until https://github.com/mozilla-mobile/fenix/issues/21901 where the + // logic that shows/hides the "Your open tabs will be shown here." message will no longer be derived + // from adapters, view holders, and item counts. + override fun showTrayList(adapter: RecyclerView.Adapter): Boolean { + return inactiveTabsSize > 0 || adapter.itemCount > 1 // InactiveTabsAdapter will always return 1 + } + + private fun observeTabsTrayInactiveTabsState(adapter: RecyclerView.Adapter) { + tabsTrayStore.flowScoped(lifecycleOwner) { flow -> + flow.map { state -> state.inactiveTabs } + .ifChanged() + .collect { inactiveTabs -> + inactiveTabsSize = inactiveTabs.size + updateTrayVisibility(showTrayList(adapter)) + } + } + } + private fun setupLayoutManager( context: Context, concatAdapter: ConcatAdapter diff --git a/app/src/main/res/drawable/card_list_row_background.xml b/app/src/main/res/drawable/card_list_row_background.xml deleted file mode 100644 index ded6ed654..000000000 --- a/app/src/main/res/drawable/card_list_row_background.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - 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 deleted file mode 100644 index d01c995ba..000000000 --- a/app/src/main/res/drawable/inactive_tab_auto_close_border_background.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/rounded_bottom_corners.xml b/app/src/main/res/drawable/rounded_bottom_corners.xml deleted file mode 100644 index 5aa8fd94a..000000000 --- a/app/src/main/res/drawable/rounded_bottom_corners.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/rounded_top_corners.xml b/app/src/main/res/drawable/rounded_top_corners.xml deleted file mode 100644 index 6957002b4..000000000 --- a/app/src/main/res/drawable/rounded_top_corners.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/inactive_footer_item.xml b/app/src/main/res/layout/inactive_footer_item.xml deleted file mode 100644 index 20fc5532f..000000000 --- a/app/src/main/res/layout/inactive_footer_item.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/inactive_header_item.xml b/app/src/main/res/layout/inactive_header_item.xml deleted file mode 100644 index 19e0e4297..000000000 --- a/app/src/main/res/layout/inactive_header_item.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/inactive_tab_list_item.xml b/app/src/main/res/layout/inactive_tab_list_item.xml deleted file mode 100644 index 059d66680..000000000 --- a/app/src/main/res/layout/inactive_tab_list_item.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/inactive_tabs_auto_close.xml b/app/src/main/res/layout/inactive_tabs_auto_close.xml deleted file mode 100644 index 5ab1de153..000000000 --- a/app/src/main/res/layout/inactive_tabs_auto_close.xml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 48ead51b2..2845e3507 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -700,7 +700,7 @@ Save tabs to collection - Tab menu + Tab menu Delete collection @@ -1739,6 +1739,10 @@ Inactive tabs Close all inactive tabs + + Expand inactive tabs + + Collapse inactive tabs diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayFragmentTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayFragmentTest.kt index cda5e9360..039f7ce43 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayFragmentTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayFragmentTest.kt @@ -213,6 +213,7 @@ class TabsTrayFragmentTest { @Test fun `WHEN setupPager is called THEN it sets the tray adapter and disables user initiated scrolling`() { val store: TabsTrayStore = mockk() + val lifecycleOwner = mockk(relaxed = true) val trayInteractor: TabsTrayInteractor = mockk() val browserInteractor: BrowserTrayInteractor = mockk() val navigationInteractor: NavigationInteractor = mockk() @@ -220,13 +221,19 @@ class TabsTrayFragmentTest { every { context.components.core.store } returns browserStore fragment.setupPager( - context, store, trayInteractor, browserInteractor, navigationInteractor + context = context, + lifecycleOwner = lifecycleOwner, + store = store, + trayInteractor = trayInteractor, + browserInteractor = browserInteractor, + navigationInteractor = navigationInteractor, ) val adapter = (tabsTrayBinding.tabsTray.adapter as TrayPagerAdapter) assertSame(context, adapter.context) + assertSame(lifecycleOwner, adapter.lifecycleOwner) assertSame(store, adapter.tabsTrayStore) - assertSame(trayInteractor, adapter.interactor) + assertSame(trayInteractor, adapter.tabsTrayInteractor) assertSame(browserInteractor, adapter.browserInteractor) assertSame(navigationInteractor, adapter.navInteractor) assertSame(browserStore, adapter.browserStore) diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/DefaultInactiveTabsInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/DefaultInactiveTabsInteractorTest.kt index f3f2ddc3d..7149f83f4 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/browser/DefaultInactiveTabsInteractorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/DefaultInactiveTabsInteractorTest.kt @@ -6,6 +6,8 @@ package org.mozilla.fenix.tabstray.browser import io.mockk.mockk import io.mockk.verify +import mozilla.components.browser.state.state.ContentState +import mozilla.components.browser.state.state.TabSessionState import org.junit.Test class DefaultInactiveTabsInteractorTest { @@ -39,4 +41,36 @@ class DefaultInactiveTabsInteractorTest { verify { controller.enableAutoClosed() } } + + @Test + fun `WHEN an inactive tab is clicked THEN open the tab`() { + val controller: InactiveTabsController = mockk(relaxed = true) + val interactor = DefaultInactiveTabsInteractor(controller) + val tab = TabSessionState( + id = "tabId", + content = ContentState( + url = "www.mozilla.com", + ) + ) + + interactor.onTabClicked(tab) + + verify { controller.openInactiveTab(tab) } + } + + @Test + fun `WHEN an inactive tab is clicked to be closed THEN close the tab`() { + val controller: InactiveTabsController = mockk(relaxed = true) + val interactor = DefaultInactiveTabsInteractor(controller) + val tab = TabSessionState( + id = "tabId", + content = ContentState( + url = "www.mozilla.com", + ) + ) + + interactor.onTabClosed(tab) + + verify { controller.closeInactiveTab(tab) } + } } diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolderTest.kt deleted file mode 100644 index 6999291c5..000000000 --- a/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabViewHolderTest.kt +++ /dev/null @@ -1,39 +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.view.LayoutInflater -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import mozilla.components.support.test.robolectric.testContext -import org.junit.Assert.assertEquals -import org.junit.Test -import org.junit.runner.RunWith -import org.mozilla.fenix.components.AppStore -import org.mozilla.fenix.ext.components -import org.mozilla.fenix.helpers.FenixRobolectricTestRunner -import org.mozilla.fenix.tabstray.TabsTrayInteractor -import org.mozilla.fenix.tabstray.browser.InactiveTabViewHolder.HeaderHolder - -@RunWith(FenixRobolectricTestRunner::class) -class InactiveTabViewHolderTest { - @Test - fun `HeaderHolder - WHEN clicked THEN notify the interactor`() { - every { testContext.components.appStore } returns AppStore() - val view = LayoutInflater.from(testContext).inflate(HeaderHolder.LAYOUT_ID, null) - val interactor: InactiveTabsInteractor = mockk(relaxed = true) - val tabsTrayInteractor: TabsTrayInteractor = mockk(relaxed = true) - val viewHolder = HeaderHolder(view, interactor, tabsTrayInteractor) - - val initialActivatedState = view.isActivated - - viewHolder.itemView.performClick() - - verify { interactor.onHeaderClicked(any()) } - - assertEquals(!initialActivatedState, view.isActivated) - } -} diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabsBindingTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabsBindingTest.kt deleted file mode 100644 index ecea67625..000000000 --- a/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabsBindingTest.kt +++ /dev/null @@ -1,58 +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 io.mockk.mockk -import io.mockk.verify -import mozilla.components.browser.state.state.createTab -import mozilla.components.browser.tabstray.TabsTray -import mozilla.components.support.test.ext.joinBlocking -import mozilla.components.support.test.rule.MainCoroutineRule -import org.junit.After -import org.junit.Assert.assertTrue -import org.junit.Rule -import org.junit.Test -import org.mozilla.fenix.tabstray.TabsTrayAction -import org.mozilla.fenix.tabstray.TabsTrayStore - -class InactiveTabsBindingTest { - val store = TabsTrayStore() - val tray: TabsTray = mockk(relaxed = true) - val binding = InactiveTabsBinding(store, tray) - - @get:Rule - val coroutinesTestRule = MainCoroutineRule() - - @After - fun teardown() { - binding.stop() - } - - @Test - fun `WHEN the store is updated THEN notify the tabs tray`() { - assertTrue(store.state.inactiveTabs.isEmpty()) - - store.dispatch(TabsTrayAction.UpdateInactiveTabs(listOf(createTab("https://mozilla.org")))).joinBlocking() - - binding.start() - - assertTrue(store.state.inactiveTabs.isNotEmpty()) - - verify { tray.updateTabs(any(), any(), any()) } - } - - @Test - fun `WHEN non-inactive tabs are updated THEN do not notify the tabs tray`() { - assertTrue(store.state.inactiveTabs.isEmpty()) - - store.dispatch(TabsTrayAction.UpdatePrivateTabs(listOf(createTab("https://mozilla.org")))).joinBlocking() - - binding.start() - - assertTrue(store.state.inactiveTabs.isEmpty()) - - verify { tray.updateTabs(emptyList(), null, null) } - } -} diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabsControllerTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabsControllerTest.kt index 570ad2da4..bc6466daa 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabsControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/InactiveTabsControllerTest.kt @@ -4,73 +4,37 @@ 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.slot import io.mockk.spyk import io.mockk.verify +import mozilla.components.browser.state.state.ContentState import mozilla.components.browser.state.state.TabSessionState -import mozilla.components.browser.state.store.BrowserStore -import mozilla.components.browser.tabstray.TabsTray import mozilla.components.service.glean.testing.GleanTestRule -import org.mozilla.fenix.GleanMetrics.TabsTray as TabsTrayMetrics -import mozilla.components.support.test.libstate.ext.waitUntilIdle import mozilla.components.support.test.robolectric.testContext -import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Rule -import mozilla.components.browser.state.state.createTab as createTabState import org.junit.Test import org.junit.runner.RunWith import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.helpers.FenixRobolectricTestRunner -import org.mozilla.fenix.tabstray.TabsTrayState -import org.mozilla.fenix.tabstray.TabsTrayStore +import org.mozilla.fenix.tabstray.TrayPagerAdapter import org.mozilla.fenix.utils.Settings +import org.mozilla.fenix.GleanMetrics.TabsTray as TabsTrayMetrics -@RunWith(FenixRobolectricTestRunner::class) // for gleanTestRule +@RunWith(FenixRobolectricTestRunner::class) class InactiveTabsControllerTest { private val settings: Settings = mockk(relaxed = true) + private val browserInteractor: BrowserTrayInteractor = mockk(relaxed = true) private val appStore = AppStore() @get:Rule val gleanTestRule = GleanTestRule(testContext) @Test - fun `WHEN expanded THEN notify filtered card`() { - val store = TabsTrayStore( - TabsTrayState( - inactiveTabs = listOf( - createTabState("https://mozilla.org", id = "1"), - createTabState("https://firefox.com", id = "2") - ) - ) - ) - val tray: TabsTray = mockk(relaxed = true) - val tabsSlot = slot>() - val controller = - InactiveTabsController(store, appStore, tray, settings) - - controller.updateCardExpansion(true) - - appStore.waitUntilIdle() - - verify { tray.updateTabs(capture(tabsSlot), null, any()) } - - assertEquals(2, tabsSlot.captured.size) - assertEquals("1", tabsSlot.captured.first().id) - } - - @Test - fun `WHEN expanded THEN track telemetry event`() { - val store = TabsTrayStore() - val controller = InactiveTabsController( - store, appStore, mockk(relaxed = true), settings - ) + fun `WHEN the inactive tabs section is expanded THEN the expanded telemetry event should be report`() { + val controller = InactiveTabsController(appStore, settings, browserInteractor) assertFalse(TabsTrayMetrics.inactiveTabsExpanded.testHasValue()) assertFalse(TabsTrayMetrics.inactiveTabsCollapsed.testHasValue()) @@ -82,11 +46,8 @@ class InactiveTabsControllerTest { } @Test - fun `WHEN collapsed THEN track telemetry event`() { - val store = TabsTrayStore() - val controller = InactiveTabsController( - store, appStore, mockk(relaxed = true), settings - ) + fun `WHEN the inactive tabs section is collapsed THEN the collapsed telemetry event should be report`() { + val controller = InactiveTabsController(appStore, settings, browserInteractor) assertFalse(TabsTrayMetrics.inactiveTabsExpanded.testHasValue()) assertFalse(TabsTrayMetrics.inactiveTabsCollapsed.testHasValue()) @@ -98,15 +59,8 @@ class InactiveTabsControllerTest { } @Test - fun `WHEN close THEN update settings and refresh`() { - val store = TabsTrayStore() - val controller = spyk( - InactiveTabsController( - store, appStore, mockk(relaxed = true), settings - ) - ) - - every { controller.refreshInactiveTabsSection() } just Runs + fun `WHEN the inactive tabs auto-close feature prompt is dismissed THEN update settings and report the telemetry event`() { + val controller = spyk(InactiveTabsController(appStore, settings, browserInteractor)) assertFalse(TabsTrayMetrics.autoCloseDimissed.testHasValue()) @@ -114,29 +68,59 @@ class InactiveTabsControllerTest { assertTrue(TabsTrayMetrics.autoCloseDimissed.testHasValue()) verify { settings.hasInactiveTabsAutoCloseDialogBeenDismissed = true } - verify { controller.refreshInactiveTabsSection() } } @Test - fun `WHEN enableAutoClosed THEN update closeTabsAfterOneMonth settings and refresh`() { - val filter: (TabSessionState) -> Boolean = { !it.content.private } - val store = BrowserStore() - val tray: TabsTray = mockk(relaxed = true) - val controller = - spyk(InactiveTabsAutoCloseDialogController(store, settings, filter, tray)) - - every { controller.refreshInactiveTabsSection() } just Runs + fun `WHEN the inactive tabs auto-close feature prompt is accepted THEN update settings and report the telemetry event`() { + val controller = spyk(InactiveTabsController(appStore, settings, browserInteractor)) assertFalse(TabsTrayMetrics.autoCloseTurnOnClicked.testHasValue()) controller.enableAutoClosed() assertTrue(TabsTrayMetrics.autoCloseTurnOnClicked.testHasValue()) - verify { settings.closeTabsAfterOneMonth = true } verify { settings.closeTabsAfterOneWeek = false } verify { settings.closeTabsAfterOneDay = false } verify { settings.manuallyCloseTabs = false } - verify { controller.refreshInactiveTabsSection() } + verify { settings.hasInactiveTabsAutoCloseDialogBeenDismissed = true } + } + + @Test + fun `WHEN an inactive tab is selected THEN the open the tab and report the telemetry event`() { + val controller = InactiveTabsController(appStore, settings, browserInteractor) + val tab = TabSessionState( + id = "tabId", + content = ContentState( + url = "www.mozilla.com", + ) + ) + + assertFalse(TabsTrayMetrics.openInactiveTab.testHasValue()) + + controller.openInactiveTab(tab) + + verify { browserInteractor.onTabSelected(tab, TrayPagerAdapter.INACTIVE_TABS_FEATURE_NAME) } + + assertTrue(TabsTrayMetrics.openInactiveTab.testHasValue()) + } + + @Test + fun `WHEN an inactive tab is closed THEN the close the tab and report the telemetry event`() { + val controller = InactiveTabsController(appStore, settings, browserInteractor) + val tab = TabSessionState( + id = "tabId", + content = ContentState( + url = "www.mozilla.com", + ) + ) + + assertFalse(TabsTrayMetrics.openInactiveTab.testHasValue()) + + controller.openInactiveTab(tab) + + verify { browserInteractor.onTabSelected(tab, TrayPagerAdapter.INACTIVE_TABS_FEATURE_NAME) } + + assertTrue(TabsTrayMetrics.openInactiveTab.testHasValue()) } }