Issue #20349: Add inactive tab grouping to tabs tray
parent
f8945b3720
commit
69d630f46c
@ -0,0 +1,68 @@
|
|||||||
|
/* 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.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base class for a tabs tray list that wants to display browser tabs.
|
||||||
|
*/
|
||||||
|
abstract class AbstractBrowserTrayList @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : RecyclerView(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
lateinit var interactor: TabsTrayInteractor
|
||||||
|
lateinit var tabsTrayStore: TabsTrayStore
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected val removeTabUseCase = RemoveTabUseCaseWrapper(
|
||||||
|
context.components.analytics.metrics
|
||||||
|
) { sessionId ->
|
||||||
|
interactor.onDeleteTab(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected val swipeToDelete by lazy {
|
||||||
|
SwipeToDeleteBinding(tabsTrayStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachedToWindow() {
|
||||||
|
super.onAttachedToWindow()
|
||||||
|
|
||||||
|
swipeToDelete.start()
|
||||||
|
|
||||||
|
adapter?.onAttachedToRecyclerView(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
super.onDetachedFromWindow()
|
||||||
|
|
||||||
|
swipeToDelete.stop()
|
||||||
|
|
||||||
|
// Notify the adapter that it is released from the view preemptively.
|
||||||
|
adapter?.onDetachedFromRecyclerView(this)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
/* 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 -> {
|
||||||
|
binding.inactiveDescription.visibility = View.GONE
|
||||||
|
binding.topDivider.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)
|
||||||
|
}
|
@ -0,0 +1,132 @@
|
|||||||
|
/* 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.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import mozilla.components.concept.tabstray.Tab as TabsTrayTab
|
||||||
|
import mozilla.components.concept.tabstray.Tabs
|
||||||
|
import mozilla.components.concept.tabstray.TabsTray
|
||||||
|
import mozilla.components.support.base.observer.ObserverRegistry
|
||||||
|
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 mozilla.components.support.base.observer.Observable as ComponentObservable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A convenience alias for readability.
|
||||||
|
*/
|
||||||
|
private typealias Adapter = ListAdapter<InactiveTabsAdapter.Item, InactiveTabViewHolder>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A convenience alias for readability.
|
||||||
|
*/
|
||||||
|
private typealias Observable = ComponentObservable<TabsTray.Observer>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [ListAdapter] for displaying the list of inactive tabs.
|
||||||
|
*/
|
||||||
|
class InactiveTabsAdapter(
|
||||||
|
private val context: Context,
|
||||||
|
private val browserTrayInteractor: BrowserTrayInteractor,
|
||||||
|
delegate: Observable = ObserverRegistry()
|
||||||
|
) : Adapter(DiffCallback), TabsTray, Observable by delegate {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InactiveTabViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(viewType, parent, false)
|
||||||
|
|
||||||
|
return when (viewType) {
|
||||||
|
HeaderHolder.LAYOUT_ID -> HeaderHolder(view)
|
||||||
|
TabViewHolder.LAYOUT_ID -> TabViewHolder(view, browserTrayInteractor)
|
||||||
|
FooterHolder.LAYOUT_ID -> FooterHolder(view)
|
||||||
|
else -> throw IllegalStateException("Unknown viewType: $viewType")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: InactiveTabViewHolder, position: Int) {
|
||||||
|
when (holder) {
|
||||||
|
is TabViewHolder -> {
|
||||||
|
val item = getItem(position) as Item.Tab
|
||||||
|
holder.bind(item.tab)
|
||||||
|
}
|
||||||
|
is FooterHolder -> {
|
||||||
|
val item = getItem(position) as Item.Footer
|
||||||
|
holder.bind(item.interval)
|
||||||
|
}
|
||||||
|
is HeaderHolder -> {
|
||||||
|
// do nothing.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
return when (position) {
|
||||||
|
0 -> HeaderHolder.LAYOUT_ID
|
||||||
|
itemCount - 1 -> FooterHolder.LAYOUT_ID
|
||||||
|
else -> TabViewHolder.LAYOUT_ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateTabs(tabs: Tabs) {
|
||||||
|
if (tabs.list.isEmpty()) {
|
||||||
|
// Early return with an empty list to remove the header/footer items.
|
||||||
|
submitList(emptyList())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val items = tabs.list.map { Item.Tab(it) }
|
||||||
|
val footer = Item.Footer(context.autoCloseInterval)
|
||||||
|
|
||||||
|
submitList(listOf(Item.Header) + items + listOf(footer))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isTabSelected(tabs: Tabs, position: Int): Boolean = false
|
||||||
|
override fun onTabsChanged(position: Int, count: Int) = Unit
|
||||||
|
override fun onTabsInserted(position: Int, count: Int) = Unit
|
||||||
|
override fun onTabsMoved(fromPosition: Int, toPosition: Int) = Unit
|
||||||
|
override fun onTabsRemoved(position: Int, count: Int) = Unit
|
||||||
|
|
||||||
|
private object DiffCallback : DiffUtil.ItemCallback<Item>() {
|
||||||
|
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: TabsTrayTab) : Item()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A footer for the inactive tab section. This may be seen only
|
||||||
|
* when at least one inactive tab is present.
|
||||||
|
*/
|
||||||
|
data class Footer(val interval: AutoCloseInterval) : Item()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
/* 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.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
|
import mozilla.components.browser.tabstray.TabViewHolder
|
||||||
|
import mozilla.components.feature.tabs.tabstray.TabsFeature
|
||||||
|
import org.mozilla.fenix.FeatureFlags
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.tabstray.ext.browserAdapter
|
||||||
|
import org.mozilla.fenix.tabstray.ext.inactiveTabsAdapter
|
||||||
|
import org.mozilla.fenix.tabstray.ext.isNormalTabActive
|
||||||
|
import org.mozilla.fenix.tabstray.ext.isNormalTabInactive
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time until which a tab is considered in-active (in days).
|
||||||
|
*/
|
||||||
|
const val DEFAULT_INACTIVE_DAYS = 4L
|
||||||
|
|
||||||
|
class NormalBrowserTrayList @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : AbstractBrowserTrayList(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum time from when a tab was created or accessed until it is considered "inactive".
|
||||||
|
*/
|
||||||
|
var maxActiveTime = TimeUnit.DAYS.toMillis(DEFAULT_INACTIVE_DAYS)
|
||||||
|
|
||||||
|
private val concatAdapter by lazy { adapter as ConcatAdapter }
|
||||||
|
|
||||||
|
override val tabsFeature by lazy {
|
||||||
|
val tabsAdapter = concatAdapter.browserAdapter
|
||||||
|
|
||||||
|
TabsFeature(
|
||||||
|
tabsAdapter,
|
||||||
|
context.components.core.store,
|
||||||
|
selectTabUseCase,
|
||||||
|
removeTabUseCase,
|
||||||
|
{ state ->
|
||||||
|
if (!FeatureFlags.inactiveTabs) {
|
||||||
|
return@TabsFeature !state.content.private
|
||||||
|
}
|
||||||
|
state.isNormalTabActive(maxActiveTime)
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val inactiveFeature by lazy {
|
||||||
|
val tabsAdapter = concatAdapter.inactiveTabsAdapter
|
||||||
|
|
||||||
|
TabsFeature(
|
||||||
|
tabsAdapter,
|
||||||
|
context.components.core.store,
|
||||||
|
selectTabUseCase,
|
||||||
|
removeTabUseCase,
|
||||||
|
{ state ->
|
||||||
|
if (!FeatureFlags.inactiveTabs) {
|
||||||
|
return@TabsFeature false
|
||||||
|
}
|
||||||
|
state.isNormalTabInactive(maxActiveTime)
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val touchHelper by lazy {
|
||||||
|
TabsTouchHelper(
|
||||||
|
observable = concatAdapter.browserAdapter,
|
||||||
|
onViewHolderTouched = {
|
||||||
|
it is TabViewHolder && swipeToDelete.isSwipeable
|
||||||
|
},
|
||||||
|
onViewHolderDraw = { context.components.settings.gridTabView.not() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachedToWindow() {
|
||||||
|
super.onAttachedToWindow()
|
||||||
|
|
||||||
|
tabsFeature.start()
|
||||||
|
inactiveFeature.start()
|
||||||
|
|
||||||
|
touchHelper.attachToRecyclerView(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
super.onDetachedFromWindow()
|
||||||
|
|
||||||
|
tabsFeature.stop()
|
||||||
|
inactiveFeature.stop()
|
||||||
|
|
||||||
|
touchHelper.attachToRecyclerView(null)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
/* 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.ext
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
|
import org.mozilla.fenix.tabstray.browser.BrowserTabsAdapter
|
||||||
|
import org.mozilla.fenix.tabstray.browser.InactiveTabsAdapter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A convenience binding for retrieving the [BrowserTabsAdapter] from the [ConcatAdapter].
|
||||||
|
*/
|
||||||
|
internal val ConcatAdapter.browserAdapter
|
||||||
|
get() = adapters.find { it is BrowserTabsAdapter } as BrowserTabsAdapter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A convenience binding for retrieving the [InactiveTabsAdapter] from the [ConcatAdapter].
|
||||||
|
*/
|
||||||
|
internal val ConcatAdapter.inactiveTabsAdapter
|
||||||
|
get() = adapters.find { it is InactiveTabsAdapter } as InactiveTabsAdapter
|
@ -0,0 +1,42 @@
|
|||||||
|
<?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/. -->
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:background="@drawable/rounded_bottom_corners"
|
||||||
|
android:elevation="@dimen/home_item_elevation"
|
||||||
|
android:paddingBottom="8dp">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/top_divider"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="?neutralFaded"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/inactive_description"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:paddingBottom="8dp"
|
||||||
|
android:textColor="?secondaryText"
|
||||||
|
android:textSize="12sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/top_divider"
|
||||||
|
tools:text="@string/inactive_tabs_description" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -0,0 +1,35 @@
|
|||||||
|
<?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/. -->
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:background="@drawable/rounded_top_corners"
|
||||||
|
android:clickable="false"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:elevation="@dimen/home_item_elevation"
|
||||||
|
android:focusable="true"
|
||||||
|
android:foreground="?android:attr/selectableItemBackground"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/inactive_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:gravity="start"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:minLines="1"
|
||||||
|
android:text="@string/inactive_tabs_title"
|
||||||
|
android:textAppearance="@style/Header16TextStyle"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Inactive tabs" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -0,0 +1,15 @@
|
|||||||
|
<?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/. -->
|
||||||
|
<mozilla.components.ui.widgets.WidgetSiteItemView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/site_list_item"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:background="?above"
|
||||||
|
android:elevation="@dimen/home_item_elevation"
|
||||||
|
android:foreground="?android:attr/selectableItemBackground"
|
||||||
|
android:minHeight="@dimen/mozac_widget_site_item_height" />
|
@ -1,101 +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.content.Context
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.util.DisplayMetrics
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.mockk
|
|
||||||
import io.mockk.mockkStatic
|
|
||||||
import io.mockk.spyk
|
|
||||||
import io.mockk.unmockkStatic
|
|
||||||
import org.junit.After
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
import org.mozilla.fenix.tabstray.TrayPagerAdapter
|
|
||||||
import org.mozilla.fenix.tabstray.ext.numberOfGridColumns
|
|
||||||
import org.mozilla.fenix.utils.Settings
|
|
||||||
|
|
||||||
class DefaultBrowserTrayInteractorTest {
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
mockkStatic("org.mozilla.fenix.tabstray.ext.ContextKt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
fun shutdown() {
|
|
||||||
unmockkStatic("org.mozilla.fenix.tabstray.ext.ContextKt")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN pager position is synced tabs THEN return a list layout manager`() {
|
|
||||||
val interactor =
|
|
||||||
DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), mockk(), mockk(), mockk())
|
|
||||||
|
|
||||||
val result = interactor.getLayoutManagerForPosition(
|
|
||||||
mockk(),
|
|
||||||
TrayPagerAdapter.POSITION_SYNCED_TABS
|
|
||||||
)
|
|
||||||
|
|
||||||
assertEquals(1, (result as GridLayoutManager).spanCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN setting is grid view THEN return grid layout manager`() {
|
|
||||||
val context = mockk<Context>()
|
|
||||||
val settings = mockk<Settings>()
|
|
||||||
val interactor =
|
|
||||||
DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), mockk(), settings, mockk())
|
|
||||||
|
|
||||||
every { context.numberOfGridColumns }.answers { 4 }
|
|
||||||
every { settings.gridTabView }.answers { true }
|
|
||||||
|
|
||||||
val result = interactor.getLayoutManagerForPosition(
|
|
||||||
context,
|
|
||||||
TrayPagerAdapter.POSITION_NORMAL_TABS
|
|
||||||
)
|
|
||||||
|
|
||||||
assertEquals(4, (result as GridLayoutManager).spanCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN setting is list view THEN return list layout manager`() {
|
|
||||||
val context = mockk<Context>()
|
|
||||||
val settings = mockk<Settings>()
|
|
||||||
val interactor =
|
|
||||||
DefaultBrowserTrayInteractor(mockk(), mockk(), mockk(), mockk(), settings, mockk())
|
|
||||||
|
|
||||||
every { context.numberOfGridColumns }.answers { 4 }
|
|
||||||
every { settings.gridTabView }.answers { false }
|
|
||||||
|
|
||||||
val result = interactor.getLayoutManagerForPosition(
|
|
||||||
context,
|
|
||||||
TrayPagerAdapter.POSITION_NORMAL_TABS
|
|
||||||
)
|
|
||||||
|
|
||||||
// Should NOT be 4.
|
|
||||||
assertEquals(1, (result as GridLayoutManager).spanCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN screen density is very low THEN numberOfGridColumns will still be a minimum of 2`() {
|
|
||||||
val context = mockk<Context>()
|
|
||||||
val resources = mockk<Resources>()
|
|
||||||
val displayMetrics = spyk<DisplayMetrics> {
|
|
||||||
widthPixels = 1
|
|
||||||
density = 1f
|
|
||||||
}
|
|
||||||
every { context.resources } returns resources
|
|
||||||
every { resources.displayMetrics } returns displayMetrics
|
|
||||||
|
|
||||||
val result = context.numberOfGridColumns
|
|
||||||
|
|
||||||
assertEquals(2, result)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,39 @@
|
|||||||
|
/* 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.ext
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.util.DisplayMetrics
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.spyk
|
||||||
|
import io.mockk.unmockkStatic
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ContextKtTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `WHEN screen density is very low THEN numberOfGridColumns will still be a minimum of 2`() {
|
||||||
|
mockkStatic("org.mozilla.fenix.tabstray.ext.ContextKt")
|
||||||
|
|
||||||
|
val context = mockk<Context>()
|
||||||
|
val resources = mockk<Resources>()
|
||||||
|
val displayMetrics = spyk<DisplayMetrics> {
|
||||||
|
widthPixels = 1
|
||||||
|
density = 1f
|
||||||
|
}
|
||||||
|
every { context.resources } returns resources
|
||||||
|
every { resources.displayMetrics } returns displayMetrics
|
||||||
|
|
||||||
|
val result = context.numberOfGridColumns
|
||||||
|
|
||||||
|
assertEquals(2, result)
|
||||||
|
|
||||||
|
unmockkStatic("org.mozilla.fenix.tabstray.ext.ContextKt")
|
||||||
|
}
|
||||||
|
}
|
@ -1,57 +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.ext
|
|
||||||
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.mockk
|
|
||||||
import mozilla.components.browser.state.state.ContentState
|
|
||||||
import mozilla.components.browser.state.state.TabSessionState
|
|
||||||
import org.junit.Assert.assertFalse
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Test
|
|
||||||
import org.mozilla.fenix.tabstray.browser.BrowserTrayList.BrowserTabType.NORMAL
|
|
||||||
import org.mozilla.fenix.tabstray.browser.BrowserTrayList.BrowserTabType.PRIVATE
|
|
||||||
|
|
||||||
class TabSessionStateKtTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN configuration is private THEN return true`() {
|
|
||||||
val contentState = mockk<ContentState>()
|
|
||||||
val state = TabSessionState(content = contentState)
|
|
||||||
val config = PRIVATE
|
|
||||||
|
|
||||||
every { contentState.private } returns true
|
|
||||||
|
|
||||||
assertTrue(state.filterFromConfig(config))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN configuration is normal THEN return false`() {
|
|
||||||
val contentState = mockk<ContentState>()
|
|
||||||
val state = TabSessionState(content = contentState)
|
|
||||||
val config = NORMAL
|
|
||||||
|
|
||||||
every { contentState.private } returns false
|
|
||||||
|
|
||||||
assertTrue(state.filterFromConfig(config))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN configuration does not match THEN return false`() {
|
|
||||||
val contentState = mockk<ContentState>()
|
|
||||||
val state = TabSessionState(content = contentState)
|
|
||||||
val config = NORMAL
|
|
||||||
|
|
||||||
every { contentState.private } returns true
|
|
||||||
|
|
||||||
assertFalse(state.filterFromConfig(config))
|
|
||||||
|
|
||||||
val config2 = PRIVATE
|
|
||||||
|
|
||||||
every { contentState.private } returns false
|
|
||||||
|
|
||||||
assertFalse(state.filterFromConfig(config2))
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue