Skip to content

Commit

Permalink
Issue mozilla-mobile#20349: Add inactive tab grouping to tabs tray
Browse files Browse the repository at this point in the history
  • Loading branch information
jonalmeida committed Jul 29, 2021
1 parent 7e56440 commit 9c1eb5a
Show file tree
Hide file tree
Showing 31 changed files with 664 additions and 162 deletions.
5 changes: 5 additions & 0 deletions app/src/main/java/org/mozilla/fenix/FeatureFlags.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,9 @@ object FeatureFlags {
* Enables the recently saved bookmarks feature in the home screen.
*/
val recentBookmarksFeature = Config.channel.isNightlyOrDebug

/**
* Identifies and separates the tabs list with a secondary section containing least used tabs.
*/
val inactiveTabs = Config.channel.isDebug
}
26 changes: 26 additions & 0 deletions app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ package org.mozilla.fenix.tabstray
import androidx.annotation.VisibleForTesting
import androidx.navigation.NavController
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.state.action.LastAccessAction
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.base.profiler.Profiler
import mozilla.components.concept.tabstray.Tab
Expand All @@ -19,6 +21,9 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.home.HomeFragment
import java.util.concurrent.TimeUnit

const val DEFAULT_INACTIVE_TEST_DAYS = 5L

interface TabsTrayController {

Expand Down Expand Up @@ -53,6 +58,16 @@ interface TabsTrayController {
* @param tabs List of [Tab]s (sessions) to be removed.
*/
fun handleMultipleTabsDeletion(tabs: Collection<Tab>)

/**
* Set the list of [tabs] into the inactive state.
*
* @param tabs List of [Tab]s to be removed.
*/
fun forceTabsAsInactive(
tabs: Collection<Tab>,
numOfDays: Long = DEFAULT_INACTIVE_TEST_DAYS
)
}

class DefaultTabsTrayController(
Expand Down Expand Up @@ -143,6 +158,17 @@ class DefaultTabsTrayController(
showUndoSnackbarForTab(isPrivate)
}

/**
* Marks all the [tabs] with the [TabSessionState.lastAccess] to 5 days; enough time to
* have a tab considered as inactive.
*/
override fun forceTabsAsInactive(tabs: Collection<Tab>, numOfDays: Long) {
tabs.forEach { tab ->
val daysSince = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(numOfDays)
browserStore.dispatch(LastAccessAction.UpdateLastAccessAction(tab.id, daysSince))
}
}

@VisibleForTesting
internal fun sendNewTabEvent(isPrivateModeSelected: Boolean) {
val eventToSend = if (isPrivateModeSelected) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ interface TabsTrayInteractor {
* Invoked when [Tab]s need to be deleted.
*/
fun onDeleteTabs(tabs: Collection<Tab>)

/**
* Called when clicking the debug menu option for inactive tabs.
*/
fun onInactiveDebugClicked(tabs: Collection<Tab>)
}

/**
Expand All @@ -54,4 +59,8 @@ class DefaultTabsTrayInteractor(
override fun onDeleteTabs(tabs: Collection<Tab>) {
controller.handleMultipleTabsDeletion(tabs)
}

override fun onInactiveDebugClicked(tabs: Collection<Tab>) {
controller.forceTabsAsInactive(tabs)
}
}
11 changes: 9 additions & 2 deletions app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
Expand All @@ -16,6 +17,7 @@ import mozilla.components.browser.state.store.BrowserStore
import org.mozilla.fenix.sync.SyncedTabsAdapter
import org.mozilla.fenix.tabstray.browser.BrowserTabsAdapter
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
import org.mozilla.fenix.tabstray.browser.InactiveTabsAdapter
import org.mozilla.fenix.tabstray.syncedtabs.TabClickDelegate
import org.mozilla.fenix.tabstray.viewholders.AbstractPageViewHolder
import org.mozilla.fenix.tabstray.viewholders.NormalBrowserPageViewHolder
Expand All @@ -31,7 +33,12 @@ class TrayPagerAdapter(
@VisibleForTesting internal val browserStore: BrowserStore
) : RecyclerView.Adapter<AbstractPageViewHolder>() {

private val normalAdapter by lazy { BrowserTabsAdapter(context, browserInteractor, store) }
private val normalAdapter by lazy {
ConcatAdapter(
BrowserTabsAdapter(context, browserInteractor, store),
InactiveTabsAdapter(context, browserInteractor)
)
}
private val privateAdapter by lazy { BrowserTabsAdapter(context, browserInteractor, store) }
private val syncedTabsAdapter by lazy { SyncedTabsAdapter(TabClickDelegate(navInteractor)) }

Expand Down Expand Up @@ -74,7 +81,7 @@ class TrayPagerAdapter(
POSITION_SYNCED_TABS -> syncedTabsAdapter
else -> throw IllegalStateException("View type does not exist.")
}
viewHolder.bind(adapter, browserInteractor.getLayoutManagerForPosition(context, position))
viewHolder.bind(adapter)
}

override fun getItemViewType(position: Int): Int {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,82 +12,56 @@ import mozilla.components.feature.tabs.tabstray.TabsFeature
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.tabstray.TabsTrayInteractor
import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.tabstray.ext.filterFromConfig

class BrowserTrayList @JvmOverloads constructor(
abstract class BrowserTrayList @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {

/**
* The browser tab types we would want to show.
*/
enum class BrowserTabType { NORMAL, PRIVATE }

lateinit var browserTabType: BrowserTabType
lateinit var interactor: TabsTrayInteractor
lateinit var tabsTrayStore: TabsTrayStore

private val tabsFeature by lazy {
// NB: The use cases here are duplicated because there isn't a nicer
// way to share them without a better dependency injection solution.
val selectTabUseCase = SelectTabUseCaseWrapper(
context.components.analytics.metrics,
context.components.useCases.tabsUseCases.selectTab
) {
interactor.onBrowserTabSelected()
}

val removeTabUseCase = RemoveTabUseCaseWrapper(
context.components.analytics.metrics
) { sessionId ->
interactor.onDeleteTab(sessionId)
}

TabsFeature(
adapter as TabsAdapter,
context.components.core.store,
selectTabUseCase,
removeTabUseCase,
{ it.filterFromConfig(browserTabType) },
{ }
)
/**
* A [TabsFeature] is required for each browser list to ensure one always exists for displaying
* tabs.
*/
abstract val tabsFeature: TabsFeature

// NB: The use cases here are duplicated because there isn't a nicer
// way to share them without a better dependency injection solution.
protected val selectTabUseCase = SelectTabUseCaseWrapper(
context.components.analytics.metrics,
context.components.useCases.tabsUseCases.selectTab
) {
interactor.onBrowserTabSelected()
}

private val swipeToDelete by lazy {
SwipeToDeleteBinding(tabsTrayStore)
protected val removeTabUseCase = RemoveTabUseCaseWrapper(
context.components.analytics.metrics
) { sessionId ->
interactor.onDeleteTab(sessionId)
}

private val touchHelper by lazy {
TabsTouchHelper(
observable = adapter as TabsAdapter,
onViewHolderTouched = { swipeToDelete.isSwipeable },
onViewHolderDraw = { context.components.settings.gridTabView.not() }
)
protected val swipeToDelete by lazy {
SwipeToDeleteBinding(tabsTrayStore)
}

override fun onAttachedToWindow() {
super.onAttachedToWindow()

tabsFeature.start()
swipeToDelete.start()

adapter?.onAttachedToRecyclerView(this)

touchHelper.attachToRecyclerView(this)
}

@VisibleForTesting
public override fun onDetachedFromWindow() {
super.onDetachedFromWindow()

tabsFeature.stop()
swipeToDelete.stop()

// Notify the adapter that it is released from the view preemptively.
adapter?.onDetachedFromRecyclerView(this)

touchHelper.attachToRecyclerView(null)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* 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.recyclerview.widget.ConcatAdapter

val ConcatAdapter.browserAdapter
get() = adapters.find { it is BrowserTabsAdapter } as BrowserTabsAdapter

val ConcatAdapter.inactiveTabsAdapter
get() = adapters.find { it is InactiveTabsAdapter } as InactiveTabsAdapter
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.fenix.tabstray.browser

import android.view.View
import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.browser.toolbar.MAX_URI_LENGTH
import mozilla.components.concept.tabstray.Tab
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.InactiveFooterItemBinding
import org.mozilla.fenix.databinding.InactiveTabListItemBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.loadIntoView
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.tabstray.browser.AutoCloseInterval.Manual
import org.mozilla.fenix.tabstray.browser.AutoCloseInterval.OneDay
import org.mozilla.fenix.tabstray.browser.AutoCloseInterval.OneMonth
import org.mozilla.fenix.tabstray.browser.AutoCloseInterval.OneWeek

sealed class InactiveTabViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
class HeaderHolder(itemView: View) : InactiveTabViewHolder(itemView) {
companion object {
const val LAYOUT_ID = R.layout.inactive_header_item
}
}

class TabViewHolder(
itemView: View,
private val browserTrayInteractor: BrowserTrayInteractor
) : InactiveTabViewHolder(itemView) {

private val binding = InactiveTabListItemBinding.bind(itemView)

fun bind(tab: Tab) {
val components = itemView.context.components
val makePrettyUrl: (String) -> String = {
it.toShortUrl(components.publicSuffixList).take(MAX_URI_LENGTH)
}

itemView.setOnClickListener {
browserTrayInteractor.open(tab)
}

binding.siteListItem.apply {
components.core.icons.loadIntoView(iconView, tab.url)
setText(tab.title, makePrettyUrl(tab.url))
setSecondaryButton(
R.drawable.mozac_ic_close,
R.string.content_description_close_button
) {
browserTrayInteractor.close(tab)
}
}
}

companion object {
const val LAYOUT_ID = R.layout.inactive_tab_list_item
}
}

class FooterHolder(itemView: View) : InactiveTabViewHolder(itemView) {

val binding = InactiveFooterItemBinding.bind(itemView)

fun bind(interval: AutoCloseInterval) {
val context = itemView.context
val stringRes = when (interval) {
Manual, OneDay -> {
itemView.visibility = View.GONE
null
}
OneWeek -> {
context.getString(interval.description)
}
OneMonth -> {
context.getString(interval.description)
}
}
if (stringRes != null) {
binding.inactiveDescription.text =
context.getString(R.string.inactive_tabs_description, stringRes)
}
}

companion object {
const val LAYOUT_ID = R.layout.inactive_footer_item
}
}
}

enum class AutoCloseInterval(@StringRes val description: Int) {
Manual(0),
OneDay(0),
OneWeek(R.string.inactive_tabs_7_days),
OneMonth(R.string.inactive_tabs_30_days)
}
Loading

0 comments on commit 9c1eb5a

Please sign in to comment.