Issue #19112: Remove old tab tray code
parent
bf605c02d9
commit
dc11c334b6
@ -1,46 +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.ui.robots
|
||||
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.espresso.matcher.ViewMatchers.Visibility
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import org.hamcrest.CoreMatchers.allOf
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.helpers.click
|
||||
|
||||
/**
|
||||
* Implementation of Robot Pattern for Synced Tabs sub menu.
|
||||
*/
|
||||
class SyncedTabsRobot {
|
||||
|
||||
fun verifySyncedTabsMenuHeader() = assertSyncedTabsMenuHeader()
|
||||
|
||||
class Transition {
|
||||
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())!!
|
||||
|
||||
fun goBack(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
|
||||
goBackButton().click()
|
||||
|
||||
BrowserRobot().interact()
|
||||
return BrowserRobot.Transition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun goBackButton() =
|
||||
onView(allOf(withContentDescription("Navigate up")))
|
||||
|
||||
private fun assertSyncedTabsMenuHeader() {
|
||||
// Replaced with the new string here, the test is assuming we are NOT signed in
|
||||
// Sync tests in SettingsSyncTest are still TO-DO, so I'm not sure that we have a test for signing into Sync
|
||||
onView(withText(R.string.sync_menu_sign_in))
|
||||
.check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
|
||||
}
|
@ -1,158 +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.tabtray
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.checkbox_item.view.*
|
||||
import kotlinx.android.synthetic.main.tab_tray_item.view.*
|
||||
import mozilla.components.browser.state.selector.findTab
|
||||
import mozilla.components.browser.tabstray.TabViewHolder
|
||||
import mozilla.components.browser.tabstray.TabsAdapter
|
||||
import mozilla.components.browser.thumbnails.loader.ThumbnailLoader
|
||||
import mozilla.components.concept.base.images.ImageLoader
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import mozilla.components.concept.tabstray.Tabs
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.metrics
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.ext.updateAccessibilityCollectionItemInfo
|
||||
|
||||
class FenixTabsAdapter(
|
||||
private val context: Context,
|
||||
imageLoader: ImageLoader = ThumbnailLoader(context.components.core.thumbnailStorage)
|
||||
) : TabsAdapter(
|
||||
viewHolderProvider = { parentView ->
|
||||
TabTrayViewHolder(
|
||||
LayoutInflater.from(context).inflate(
|
||||
if (context.settings().gridTabView) R.layout.tab_tray_grid_item else R.layout.tab_tray_item,
|
||||
parentView,
|
||||
false
|
||||
),
|
||||
imageLoader
|
||||
)
|
||||
}
|
||||
) {
|
||||
private lateinit var tabsList: RecyclerView
|
||||
var tabTrayInteractor: TabTrayInteractor? = null
|
||||
|
||||
private val mode: TabTrayDialogFragmentState.Mode?
|
||||
get() = tabTrayInteractor?.onModeRequested()
|
||||
|
||||
val selectedItems get() = mode?.selectedItems ?: setOf()
|
||||
|
||||
var onTabsUpdated: (() -> Unit)? = null
|
||||
var tabCount = 0
|
||||
|
||||
override fun updateTabs(tabs: Tabs) {
|
||||
super.updateTabs(tabs)
|
||||
onTabsUpdated?.invoke()
|
||||
tabCount = tabs.list.size
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: TabViewHolder,
|
||||
position: Int,
|
||||
payloads: List<Any>
|
||||
) {
|
||||
if (payloads.isNullOrEmpty()) {
|
||||
onBindViewHolder(holder, position)
|
||||
return
|
||||
}
|
||||
|
||||
// Having non-empty payloads means we have to make a partial update.
|
||||
// This currently only happens when changing between the Normal and MultiSelect modes
|
||||
// when we want to either show the last opened tab as selected (default) or hide this ui decorator.
|
||||
if (mode is TabTrayDialogFragmentState.Mode.Normal) {
|
||||
super.onBindViewHolder(holder, position, listOf(PAYLOAD_HIGHLIGHT_SELECTED_ITEM))
|
||||
} else {
|
||||
super.onBindViewHolder(holder, position, listOf(PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM))
|
||||
}
|
||||
|
||||
holder.tab?.let { showCheckedIfSelected(it, holder.itemView) }
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
|
||||
super.onBindViewHolder(holder, position)
|
||||
|
||||
val isListTabView = context.settings().listTabView
|
||||
|
||||
val itemIndex: Int
|
||||
val rowIndex: Int
|
||||
val columnIndex: Int
|
||||
|
||||
if (isListTabView) {
|
||||
itemIndex = tabCount - position - 1
|
||||
rowIndex = itemIndex
|
||||
columnIndex = 1
|
||||
} else {
|
||||
val columnsCount = (tabsList.layoutManager as GridLayoutManager).spanCount
|
||||
itemIndex = position
|
||||
rowIndex = itemIndex / columnsCount
|
||||
columnIndex = itemIndex % columnsCount
|
||||
}
|
||||
|
||||
holder.itemView.updateAccessibilityCollectionItemInfo(
|
||||
rowIndex,
|
||||
columnIndex,
|
||||
selectedItems.contains(holder.tab)
|
||||
)
|
||||
|
||||
holder.tab?.let { tab ->
|
||||
showCheckedIfSelected(tab, holder.itemView)
|
||||
|
||||
val tabIsPrivate =
|
||||
context.components.core.store.state.findTab(tab.id)?.content?.private == true
|
||||
if (!tabIsPrivate) {
|
||||
holder.itemView.setOnLongClickListener {
|
||||
if (mode is TabTrayDialogFragmentState.Mode.Normal) {
|
||||
context.metrics.track(Event.CollectionTabLongPressed)
|
||||
tabTrayInteractor?.onAddSelectedTab(
|
||||
tab
|
||||
)
|
||||
}
|
||||
true
|
||||
}
|
||||
} else {
|
||||
holder.itemView.setOnLongClickListener(null)
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
if (mode is TabTrayDialogFragmentState.Mode.MultiSelect) {
|
||||
if (mode?.selectedItems?.contains(tab) == true) {
|
||||
tabTrayInteractor?.onRemoveSelectedTab(tab = tab)
|
||||
} else {
|
||||
tabTrayInteractor?.onAddSelectedTab(tab = tab)
|
||||
}
|
||||
} else {
|
||||
tabTrayInteractor?.onOpenTab(tab = tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView)
|
||||
tabsList = recyclerView
|
||||
}
|
||||
|
||||
override fun isTabSelected(tabs: Tabs, position: Int): Boolean {
|
||||
return mode is TabTrayDialogFragmentState.Mode.Normal &&
|
||||
tabs.selectedIndex == position
|
||||
}
|
||||
|
||||
private fun showCheckedIfSelected(tab: Tab, view: View) {
|
||||
val shouldBeChecked =
|
||||
mode is TabTrayDialogFragmentState.Mode.MultiSelect && selectedItems.contains(tab)
|
||||
view.selected_mask.isVisible = shouldBeChecked
|
||||
view.mozac_browser_tabstray_close.isVisible = mode is TabTrayDialogFragmentState.Mode.Normal
|
||||
}
|
||||
}
|
@ -1,40 +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.tabtray
|
||||
|
||||
import android.content.Context
|
||||
import mozilla.components.browser.menu.BrowserMenuBuilder
|
||||
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
|
||||
import org.mozilla.fenix.R
|
||||
|
||||
class MultiselectSelectionMenu(
|
||||
private val context: Context,
|
||||
private val onItemTapped: (Item) -> Unit = {}
|
||||
) {
|
||||
sealed class Item {
|
||||
object BookmarkTabs : Item()
|
||||
object DeleteTabs : Item()
|
||||
}
|
||||
|
||||
val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
|
||||
|
||||
private val menuItems by lazy {
|
||||
listOf(
|
||||
SimpleBrowserMenuItem(
|
||||
context.getString(R.string.tab_tray_multiselect_menu_item_bookmark),
|
||||
textColorResource = R.color.primary_text_normal_theme
|
||||
) {
|
||||
onItemTapped.invoke(Item.BookmarkTabs)
|
||||
},
|
||||
|
||||
SimpleBrowserMenuItem(
|
||||
context.getString(R.string.tab_tray_multiselect_menu_item_close),
|
||||
textColorResource = R.color.primary_text_normal_theme
|
||||
) {
|
||||
onItemTapped.invoke(Item.DeleteTabs)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
@ -1,108 +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.tabtray
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ext.setNewAccessibilityParent
|
||||
import org.mozilla.fenix.tabtray.SaveToCollectionsButtonAdapter.Item
|
||||
import org.mozilla.fenix.tabtray.SaveToCollectionsButtonAdapter.ViewHolder
|
||||
|
||||
/**
|
||||
* An adapter to display a single 'Save to Collections' button that can be used to display between
|
||||
* multiple [RecyclerView.Adapter] in one [RecyclerView].
|
||||
*/
|
||||
class SaveToCollectionsButtonAdapter(
|
||||
private val interactor: TabTrayInteractor,
|
||||
private val isPrivate: () -> Boolean = { false }
|
||||
) : ListAdapter<Item, ViewHolder>(DiffCallback) {
|
||||
|
||||
init {
|
||||
submitList(listOf(Item))
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val itemView = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
||||
return ViewHolder(itemView, interactor)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
|
||||
// remove button from node info of tabs list for a11y services,and add it to the tab tray node
|
||||
holder.itemView.accessibilityDelegate = object : View.AccessibilityDelegate() {
|
||||
override fun onInitializeAccessibilityNodeInfo(
|
||||
host: View?,
|
||||
info: AccessibilityNodeInfo?
|
||||
) {
|
||||
super.onInitializeAccessibilityNodeInfo(host, info)
|
||||
info?.collectionItemInfo = null
|
||||
(holder.itemView.parentForAccessibility.parentForAccessibility as? View)?.let {
|
||||
holder.itemView.setNewAccessibilityParent(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (payloads.isNullOrEmpty()) {
|
||||
onBindViewHolder(holder, position)
|
||||
return
|
||||
}
|
||||
|
||||
when (val change = payloads[0]) {
|
||||
is TabTrayView.TabChange -> {
|
||||
holder.itemView.isVisible = change == TabTrayView.TabChange.NORMAL
|
||||
}
|
||||
is MultiselectModeChange -> {
|
||||
holder.itemView.isVisible = change == MultiselectModeChange.NORMAL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.itemView.isVisible = !isPrivate() &&
|
||||
interactor.onModeRequested() is TabTrayDialogFragmentState.Mode.Normal
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return ViewHolder.LAYOUT_ID
|
||||
}
|
||||
|
||||
private object DiffCallback : DiffUtil.ItemCallback<Item>() {
|
||||
override fun areItemsTheSame(oldItem: Item, newItem: Item) = true
|
||||
|
||||
override fun areContentsTheSame(oldItem: Item, newItem: Item) = true
|
||||
}
|
||||
|
||||
enum class MultiselectModeChange {
|
||||
MULTISELECT, NORMAL
|
||||
}
|
||||
|
||||
/**
|
||||
* An object to identify the data type.
|
||||
*/
|
||||
object Item
|
||||
|
||||
class ViewHolder(
|
||||
itemView: View,
|
||||
private val interactor: TabTrayInteractor
|
||||
) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
|
||||
init {
|
||||
itemView.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onClick(v: View?) {
|
||||
interactor.onEnterMultiselect()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LAYOUT_ID = R.layout.tabs_tray_save_to_collections_item
|
||||
}
|
||||
}
|
||||
}
|
@ -1,251 +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.tabtray
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.appservices.places.BookmarkRoot
|
||||
import mozilla.components.browser.state.selector.findTab
|
||||
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
|
||||
import mozilla.components.browser.state.selector.normalTabs
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
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.engine.prompt.ShareData
|
||||
import mozilla.components.concept.storage.BookmarksStorage
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
|
||||
import org.mozilla.fenix.components.TabCollectionStorage
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.components.metrics.MetricController
|
||||
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
|
||||
import org.mozilla.fenix.home.HomeFragment
|
||||
|
||||
/**
|
||||
* [TabTrayDialogFragment] controller.
|
||||
*
|
||||
* Delegated by View Interactors, handles container business logic and operates changes on it.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface TabTrayController {
|
||||
fun handleNewTabTapped(private: Boolean)
|
||||
fun handleTabTrayDismissed()
|
||||
fun handleTabSettingsClicked()
|
||||
fun handleShareTabsOfTypeClicked(private: Boolean)
|
||||
fun handleShareSelectedTabsClicked(selectedTabs: Set<Tab>)
|
||||
fun handleSaveToCollectionClicked(selectedTabs: Set<Tab>)
|
||||
fun handleBookmarkSelectedTabs(selectedTabs: Set<Tab>)
|
||||
fun handleDeleteSelectedTabs(selectedTabs: Set<Tab>)
|
||||
fun handleCloseAllTabsClicked(private: Boolean)
|
||||
fun handleBackPressed(): Boolean
|
||||
fun onModeRequested(): TabTrayDialogFragmentState.Mode
|
||||
fun handleAddSelectedTab(tab: Tab)
|
||||
fun handleRemoveSelectedTab(tab: Tab)
|
||||
fun handleOpenTab(tab: Tab)
|
||||
fun handleEnterMultiselect()
|
||||
fun handleRecentlyClosedClicked()
|
||||
fun handleGoToTabsSettingClicked()
|
||||
}
|
||||
|
||||
/**
|
||||
* Default behavior of [TabTrayController]. Other implementations are possible.
|
||||
*
|
||||
* @param activity [Activity] the current activity.
|
||||
* @param profiler [Profiler] used for profiling.
|
||||
* @param browserStore [BrowserStore] holds the global [BrowserState].
|
||||
* @param browsingModeManager [HomeActivity] used for registering browsing mode.
|
||||
* @param tabCollectionStorage [TabCollectionStorage] storage for saving collections.
|
||||
* @param ioScope [CoroutineScope] with an IO dispatcher used for structured concurrency.
|
||||
* @param metrics reference to the configured [MetricController] to record telemetry events.
|
||||
* @param tabsUseCases [TabsUseCases] use cases related to the tabs feature.
|
||||
* @param navController - [NavController] used for navigation.
|
||||
* @param dismissTabTray callback allowing to request this entire Fragment to be dismissed.
|
||||
* @param dismissTabTrayAndNavigateHome callback allowing showing an undo snackbar after tab deletion.
|
||||
* @param registerCollectionStorageObserver callback allowing for registering the [TabCollectionStorage.Observer]
|
||||
* when needed.
|
||||
* @param tabTrayDialogFragmentStore [TabTrayDialogFragmentStore] holding the State for all Views displayed
|
||||
* in this Controller's Fragment.
|
||||
* @param selectTabUseCase [TabsUseCases.SelectTabUseCase] callback allowing for selecting a tab.
|
||||
* @param showChooseCollectionDialog callback allowing saving a list of sessions to an existing collection.
|
||||
* @param showAddNewCollectionDialog callback allowing for saving a list of sessions to a new collection.
|
||||
* @param showUndoSnackbarForTabs callback allowing for showing an undo snackbar for removed tabs.
|
||||
* @param showBookmarksSnackbar callback allowing for showing a snackbar with action to view bookmarks.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class DefaultTabTrayController(
|
||||
private val activity: HomeActivity,
|
||||
private val profiler: Profiler?,
|
||||
private val browserStore: BrowserStore,
|
||||
private val browsingModeManager: BrowsingModeManager,
|
||||
private val tabCollectionStorage: TabCollectionStorage,
|
||||
private val bookmarksStorage: BookmarksStorage,
|
||||
private val ioScope: CoroutineScope,
|
||||
private val metrics: MetricController,
|
||||
private val tabsUseCases: TabsUseCases,
|
||||
private val navController: NavController,
|
||||
private val dismissTabTray: () -> Unit,
|
||||
private val dismissTabTrayAndNavigateHome: (String) -> Unit,
|
||||
private val registerCollectionStorageObserver: () -> Unit,
|
||||
private val tabTrayDialogFragmentStore: TabTrayDialogFragmentStore,
|
||||
private val selectTabUseCase: TabsUseCases.SelectTabUseCase,
|
||||
private val showChooseCollectionDialog: (List<TabSessionState>) -> Unit,
|
||||
private val showAddNewCollectionDialog: (List<TabSessionState>) -> Unit,
|
||||
private val showUndoSnackbarForTabs: () -> Unit,
|
||||
private val showBookmarksSnackbar: () -> Unit
|
||||
) : TabTrayController {
|
||||
|
||||
override fun handleNewTabTapped(private: Boolean) {
|
||||
val startTime = profiler?.getProfilerTime()
|
||||
browsingModeManager.mode = BrowsingMode.fromBoolean(private)
|
||||
navController.navigateBlockingForAsyncNavGraph(
|
||||
TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true))
|
||||
dismissTabTray()
|
||||
profiler?.addMarker(
|
||||
"DefaultTabTrayController.onNewTabTapped",
|
||||
startTime
|
||||
)
|
||||
}
|
||||
|
||||
override fun handleTabSettingsClicked() {
|
||||
navController.navigateBlockingForAsyncNavGraph(
|
||||
TabTrayDialogFragmentDirections.actionGlobalTabSettingsFragment())
|
||||
}
|
||||
|
||||
override fun handleTabTrayDismissed() {
|
||||
dismissTabTray()
|
||||
}
|
||||
|
||||
override fun handleSaveToCollectionClicked(selectedTabs: Set<Tab>) {
|
||||
metrics.track(Event.TabsTraySaveToCollectionPressed)
|
||||
|
||||
val sessionList = selectedTabs.map {
|
||||
browserStore.state.findTab(it.id) ?: return
|
||||
}
|
||||
|
||||
// Only register the observer right before moving to collection creation
|
||||
registerCollectionStorageObserver()
|
||||
|
||||
when {
|
||||
tabCollectionStorage.cachedTabCollections.isNotEmpty() -> {
|
||||
showChooseCollectionDialog(sessionList)
|
||||
}
|
||||
else -> {
|
||||
showAddNewCollectionDialog(sessionList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleShareTabsOfTypeClicked(private: Boolean) {
|
||||
val tabs = browserStore.state.getNormalOrPrivateTabs(private)
|
||||
val data = tabs.map {
|
||||
ShareData(url = it.content.url, title = it.content.title)
|
||||
}
|
||||
val directions = TabTrayDialogFragmentDirections.actionGlobalShareFragment(
|
||||
data = data.toTypedArray()
|
||||
)
|
||||
navController.navigateBlockingForAsyncNavGraph(directions)
|
||||
}
|
||||
|
||||
override fun handleShareSelectedTabsClicked(selectedTabs: Set<Tab>) {
|
||||
val data = selectedTabs.map {
|
||||
ShareData(url = it.url, title = it.title)
|
||||
}
|
||||
val directions = TabTrayDialogFragmentDirections.actionGlobalShareFragment(
|
||||
data = data.toTypedArray()
|
||||
)
|
||||
navController.navigateBlockingForAsyncNavGraph(directions)
|
||||
}
|
||||
|
||||
override fun handleBookmarkSelectedTabs(selectedTabs: Set<Tab>) {
|
||||
selectedTabs.forEach {
|
||||
ioScope.launch {
|
||||
val shouldAddBookmark = bookmarksStorage.getBookmarksWithUrl(it.url)
|
||||
.firstOrNull { it.url == it.url } == null
|
||||
if (shouldAddBookmark) {
|
||||
bookmarksStorage.addItem(
|
||||
BookmarkRoot.Mobile.id,
|
||||
url = it.url,
|
||||
title = it.title,
|
||||
position = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
|
||||
showBookmarksSnackbar()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun handleDeleteSelectedTabs(selectedTabs: Set<Tab>) {
|
||||
if (browserStore.state.normalTabs.size == selectedTabs.size) {
|
||||
dismissTabTrayAndNavigateHome(HomeFragment.ALL_NORMAL_TABS)
|
||||
} else {
|
||||
selectedTabs.map { it.id }.let {
|
||||
tabsUseCases.removeTabs(it)
|
||||
}
|
||||
|
||||
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
|
||||
showUndoSnackbarForTabs()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun handleCloseAllTabsClicked(private: Boolean) {
|
||||
val sessionsToClose = if (private) {
|
||||
HomeFragment.ALL_PRIVATE_TABS
|
||||
} else {
|
||||
HomeFragment.ALL_NORMAL_TABS
|
||||
}
|
||||
|
||||
dismissTabTrayAndNavigateHome(sessionsToClose)
|
||||
}
|
||||
|
||||
override fun handleAddSelectedTab(tab: Tab) {
|
||||
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.AddItemForCollection(tab))
|
||||
}
|
||||
|
||||
override fun handleRemoveSelectedTab(tab: Tab) {
|
||||
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.RemoveItemForCollection(tab))
|
||||
}
|
||||
|
||||
override fun handleBackPressed(): Boolean {
|
||||
return if (tabTrayDialogFragmentStore.state.mode is TabTrayDialogFragmentState.Mode.MultiSelect) {
|
||||
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onModeRequested(): TabTrayDialogFragmentState.Mode {
|
||||
return tabTrayDialogFragmentStore.state.mode
|
||||
}
|
||||
|
||||
override fun handleOpenTab(tab: Tab) {
|
||||
selectTabUseCase.invoke(tab.id)
|
||||
}
|
||||
|
||||
override fun handleEnterMultiselect() {
|
||||
tabTrayDialogFragmentStore.dispatch(TabTrayDialogFragmentAction.EnterMultiSelectMode)
|
||||
}
|
||||
|
||||
override fun handleRecentlyClosedClicked() {
|
||||
val directions = TabTrayDialogFragmentDirections.actionGlobalRecentlyClosed()
|
||||
navController.navigateBlockingForAsyncNavGraph(directions)
|
||||
metrics.track(Event.RecentlyClosedTabsOpened)
|
||||
}
|
||||
|
||||
override fun handleGoToTabsSettingClicked() {
|
||||
val directions = TabTrayDialogFragmentDirections.actionGlobalTabSettingsFragment()
|
||||
navController.navigateBlockingForAsyncNavGraph(directions)
|
||||
metrics.track(Event.TabsTrayCfrTapped)
|
||||
}
|
||||
}
|
@ -1,515 +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.tabtray
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.widget.EditText
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.synthetic.main.component_tabstray.view.*
|
||||
import kotlinx.android.synthetic.main.component_tabstray_fab.view.*
|
||||
import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.*
|
||||
import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.view.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import mozilla.appservices.places.BookmarkRoot
|
||||
import mozilla.components.browser.state.selector.findTab
|
||||
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
|
||||
import mozilla.components.browser.state.selector.normalTabs
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.browser.thumbnails.loader.ThumbnailLoader
|
||||
import mozilla.components.feature.tab.collections.TabCollection
|
||||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
import mozilla.components.feature.tabs.tabstray.TabsFeature
|
||||
import mozilla.components.lib.state.ext.consumeFrom
|
||||
import mozilla.components.support.base.feature.UserInteractionHandler
|
||||
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
|
||||
import mozilla.components.support.ktx.android.view.showKeyboard
|
||||
import mozilla.components.support.utils.ext.bottom
|
||||
import mozilla.components.support.utils.ext.left
|
||||
import mozilla.components.support.utils.ext.right
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.NavGraphDirections
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.collections.CollectionsListAdapter
|
||||
import org.mozilla.fenix.components.FenixSnackbar
|
||||
import org.mozilla.fenix.components.StoreProvider
|
||||
import org.mozilla.fenix.components.TabCollectionStorage
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.getDefaultCollectionNumber
|
||||
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
|
||||
import org.mozilla.fenix.ext.metrics
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.home.HomeScreenViewModel
|
||||
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentState.Mode
|
||||
import org.mozilla.fenix.utils.allowUndo
|
||||
|
||||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||
class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
|
||||
private val args by navArgs<TabTrayDialogFragmentArgs>()
|
||||
|
||||
private val tabsFeature = ViewBoundFeatureWrapper<TabsFeature>()
|
||||
private var _tabTrayView: TabTrayView? = null
|
||||
private var currentOrientation: Int? = null
|
||||
private val tabTrayView: TabTrayView
|
||||
get() = _tabTrayView!!
|
||||
private lateinit var tabTrayDialogStore: TabTrayDialogFragmentStore
|
||||
|
||||
private val snackbarAnchor: View?
|
||||
get() =
|
||||
// Fab is hidden when Talkback is activated. See #16592
|
||||
if (requireContext().settings().accessibilityServicesEnabled) null
|
||||
else if (tabTrayView.fabView.new_tab_button.isVisible ||
|
||||
tabTrayView.mode != Mode.Normal
|
||||
) tabTrayView.fabView.new_tab_button
|
||||
/* During selection of the tabs to the collection, the FAB is not visible,
|
||||
which leads to not attaching a needed AnchorView. That's why, we're not only
|
||||
checking, if it's not visible, but also if we're not in a "Normal" mode, so after
|
||||
selecting tabs for a collection, we're pushing snackbar
|
||||
above the FAB, as we're switching from "Multiselect" to "Normal". */
|
||||
else null
|
||||
|
||||
private val collectionStorageObserver = object : TabCollectionStorage.Observer {
|
||||
override fun onCollectionCreated(title: String, sessions: List<TabSessionState>, id: Long?) {
|
||||
showCollectionSnackbar(sessions.size, true, collectionToSelect = id)
|
||||
}
|
||||
|
||||
override fun onTabsAdded(tabCollection: TabCollection, sessions: List<TabSessionState>) {
|
||||
showCollectionSnackbar(
|
||||
sessions.size,
|
||||
collectionToSelect = tabCollection.id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val selectTabUseCase = object : TabsUseCases.SelectTabUseCase {
|
||||
override fun invoke(tabId: String) {
|
||||
requireContext().components.analytics.metrics.track(Event.OpenedExistingTab)
|
||||
requireComponents.useCases.tabsUseCases.selectTab(tabId)
|
||||
navigateToBrowser()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return object : Dialog(requireContext(), this.theme) {
|
||||
override fun onBackPressed() {
|
||||
this@TabTrayDialogFragment.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val removeTabUseCase = object : TabsUseCases.RemoveTabUseCase {
|
||||
override fun invoke(sessionId: String) {
|
||||
requireContext().components.analytics.metrics.track(Event.ClosedExistingTab)
|
||||
showUndoSnackbarForTab(sessionId)
|
||||
removeIfNotLastTab(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeIfNotLastTab(sessionId: String) {
|
||||
// We only want to *immediately* remove a tab if there are more than one in the tab tray
|
||||
// If there is only one, the HomeFragment handles deleting the tab (to better support snackbars)
|
||||
val browserStore = requireComponents.core.store
|
||||
val sessionToRemove = browserStore.state.findTab(sessionId)
|
||||
sessionToRemove?.let {
|
||||
if (browserStore.state.getNormalOrPrivateTabs(it.content.private).size != 1) {
|
||||
requireComponents.useCases.tabsUseCases.removeTab(sessionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NO_TITLE, R.style.TabTrayDialogStyle)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
tabTrayDialogStore = StoreProvider.get(this) {
|
||||
TabTrayDialogFragmentStore(
|
||||
TabTrayDialogFragmentState(
|
||||
requireComponents.core.store.state,
|
||||
if (args.enterMultiselect) Mode.MultiSelect(setOf()) else Mode.Normal
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return inflater.inflate(R.layout.fragment_tab_tray_dialog, container, false)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
|
||||
val isLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
tabTrayView.setTopOffset(isLandscape)
|
||||
|
||||
if (newConfig.orientation != currentOrientation) {
|
||||
tabTrayView.dismissMenu()
|
||||
tabTrayView.updateBottomSheetBehavior()
|
||||
|
||||
if (requireContext().settings().gridTabView) {
|
||||
// Update the number of columns to use in the grid view when the screen
|
||||
// orientation changes.
|
||||
tabTrayView.updateTabsTrayLayout()
|
||||
}
|
||||
currentOrientation = newConfig.orientation
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Suppress("LongMethod")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val activity = activity as HomeActivity
|
||||
val isPrivate = activity.browsingModeManager.mode.isPrivate
|
||||
|
||||
val thumbnailLoader = ThumbnailLoader(requireContext().components.core.thumbnailStorage)
|
||||
val adapter = FenixTabsAdapter(requireContext(), thumbnailLoader)
|
||||
currentOrientation = resources.configuration.orientation
|
||||
|
||||
_tabTrayView = TabTrayView(
|
||||
view.tabLayout,
|
||||
adapter,
|
||||
interactor = TabTrayFragmentInteractor(
|
||||
DefaultTabTrayController(
|
||||
activity = activity,
|
||||
profiler = activity.components.core.engine.profiler,
|
||||
browserStore = activity.components.core.store,
|
||||
tabsUseCases = activity.components.useCases.tabsUseCases,
|
||||
ioScope = lifecycleScope + Dispatchers.IO,
|
||||
metrics = activity.components.analytics.metrics,
|
||||
browsingModeManager = activity.browsingModeManager,
|
||||
tabCollectionStorage = activity.components.core.tabCollectionStorage,
|
||||
bookmarksStorage = activity.components.core.bookmarksStorage,
|
||||
navController = findNavController(),
|
||||
dismissTabTray = ::dismissAllowingStateLoss,
|
||||
dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome,
|
||||
registerCollectionStorageObserver = ::registerCollectionStorageObserver,
|
||||
tabTrayDialogFragmentStore = tabTrayDialogStore,
|
||||
selectTabUseCase = selectTabUseCase,
|
||||
showChooseCollectionDialog = ::showChooseCollectionDialog,
|
||||
showAddNewCollectionDialog = ::showAddNewCollectionDialog,
|
||||
showUndoSnackbarForTabs = ::showUndoSnackbarForTabs,
|
||||
showBookmarksSnackbar = ::showBookmarksSnackbar
|
||||
)
|
||||
),
|
||||
isPrivate = isPrivate,
|
||||
isInLandscape = ::isInLandscape,
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
) { private ->
|
||||
val filter: (TabSessionState) -> Boolean = { state -> private == state.content.private }
|
||||
|
||||
tabsFeature.get()?.filterTabs(filter)
|
||||
|
||||
setSecureFlagsIfNeeded(private)
|
||||
}
|
||||
|
||||
tabsFeature.set(
|
||||
TabsFeature(
|
||||
adapter,
|
||||
view.context.components.core.store,
|
||||
selectTabUseCase,
|
||||
removeTabUseCase,
|
||||
{ it.content.private == isPrivate },
|
||||
{ }
|
||||
),
|
||||
owner = viewLifecycleOwner,
|
||||
view = view
|
||||
)
|
||||
|
||||
tabLayout.setOnClickListener {
|
||||
requireContext().components.analytics.metrics.track(Event.TabsTrayClosed)
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
view.tabLayout.setOnApplyWindowInsetsListener { v, insets ->
|
||||
// This will be addressed on https://github.com/mozilla-mobile/fenix/issues/17807
|
||||
@Suppress("DEPRECATION")
|
||||
v.updatePadding(
|
||||
left = insets.left(),
|
||||
right = insets.right(),
|
||||
bottom = insets.bottom()
|
||||
)
|
||||
|
||||
// This will be addressed on https://github.com/mozilla-mobile/fenix/issues/17807
|
||||
@Suppress("DEPRECATION")
|
||||
tabTrayView.view.tab_wrapper.updatePadding(
|
||||
bottom = insets.bottom()
|
||||
)
|
||||
|
||||
insets
|
||||
}
|
||||
|
||||
consumeFrom(requireComponents.core.store) {
|
||||
tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.BrowserStateChanged(it))
|
||||
}
|
||||
|
||||
consumeFrom(tabTrayDialogStore) {
|
||||
tabTrayView.updateState(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSecureFlagsIfNeeded(private: Boolean) {
|
||||
if (private && context?.settings()?.allowScreenshotsInPrivateMode == false) {
|
||||
dialog?.window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
} else {
|
||||
dialog?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUndoSnackbarForTabs() {
|
||||
lifecycleScope.allowUndo(
|
||||
requireView().tabLayout,
|
||||
getString(R.string.snackbar_message_tabs_closed),
|
||||
getString(R.string.snackbar_deleted_undo),
|
||||
{
|
||||
requireComponents.useCases.tabsUseCases.undo.invoke()
|
||||
},
|
||||
operation = { },
|
||||
elevation = ELEVATION,
|
||||
anchorView = snackbarAnchor
|
||||
)
|
||||
}
|
||||
|
||||
private fun showUndoSnackbarForTab(sessionId: String) {
|
||||
val store = requireComponents.core.store
|
||||
val tab = requireComponents.core.store.state.findTab(sessionId) ?: return
|
||||
|
||||
// Check if this is the last tab of this session type
|
||||
val isLastOpenTab =
|
||||
store.state.tabs.filter { it.content.private == tab.content.private }.size == 1
|
||||
if (isLastOpenTab) {
|
||||
dismissTabTrayAndNavigateHome(sessionId)
|
||||
return
|
||||
}
|
||||
|
||||
val snackbarMessage = if (tab.content.private) {
|
||||
getString(R.string.snackbar_private_tab_closed)
|
||||
} else {
|
||||
getString(R.string.snackbar_tab_closed)
|
||||
}
|
||||
|
||||
lifecycleScope.allowUndo(
|
||||
requireView().tabLayout,
|
||||
snackbarMessage,
|
||||
getString(R.string.snackbar_deleted_undo),
|
||||
{
|
||||
requireComponents.useCases.tabsUseCases.undo.invoke()
|
||||
_tabTrayView?.scrollToSelectedBrowserTab(tab.id)
|
||||
},
|
||||
operation = { },
|
||||
elevation = ELEVATION,
|
||||
anchorView = snackbarAnchor
|
||||
)
|
||||
}
|
||||
|
||||
private val homeViewModel: HomeScreenViewModel by activityViewModels()
|
||||
|
||||
private fun dismissTabTrayAndNavigateHome(sessionId: String) {
|
||||
homeViewModel.sessionToDelete = sessionId
|
||||
val directions = NavGraphDirections.actionGlobalHome()
|
||||
findNavController().navigateBlockingForAsyncNavGraph(directions)
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
_tabTrayView = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
fun navigateToBrowser() {
|
||||
dismissAllowingStateLoss()
|
||||
if (findNavController().currentDestination?.id == R.id.browserFragment) return
|
||||
if (!findNavController().popBackStack(R.id.browserFragment, false)) {
|
||||
findNavController().navigateBlockingForAsyncNavGraph(R.id.browserFragment)
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerCollectionStorageObserver() {
|
||||
requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this)
|
||||
}
|
||||
|
||||
private fun showCollectionSnackbar(
|
||||
tabSize: Int,
|
||||
isNewCollection: Boolean = false,
|
||||
collectionToSelect: Long?
|
||||
) {
|
||||
view.let {
|
||||
val messageStringRes = when {
|
||||
isNewCollection -> {
|
||||
R.string.create_collection_tabs_saved_new_collection
|
||||
}
|
||||
tabSize > 1 -> {
|
||||
R.string.create_collection_tabs_saved
|
||||
}
|
||||
else -> {
|
||||
R.string.create_collection_tab_saved
|
||||
}
|
||||
}
|
||||
val snackbar = FenixSnackbar
|
||||
.make(
|
||||
duration = FenixSnackbar.LENGTH_LONG,
|
||||
isDisplayedWithBrowserToolbar = true,
|
||||
view = (view as View)
|
||||
)
|
||||
.setAnchorView(snackbarAnchor)
|
||||
.setText(requireContext().getString(messageStringRes))
|
||||
.setAction(requireContext().getString(R.string.create_collection_view)) {
|
||||
dismissAllowingStateLoss()
|
||||
findNavController().navigateBlockingForAsyncNavGraph(
|
||||
TabTrayDialogFragmentDirections.actionGlobalHome(
|
||||
focusOnAddressBar = false,
|
||||
focusOnCollection = collectionToSelect ?: -1L
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
snackbar.view.elevation = ELEVATION
|
||||
snackbar.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showBookmarksSnackbar() {
|
||||
val snackbar = FenixSnackbar
|
||||
.make(
|
||||
duration = FenixSnackbar.LENGTH_LONG,
|
||||
isDisplayedWithBrowserToolbar = false,
|
||||
view = (view as View)
|
||||
)
|
||||
.setAnchorView(snackbarAnchor)
|
||||
.setText(requireContext().getString(R.string.snackbar_message_bookmarks_saved))
|
||||
.setAction(requireContext().getString(R.string.snackbar_message_bookmarks_view)) {
|
||||
dismissAllowingStateLoss()
|
||||
findNavController().navigateBlockingForAsyncNavGraph(
|
||||
TabTrayDialogFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id)
|
||||
)
|
||||
}
|
||||
|
||||
snackbar.view.elevation = ELEVATION
|
||||
snackbar.show()
|
||||
}
|
||||
|
||||
override fun onBackPressed(): Boolean {
|
||||
if (!tabTrayView.onBackPressed()) {
|
||||
dismiss()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun showChooseCollectionDialog(sessionList: List<TabSessionState>) {
|
||||
context?.let {
|
||||
val tabCollectionStorage = it.components.core.tabCollectionStorage
|
||||
val collections =
|
||||
tabCollectionStorage.cachedTabCollections.map { it.title }.toTypedArray()
|
||||
val customLayout =
|
||||
LayoutInflater.from(it).inflate(R.layout.add_new_collection_dialog, null)
|
||||
val list = customLayout.findViewById<RecyclerView>(R.id.recycler_view)
|
||||
list.layoutManager = LinearLayoutManager(it)
|
||||
|
||||
val builder = AlertDialog.Builder(it).setTitle(R.string.tab_tray_select_collection)
|
||||
.setView(customLayout)
|
||||
.setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
val selectedCollection =
|
||||
(list.adapter as CollectionsListAdapter).getSelectedCollection()
|
||||
val collection = tabCollectionStorage.cachedTabCollections[selectedCollection]
|
||||
viewLifecycleOwner.lifecycleScope.launch(Main) {
|
||||
tabCollectionStorage.addTabsToCollection(collection, sessionList)
|
||||
it.metrics.track(
|
||||
Event.CollectionTabsAdded(
|
||||
it.components.core.store.state.normalTabs.size,
|
||||
sessionList.size
|
||||
)
|
||||
)
|
||||
tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
|
||||
dialog.dismiss()
|
||||
}
|
||||
}.setNegativeButton(android.R.string.cancel) { dialog, _ ->
|
||||
tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
|
||||
dialog.cancel()
|
||||
}
|
||||
|
||||
val dialog = builder.create()
|
||||
val adapter =
|
||||
CollectionsListAdapter(arrayOf(it.getString(R.string.tab_tray_add_new_collection)) + collections) {
|
||||
dialog.dismiss()
|
||||
showAddNewCollectionDialog(sessionList)
|
||||
}
|
||||
list.adapter = adapter
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAddNewCollectionDialog(sessionList: List<TabSessionState>) {
|
||||
context?.let {
|
||||
val tabCollectionStorage = it.components.core.tabCollectionStorage
|
||||
val customLayout =
|
||||
LayoutInflater.from(it).inflate(R.layout.name_collection_dialog, null)
|
||||
val collectionNameEditText: EditText =
|
||||
customLayout.findViewById(R.id.collection_name)
|
||||
collectionNameEditText.setText(
|
||||
it.getString(
|
||||
R.string.create_collection_default_name,
|
||||
tabCollectionStorage.cachedTabCollections.getDefaultCollectionNumber()
|
||||
)
|
||||
)
|
||||
|
||||
AlertDialog.Builder(it).setTitle(R.string.tab_tray_add_new_collection)
|
||||
.setView(customLayout).setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
tabCollectionStorage.createCollection(
|
||||
collectionNameEditText.text.toString(),
|
||||
sessionList
|
||||
)
|
||||
it.metrics.track(
|
||||
Event.CollectionSaved(
|
||||
it.components.core.store.state.normalTabs.size,
|
||||
sessionList.size
|
||||
)
|
||||
)
|
||||
launch(Main) {
|
||||
tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}.setNegativeButton(android.R.string.cancel) { dialog, _ ->
|
||||
tabTrayDialogStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
|
||||
dialog.cancel()
|
||||
}.create().show().also {
|
||||
collectionNameEditText.setSelection(0, collectionNameEditText.text.length)
|
||||
collectionNameEditText.showKeyboard()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isInLandscape(): Boolean {
|
||||
return requireContext().resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ELEVATION = 80f
|
||||
}
|
||||
}
|
@ -1,76 +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.tabtray
|
||||
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import mozilla.components.lib.state.Action
|
||||
import mozilla.components.lib.state.State
|
||||
import mozilla.components.lib.state.Store
|
||||
|
||||
/**
|
||||
* The [Store] for holding the [TabTrayDialogFragmentState] and
|
||||
* applying [TabTrayDialogFragmentAction]s.
|
||||
*/
|
||||
class TabTrayDialogFragmentStore(initialState: TabTrayDialogFragmentState) :
|
||||
Store<TabTrayDialogFragmentState, TabTrayDialogFragmentAction>(
|
||||
initialState,
|
||||
::tabTrayStateReducer
|
||||
)
|
||||
|
||||
/**
|
||||
* Actions to dispatch through the `TabTrayDialogFragmentStore` to modify
|
||||
* `TabTrayDialogFragmentState` through the reducer.
|
||||
*/
|
||||
sealed class TabTrayDialogFragmentAction : Action {
|
||||
data class BrowserStateChanged(val browserState: BrowserState) : TabTrayDialogFragmentAction()
|
||||
object EnterMultiSelectMode : TabTrayDialogFragmentAction()
|
||||
object ExitMultiSelectMode : TabTrayDialogFragmentAction()
|
||||
data class AddItemForCollection(val item: Tab) : TabTrayDialogFragmentAction()
|
||||
data class RemoveItemForCollection(val item: Tab) : TabTrayDialogFragmentAction()
|
||||
}
|
||||
|
||||
/**
|
||||
* The state for the Tab Tray Dialog Screen
|
||||
* @property mode Current Mode of Multiselection
|
||||
*/
|
||||
data class TabTrayDialogFragmentState(val browserState: BrowserState, val mode: Mode) : State {
|
||||
sealed class Mode {
|
||||
open val selectedItems = emptySet<Tab>()
|
||||
|
||||
object Normal : Mode()
|
||||
data class MultiSelect(override val selectedItems: Set<Tab>) : Mode()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The TabTrayDialogFragmentState Reducer.
|
||||
*/
|
||||
private fun tabTrayStateReducer(
|
||||
state: TabTrayDialogFragmentState,
|
||||
action: TabTrayDialogFragmentAction
|
||||
): TabTrayDialogFragmentState {
|
||||
return when (action) {
|
||||
is TabTrayDialogFragmentAction.BrowserStateChanged -> state.copy(browserState = action.browserState)
|
||||
is TabTrayDialogFragmentAction.AddItemForCollection ->
|
||||
state.copy(mode = TabTrayDialogFragmentState.Mode.MultiSelect(state.mode.selectedItems + action.item))
|
||||
is TabTrayDialogFragmentAction.RemoveItemForCollection -> {
|
||||
val selected = state.mode.selectedItems - action.item
|
||||
state.copy(
|
||||
mode = if (selected.isEmpty()) {
|
||||
TabTrayDialogFragmentState.Mode.Normal
|
||||
} else {
|
||||
TabTrayDialogFragmentState.Mode.MultiSelect(selected)
|
||||
}
|
||||
)
|
||||
}
|
||||
is TabTrayDialogFragmentAction.ExitMultiSelectMode -> state.copy(mode = TabTrayDialogFragmentState.Mode.Normal)
|
||||
is TabTrayDialogFragmentAction.EnterMultiSelectMode -> state.copy(
|
||||
mode = TabTrayDialogFragmentState.Mode.MultiSelect(
|
||||
setOf()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
@ -1,170 +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.tabtray
|
||||
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
interface TabTrayInteractor {
|
||||
/**
|
||||
* Called when user clicks the new tab button.
|
||||
*/
|
||||
fun onNewTabTapped(private: Boolean)
|
||||
|
||||
/**
|
||||
* Called when tab tray should be dismissed.
|
||||
*/
|
||||
fun onTabTrayDismissed()
|
||||
|
||||
/**
|
||||
* Called when user clicks the share tabs button.
|
||||
*/
|
||||
fun onShareTabsOfTypeClicked(private: Boolean)
|
||||
|
||||
/**
|
||||
* Called when user clicks button to share selected tabs in multiselect.
|
||||
*/
|
||||
fun onShareSelectedTabsClicked(selectedTabs: Set<Tab>)
|
||||
|
||||
/**
|
||||
* Called when user clicks bookmark button in menu to bookmark selected tabs in multiselect.
|
||||
*/
|
||||
fun onBookmarkSelectedTabs(selectedTabs: Set<Tab>)
|
||||
|
||||
/**
|
||||
* Called when user clicks delete button in menu to delete selected tabs in multiselect.
|
||||
*/
|
||||
fun onDeleteSelectedTabs(selectedTabs: Set<Tab>)
|
||||
|
||||
/**
|
||||
* Called when user clicks the tab settings button.
|
||||
*/
|
||||
fun onTabSettingsClicked()
|
||||
|
||||
/**
|
||||
* Called when user clicks button to save selected tabs to a collection.
|
||||
*/
|
||||
fun onSaveToCollectionClicked(selectedTabs: Set<Tab>)
|
||||
|
||||
/**
|
||||
* Called when user clicks the close all tabs button.
|
||||
*/
|
||||
fun onCloseAllTabsClicked(private: Boolean)
|
||||
|
||||
/**
|
||||
* Called when the physical back button is clicked.
|
||||
*/
|
||||
fun onBackPressed(): Boolean
|
||||
|
||||
/**
|
||||
* Called when a requester needs to know the current mode of the tab tray.
|
||||
*/
|
||||
fun onModeRequested(): TabTrayDialogFragmentState.Mode
|
||||
|
||||
/**
|
||||
* Called when user clicks on the action button prompt in the info banner CFR for
|
||||
* automatically closing tabs or changing the layout of open tabs.
|
||||
*/
|
||||
fun onGoToTabsSettings()
|
||||
|
||||
/**
|
||||
* Called when a tab should be opened in the browser.
|
||||
*/
|
||||
fun onOpenTab(tab: Tab)
|
||||
|
||||
/**
|
||||
* Called when a tab should be selected in multiselect mode.
|
||||
*/
|
||||
fun onAddSelectedTab(tab: Tab)
|
||||
|
||||
/**
|
||||
* Called when a tab should be unselected in multiselect mode.
|
||||
*/
|
||||
fun onRemoveSelectedTab(tab: Tab)
|
||||
|
||||
/**
|
||||
* Called when multiselect mode should be entered with no tabs selected.
|
||||
*/
|
||||
fun onEnterMultiselect()
|
||||
|
||||
/**
|
||||
* Called when user clicks the recently closed tabs menu button.
|
||||
*/
|
||||
fun onOpenRecentlyClosedClicked()
|
||||
}
|
||||
|
||||
/**
|
||||
* Interactor for the tab tray fragment.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class TabTrayFragmentInteractor(private val controller: TabTrayController) : TabTrayInteractor {
|
||||
override fun onNewTabTapped(private: Boolean) {
|
||||
controller.handleNewTabTapped(private)
|
||||
}
|
||||
|
||||
override fun onTabTrayDismissed() {
|
||||
controller.handleTabTrayDismissed()
|
||||
}
|
||||
|
||||
override fun onTabSettingsClicked() {
|
||||
controller.handleTabSettingsClicked()
|
||||
}
|
||||
|
||||
override fun onOpenRecentlyClosedClicked() {
|
||||
controller.handleRecentlyClosedClicked()
|
||||
}
|
||||
|
||||
override fun onShareTabsOfTypeClicked(private: Boolean) {
|
||||
controller.handleShareTabsOfTypeClicked(private)
|
||||
}
|
||||
|
||||
override fun onShareSelectedTabsClicked(selectedTabs: Set<Tab>) {
|
||||
controller.handleShareSelectedTabsClicked(selectedTabs)
|
||||
}
|
||||
|
||||
override fun onBookmarkSelectedTabs(selectedTabs: Set<Tab>) {
|
||||
controller.handleBookmarkSelectedTabs(selectedTabs)
|
||||
}
|
||||
|
||||
override fun onDeleteSelectedTabs(selectedTabs: Set<Tab>) {
|
||||
controller.handleDeleteSelectedTabs(selectedTabs)
|
||||
}
|
||||
|
||||
override fun onSaveToCollectionClicked(selectedTabs: Set<Tab>) {
|
||||
controller.handleSaveToCollectionClicked(selectedTabs)
|
||||
}
|
||||
|
||||
override fun onCloseAllTabsClicked(private: Boolean) {
|
||||
controller.handleCloseAllTabsClicked(private)
|
||||
}
|
||||
|
||||
override fun onBackPressed(): Boolean {
|
||||
return controller.handleBackPressed()
|
||||
}
|
||||
|
||||
override fun onModeRequested(): TabTrayDialogFragmentState.Mode {
|
||||
return controller.onModeRequested()
|
||||
}
|
||||
|
||||
override fun onAddSelectedTab(tab: Tab) {
|
||||
controller.handleAddSelectedTab(tab)
|
||||
}
|
||||
|
||||
override fun onRemoveSelectedTab(tab: Tab) {
|
||||
controller.handleRemoveSelectedTab(tab)
|
||||
}
|
||||
|
||||
override fun onOpenTab(tab: Tab) {
|
||||
controller.handleOpenTab(tab)
|
||||
}
|
||||
|
||||
override fun onEnterMultiselect() {
|
||||
controller.handleEnterMultiselect()
|
||||
}
|
||||
|
||||
override fun onGoToTabsSettings() {
|
||||
controller.handleGoToTabsSettingClicked()
|
||||
}
|
||||
}
|
@ -1,72 +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.tabtray
|
||||
|
||||
import android.content.Context
|
||||
import mozilla.components.browser.menu.BrowserMenuBuilder
|
||||
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.components
|
||||
|
||||
class TabTrayItemMenu(
|
||||
private val context: Context,
|
||||
private val shouldShowShareAllTabs: () -> Boolean,
|
||||
private val shouldShowSelectTabs: () -> Boolean,
|
||||
private val hasOpenTabs: () -> Boolean,
|
||||
private val onItemTapped: (Item) -> Unit = {}
|
||||
) {
|
||||
|
||||
sealed class Item {
|
||||
object ShareAllTabs : Item()
|
||||
object OpenTabSettings : Item()
|
||||
object SelectTabs : Item()
|
||||
object CloseAllTabs : Item()
|
||||
object OpenRecentlyClosed : Item()
|
||||
}
|
||||
|
||||
val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
|
||||
|
||||
private val menuItems by lazy {
|
||||
listOf(
|
||||
SimpleBrowserMenuItem(
|
||||
context.getString(R.string.tabs_tray_select_tabs),
|
||||
textColorResource = R.color.primary_text_normal_theme
|
||||
) {
|
||||
onItemTapped.invoke(Item.SelectTabs)
|
||||
}.apply { visible = shouldShowSelectTabs },
|
||||
|
||||
SimpleBrowserMenuItem(
|
||||
context.getString(R.string.tab_tray_menu_item_share),
|
||||
textColorResource = R.color.primary_text_normal_theme
|
||||
) {
|
||||
context.components.analytics.metrics.track(Event.TabsTrayShareAllTabsPressed)
|
||||
onItemTapped.invoke(Item.ShareAllTabs)
|
||||
}.apply { visible = { shouldShowShareAllTabs() && hasOpenTabs() } },
|
||||
|
||||
SimpleBrowserMenuItem(
|
||||
context.getString(R.string.tab_tray_menu_tab_settings),
|
||||
textColorResource = R.color.primary_text_normal_theme
|
||||
) {
|
||||
onItemTapped.invoke(Item.OpenTabSettings)
|
||||
},
|
||||
|
||||
SimpleBrowserMenuItem(
|
||||
context.getString(R.string.tab_tray_menu_recently_closed),
|
||||
textColorResource = R.color.primary_text_normal_theme
|
||||
) {
|
||||
onItemTapped.invoke(Item.OpenRecentlyClosed)
|
||||
},
|
||||
|
||||
SimpleBrowserMenuItem(
|
||||
context.getString(R.string.tab_tray_menu_item_close),
|
||||
textColorResource = R.color.primary_text_normal_theme
|
||||
) {
|
||||
context.components.analytics.metrics.track(Event.TabsTrayCloseAllTabsPressed)
|
||||
onItemTapped.invoke(Item.CloseAllTabs)
|
||||
}.apply { visible = hasOpenTabs }
|
||||
)
|
||||
}
|
||||
}
|
@ -1,744 +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.tabtray
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.INVISIBLE
|
||||
import android.view.View.VISIBLE
|
||||
import android.view.ViewGroup
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import kotlinx.android.synthetic.main.component_tabstray.view.*
|
||||
import kotlinx.android.synthetic.main.component_tabstray_fab.view.*
|
||||
import kotlinx.android.synthetic.main.tabs_tray_tab_counter.*
|
||||
import kotlinx.android.synthetic.main.tabstray_multiselect_items.view.*
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.browser.menu.BrowserMenu
|
||||
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
|
||||
import mozilla.components.browser.state.selector.normalTabs
|
||||
import mozilla.components.browser.state.selector.privateTabs
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.browser.tabstray.TabViewHolder
|
||||
import mozilla.components.support.ktx.android.util.dpToPx
|
||||
import mozilla.components.ui.tabcounter.TabCounter.Companion.INFINITE_CHAR_PADDING_BOTTOM
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import mozilla.components.ui.tabcounter.TabCounter.Companion.MAX_VISIBLE_TABS
|
||||
import mozilla.components.ui.tabcounter.TabCounter.Companion.SO_MANY_TABS_OPEN
|
||||
import org.mozilla.fenix.browser.infobanner.InfoBanner
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.ext.updateAccessibilityCollectionInfo
|
||||
import org.mozilla.fenix.tabtray.SaveToCollectionsButtonAdapter.MultiselectModeChange
|
||||
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentState.Mode
|
||||
import org.mozilla.fenix.utils.Settings
|
||||
import java.text.NumberFormat
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* View that contains and configures the BrowserAwesomeBar
|
||||
*/
|
||||
@Suppress("LongParameterList", "TooManyFunctions", "LargeClass", "ForbiddenComment")
|
||||
class TabTrayView(
|
||||
private val container: ViewGroup,
|
||||
private val tabsAdapter: FenixTabsAdapter,
|
||||
private val interactor: TabTrayInteractor,
|
||||
isPrivate: Boolean,
|
||||
private val isInLandscape: () -> Boolean,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
private val filterTabs: (Boolean) -> Unit
|
||||
) : LayoutContainer, TabLayout.OnTabSelectedListener {
|
||||
val lifecycleScope = lifecycleOwner.lifecycleScope
|
||||
val fabView = LayoutInflater.from(container.context)
|
||||
.inflate(R.layout.component_tabstray_fab, container, true)
|
||||
|
||||
private val hasAccessibilityEnabled = container.context.settings().accessibilityServicesEnabled
|
||||
|
||||
val view = LayoutInflater.from(container.context)
|
||||
.inflate(R.layout.component_tabstray, container, true)
|
||||
|
||||
private val isPrivateModeSelected: Boolean get() = view.tab_layout.selectedTabPosition == PRIVATE_TAB_ID
|
||||
|
||||
private val behavior = BottomSheetBehavior.from(view.tab_wrapper)
|
||||
|
||||
private val concatAdapter = ConcatAdapter(tabsAdapter)
|
||||
private val tabTrayItemMenu: TabTrayItemMenu
|
||||
private var menu: BrowserMenu? = null
|
||||
|
||||
private val multiselectSelectionMenu: MultiselectSelectionMenu
|
||||
private var multiselectMenu: BrowserMenu? = null
|
||||
|
||||
private var tabsTouchHelper: TabsTouchHelper
|
||||
private val collectionsButtonAdapter =
|
||||
SaveToCollectionsButtonAdapter(interactor) { isPrivateModeSelected }
|
||||
|
||||
private var hasLoaded = false
|
||||
|
||||
override val containerView: View?
|
||||
get() = container
|
||||
|
||||
private val components = container.context.components
|
||||
|
||||
private val checkOpenTabs = {
|
||||
if (isPrivateModeSelected) {
|
||||
view.context.components.core.store.state.privateTabs.isNotEmpty()
|
||||
} else {
|
||||
view.context.components.core.store.state.normalTabs.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
components.analytics.metrics.track(Event.TabsTrayOpened)
|
||||
|
||||
toggleFabText(isPrivate)
|
||||
|
||||
view.topBar.setOnClickListener {
|
||||
// no-op, consume the touch event to prevent it advancing the tray to the next state.
|
||||
}
|
||||
behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
if (
|
||||
interactor.onModeRequested() is Mode.Normal &&
|
||||
!hasAccessibilityEnabled &&
|
||||
slideOffset >= SLIDE_OFFSET
|
||||
) {
|
||||
fabView.new_tab_button.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
|
||||
components.analytics.metrics.track(Event.TabsTrayClosed)
|
||||
interactor.onTabTrayDismissed()
|
||||
}
|
||||
// We only support expanded and collapsed states. Don't allow STATE_HALF_EXPANDED.
|
||||
else if (newState == BottomSheetBehavior.STATE_HALF_EXPANDED) {
|
||||
behavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
val selectedTabIndex = if (!isPrivate) {
|
||||
DEFAULT_TAB_ID
|
||||
} else {
|
||||
PRIVATE_TAB_ID
|
||||
}
|
||||
|
||||
view.tab_layout.getTabAt(selectedTabIndex)?.also {
|
||||
view.tab_layout.selectTab(it, true)
|
||||
}
|
||||
|
||||
view.tab_layout.addOnTabSelectedListener(this)
|
||||
|
||||
val tabs = getTabs(isPrivate)
|
||||
|
||||
updateBottomSheetBehavior()
|
||||
|
||||
setTopOffset(isInLandscape())
|
||||
|
||||
updateTabsTrayLayout()
|
||||
|
||||
view.tabsTray.apply {
|
||||
adapter = concatAdapter
|
||||
|
||||
tabsTouchHelper = TabsTouchHelper(
|
||||
observable = tabsAdapter,
|
||||
onViewHolderTouched = { it is TabViewHolder }
|
||||
)
|
||||
|
||||
tabsTouchHelper.attachToRecyclerView(this)
|
||||
|
||||
tabsAdapter.tabTrayInteractor = interactor
|
||||
tabsAdapter.onTabsUpdated = {
|
||||
concatAdapter.addAdapter(collectionsButtonAdapter)
|
||||
|
||||
if (hasAccessibilityEnabled) {
|
||||
tabsAdapter.notifyItemRangeChanged(0, tabs.size)
|
||||
}
|
||||
if (!hasLoaded) {
|
||||
hasLoaded = true
|
||||
scrollToSelectedBrowserTab()
|
||||
if (view.context.settings().accessibilityServicesEnabled) {
|
||||
lifecycleScope.launch {
|
||||
delay(SELECTION_DELAY.toLong())
|
||||
lifecycleScope.launch(Main) {
|
||||
layoutManager?.findViewByPosition(getSelectedBrowserTabViewIndex())
|
||||
?.requestFocus()
|
||||
layoutManager?.findViewByPosition(getSelectedBrowserTabViewIndex())
|
||||
?.sendAccessibilityEvent(
|
||||
AccessibilityEvent.TYPE_VIEW_FOCUSED
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tabTrayItemMenu =
|
||||
TabTrayItemMenu(
|
||||
context = view.context,
|
||||
shouldShowShareAllTabs = { checkOpenTabs.invoke() && view.tab_layout.selectedTabPosition == 0 },
|
||||
shouldShowSelectTabs = { checkOpenTabs.invoke() && view.tab_layout.selectedTabPosition == 0 },
|
||||
hasOpenTabs = checkOpenTabs
|
||||
) {
|
||||
when (it) {
|
||||
is TabTrayItemMenu.Item.ShareAllTabs -> interactor.onShareTabsOfTypeClicked(
|
||||
isPrivateModeSelected
|
||||
)
|
||||
is TabTrayItemMenu.Item.OpenTabSettings -> interactor.onTabSettingsClicked()
|
||||
is TabTrayItemMenu.Item.SelectTabs -> interactor.onEnterMultiselect()
|
||||
is TabTrayItemMenu.Item.CloseAllTabs -> interactor.onCloseAllTabsClicked(
|
||||
isPrivateModeSelected
|
||||
)
|
||||
is TabTrayItemMenu.Item.OpenRecentlyClosed -> interactor.onOpenRecentlyClosedClicked()
|
||||
}
|
||||
}
|
||||
|
||||
multiselectSelectionMenu = MultiselectSelectionMenu(
|
||||
context = view.context
|
||||
) {
|
||||
when (it) {
|
||||
is MultiselectSelectionMenu.Item.BookmarkTabs -> interactor.onBookmarkSelectedTabs(
|
||||
mode.selectedItems
|
||||
)
|
||||
is MultiselectSelectionMenu.Item.DeleteTabs -> interactor.onDeleteSelectedTabs(
|
||||
mode.selectedItems
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
view.tab_tray_overflow.setOnClickListener {
|
||||
components.analytics.metrics.track(Event.TabsTrayMenuOpened)
|
||||
menu = tabTrayItemMenu.menuBuilder.build(container.context)
|
||||
menu?.show(it)?.also { popupMenu ->
|
||||
(popupMenu.contentView as? CardView)?.setCardBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
view.context,
|
||||
R.color.foundation_normal_theme
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
adjustNewTabButtonsForNormalMode()
|
||||
|
||||
displayInfoBannerIfNeccessary(tabs, view.context.settings())
|
||||
}
|
||||
|
||||
private fun displayInfoBannerIfNeccessary(tabs: List<TabSessionState>, settings: Settings) {
|
||||
@Suppress("ComplexCondition")
|
||||
val infoBanner = if (
|
||||
settings.shouldShowGridViewBanner &&
|
||||
settings.canShowCfr &&
|
||||
settings.listTabView &&
|
||||
tabs.size >= TAB_COUNT_SHOW_CFR
|
||||
) {
|
||||
InfoBanner(
|
||||
context = view.context,
|
||||
message = view.context.getString(R.string.tab_tray_grid_view_banner_message),
|
||||
dismissText = view.context.getString(R.string.tab_tray_grid_view_banner_negative_button_text),
|
||||
actionText = view.context.getString(R.string.tab_tray_grid_view_banner_positive_button_text),
|
||||
container = view.infoBanner,
|
||||
dismissByHiding = true,
|
||||
dismissAction = {
|
||||
components.analytics.metrics.track(Event.TabsTrayCfrDismissed)
|
||||
settings.shouldShowGridViewBanner = false
|
||||
}
|
||||
) {
|
||||
interactor.onGoToTabsSettings()
|
||||
settings.shouldShowGridViewBanner = false
|
||||
}
|
||||
} else if (
|
||||
settings.shouldShowAutoCloseTabsBanner &&
|
||||
settings.canShowCfr &&
|
||||
tabs.size >= TAB_COUNT_SHOW_CFR
|
||||
) {
|
||||
InfoBanner(
|
||||
context = view.context,
|
||||
message = view.context.getString(R.string.tab_tray_close_tabs_banner_message),
|
||||
dismissText = view.context.getString(R.string.tab_tray_close_tabs_banner_negative_button_text),
|
||||
actionText = view.context.getString(R.string.tab_tray_close_tabs_banner_positive_button_text),
|
||||
container = view.infoBanner,
|
||||
dismissByHiding = true,
|
||||
dismissAction = { settings.shouldShowAutoCloseTabsBanner = false }
|
||||
) {
|
||||
interactor.onGoToTabsSettings()
|
||||
settings.shouldShowAutoCloseTabsBanner = false
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
infoBanner?.apply {
|
||||
view.infoBanner.visibility = VISIBLE
|
||||
showBanner()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTabs(isPrivate: Boolean): List<TabSessionState> = if (isPrivate) {
|
||||
view.context.components.core.store.state.privateTabs
|
||||
} else {
|
||||
view.context.components.core.store.state.normalTabs
|
||||
}
|
||||
|
||||
private fun getTabsNumberInAnyMode(): Int {
|
||||
return max(
|
||||
view.context.components.core.store.state.normalTabs.size,
|
||||
view.context.components.core.store.state.privateTabs.size
|
||||
)
|
||||
}
|
||||
|
||||
private fun getTabsNumberForExpandingTray(): Int {
|
||||
return if (container.context.settings().gridTabView) {
|
||||
EXPAND_AT_GRID_SIZE
|
||||
} else {
|
||||
EXPAND_AT_LIST_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
private fun adjustNewTabButtonsForNormalMode() {
|
||||
view.tab_tray_new_tab.apply {
|
||||
isVisible = hasAccessibilityEnabled
|
||||
setOnClickListener {
|
||||
sendNewTabEvent(isPrivateModeSelected)
|
||||
interactor.onNewTabTapped(isPrivateModeSelected)
|
||||
}
|
||||
}
|
||||
|
||||
fabView.new_tab_button.apply {
|
||||
isVisible = !hasAccessibilityEnabled
|
||||
setOnClickListener {
|
||||
sendNewTabEvent(isPrivateModeSelected)
|
||||
interactor.onNewTabTapped(isPrivateModeSelected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendNewTabEvent(isPrivateModeSelected: Boolean) {
|
||||
val eventToSend = if (isPrivateModeSelected) {
|
||||
Event.NewPrivateTabTapped
|
||||
} else {
|
||||
Event.NewTabTapped
|
||||
}
|
||||
|
||||
components.analytics.metrics.track(eventToSend)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the bottom sheet height based on the number tabs or screen orientation.
|
||||
* Show the bottom sheet fully expanded if it is in landscape mode or the number of
|
||||
* tabs are greater or equal to the expand size limit.
|
||||
*/
|
||||
fun updateBottomSheetBehavior() {
|
||||
if (isInLandscape() || getTabsNumberInAnyMode() >= getTabsNumberForExpandingTray()) {
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
} else {
|
||||
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
}
|
||||
}
|
||||
|
||||
enum class TabChange {
|
||||
PRIVATE, NORMAL
|
||||
}
|
||||
|
||||
private fun toggleSaveToCollectionButton(isPrivate: Boolean) {
|
||||
collectionsButtonAdapter.notifyItemChanged(
|
||||
0,
|
||||
if (isPrivate) TabChange.PRIVATE else TabChange.NORMAL
|
||||
)
|
||||
}
|
||||
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
toggleFabText(isPrivateModeSelected)
|
||||
filterTabs.invoke(isPrivateModeSelected)
|
||||
toggleSaveToCollectionButton(isPrivateModeSelected)
|
||||
|
||||
updateUINormalMode(view.context.components.core.store.state)
|
||||
scrollToSelectedBrowserTab()
|
||||
|
||||
if (isPrivateModeSelected) {
|
||||
components.analytics.metrics.track(Event.TabsTrayPrivateModeTapped)
|
||||
} else {
|
||||
components.analytics.metrics.track(Event.TabsTrayNormalModeTapped)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) = Unit
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
|
||||
|
||||
var mode: Mode = Mode.Normal
|
||||
private set
|
||||
|
||||
fun updateTabsTrayLayout() {
|
||||
if (container.context.settings().gridTabView) {
|
||||
setupGridTabView()
|
||||
} else {
|
||||
setupListTabView()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupGridTabView() {
|
||||
view.tabsTray.apply {
|
||||
val gridLayoutManager =
|
||||
GridLayoutManager(container.context, getNumberOfGridColumns(container.context))
|
||||
|
||||
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
val numTabs = tabsAdapter.itemCount
|
||||
return if (position < numTabs) {
|
||||
1
|
||||
} else {
|
||||
getNumberOfGridColumns(container.context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layoutManager = gridLayoutManager
|
||||
|
||||
// Ensure items have the same all around padding - 16 dp. Avoid the double spacing issue.
|
||||
// A 8dp padding is already set in xml, pad the parent with the remaining needed 8dp.
|
||||
updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
val padding = GRID_ITEM_PARENT_PADDING.dpToPx(resources.displayMetrics)
|
||||
// Account for the already set bottom padding needed to accommodate the fab.
|
||||
val bottomPadding = paddingBottom + padding
|
||||
setPadding(padding, padding, padding, bottomPadding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of columns that will fit in the grid layout for the current screen.
|
||||
*/
|
||||
private fun getNumberOfGridColumns(context: Context): Int {
|
||||
val displayMetrics = context.resources.displayMetrics
|
||||
val screenWidthDp = displayMetrics.widthPixels / displayMetrics.density
|
||||
val columnCount = (screenWidthDp / COLUMN_WIDTH_DP).toInt()
|
||||
return if (columnCount >= 2) columnCount else 2
|
||||
}
|
||||
|
||||
private fun setupListTabView() {
|
||||
view.tabsTray.apply {
|
||||
layoutManager = LinearLayoutManager(container.context)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateState(state: TabTrayDialogFragmentState) {
|
||||
val oldMode = mode
|
||||
|
||||
if (oldMode::class != state.mode::class) {
|
||||
updateTabsForMultiselectModeChanged(state.mode is Mode.MultiSelect)
|
||||
if (view.context.settings().accessibilityServicesEnabled) {
|
||||
view.announceForAccessibility(
|
||||
if (state.mode == Mode.Normal) view.context.getString(
|
||||
R.string.tab_tray_exit_multiselect_content_description
|
||||
) else view.context.getString(R.string.tab_tray_enter_multiselect_content_description)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
mode = state.mode
|
||||
when (state.mode) {
|
||||
Mode.Normal -> {
|
||||
view.tabsTray.apply {
|
||||
tabsTouchHelper.attachToRecyclerView(this)
|
||||
}
|
||||
|
||||
toggleUIMultiselect(multiselect = false)
|
||||
|
||||
updateUINormalMode(state.browserState)
|
||||
}
|
||||
is Mode.MultiSelect -> {
|
||||
// Disable swipe to delete while in multiselect
|
||||
tabsTouchHelper.attachToRecyclerView(null)
|
||||
|
||||
toggleUIMultiselect(multiselect = true)
|
||||
|
||||
fabView.new_tab_button.isVisible = false
|
||||
view.tab_tray_new_tab.isVisible = false
|
||||
view.collect_multi_select.isVisible = state.mode.selectedItems.isNotEmpty()
|
||||
view.share_multi_select.isVisible = state.mode.selectedItems.isNotEmpty()
|
||||
view.menu_multi_select.isVisible = state.mode.selectedItems.isNotEmpty()
|
||||
|
||||
view.multiselect_title.text = view.context.getString(
|
||||
R.string.tab_tray_multi_select_title,
|
||||
state.mode.selectedItems.size
|
||||
)
|
||||
view.collect_multi_select.setOnClickListener {
|
||||
interactor.onSaveToCollectionClicked(state.mode.selectedItems)
|
||||
}
|
||||
view.share_multi_select.setOnClickListener {
|
||||
interactor.onShareSelectedTabsClicked(state.mode.selectedItems)
|
||||
}
|
||||
view.menu_multi_select.setOnClickListener {
|
||||
multiselectMenu = multiselectSelectionMenu.menuBuilder.build(container.context)
|
||||
multiselectMenu?.show(it)?.also { popupMenu ->
|
||||
(popupMenu.contentView as? CardView)?.setCardBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
view.context,
|
||||
R.color.foundation_normal_theme
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
view.exit_multi_select.setOnClickListener {
|
||||
interactor.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (oldMode.selectedItems != state.mode.selectedItems) {
|
||||
val unselectedItems = oldMode.selectedItems - state.mode.selectedItems
|
||||
|
||||
state.mode.selectedItems.union(unselectedItems).forEach { item ->
|
||||
if (view.context.settings().accessibilityServicesEnabled) {
|
||||
view.announceForAccessibility(
|
||||
if (unselectedItems.contains(item)) view.context.getString(
|
||||
R.string.tab_tray_item_unselected_multiselect_content_description,
|
||||
item.title
|
||||
) else view.context.getString(
|
||||
R.string.tab_tray_item_selected_multiselect_content_description,
|
||||
item.title
|
||||
)
|
||||
)
|
||||
}
|
||||
updateTabsForSelectionChanged(item.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ConstraintLayout.setChildWPercent(percentage: Float, @IdRes childId: Int) {
|
||||
this.findViewById<View>(childId)?.let {
|
||||
val constraintSet = ConstraintSet()
|
||||
constraintSet.clone(this)
|
||||
constraintSet.constrainPercentWidth(it.id, percentage)
|
||||
constraintSet.applyTo(this)
|
||||
it.requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUINormalMode(browserState: BrowserState) {
|
||||
val hasNoTabs = if (isPrivateModeSelected) {
|
||||
browserState.privateTabs.isEmpty()
|
||||
} else {
|
||||
browserState.normalTabs.isEmpty()
|
||||
}
|
||||
|
||||
view.tab_tray_empty_view.isVisible = hasNoTabs
|
||||
if (hasNoTabs) {
|
||||
view.tab_tray_empty_view.text = if (isPrivateModeSelected) {
|
||||
view.context.getString(R.string.no_private_tabs_description)
|
||||
} else {
|
||||
view.context?.getString(R.string.no_open_tabs_description)
|
||||
}
|
||||
}
|
||||
|
||||
view.tabsTray.visibility = if (hasNoTabs) {
|
||||
INVISIBLE
|
||||
} else {
|
||||
VISIBLE
|
||||
}
|
||||
|
||||
counter_text.text = updateTabCounter(browserState.normalTabs.size)
|
||||
updateTabTrayViewAccessibility(browserState.normalTabs.size)
|
||||
|
||||
adjustNewTabButtonsForNormalMode()
|
||||
}
|
||||
|
||||
private fun toggleUIMultiselect(multiselect: Boolean) {
|
||||
view.multiselect_title.isVisible = multiselect
|
||||
view.collect_multi_select.isVisible = multiselect
|
||||
view.share_multi_select.isVisible = multiselect
|
||||
view.menu_multi_select.isVisible = multiselect
|
||||
view.exit_multi_select.isVisible = multiselect
|
||||
|
||||
view.topBar.setBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
view.context,
|
||||
if (multiselect) R.color.accent_normal_theme else R.color.foundation_normal_theme
|
||||
)
|
||||
)
|
||||
|
||||
view.handle.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
height = view.resources.getDimensionPixelSize(
|
||||
if (multiselect) {
|
||||
R.dimen.tab_tray_multiselect_handle_height
|
||||
} else {
|
||||
R.dimen.bottom_sheet_handle_height
|
||||
}
|
||||
)
|
||||
topMargin = view.resources.getDimensionPixelSize(
|
||||
if (multiselect) {
|
||||
R.dimen.tab_tray_multiselect_handle_top_margin
|
||||
} else {
|
||||
R.dimen.bottom_sheet_handle_top_margin
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
view.tab_wrapper.setChildWPercent(
|
||||
if (multiselect) 1F else NORMAL_HANDLE_PERCENT_WIDTH,
|
||||
view.handle.id
|
||||
)
|
||||
|
||||
view.handle.setBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
view.context,
|
||||
if (multiselect) R.color.accent_normal_theme else R.color.secondary_text_normal_theme
|
||||
)
|
||||
)
|
||||
|
||||
view.tab_layout.isVisible = !multiselect
|
||||
view.tab_tray_empty_view.isVisible = !multiselect
|
||||
view.tab_tray_overflow.isVisible = !multiselect
|
||||
view.tab_layout.isVisible = !multiselect
|
||||
}
|
||||
|
||||
private fun updateTabsForMultiselectModeChanged(inMultiselectMode: Boolean) {
|
||||
view.tabsTray.apply {
|
||||
val tabs = view.context.components.core.store.state.getNormalOrPrivateTabs(
|
||||
isPrivateModeSelected
|
||||
)
|
||||
|
||||
collectionsButtonAdapter.notifyItemChanged(
|
||||
0,
|
||||
if (inMultiselectMode) MultiselectModeChange.MULTISELECT else MultiselectModeChange.NORMAL
|
||||
)
|
||||
|
||||
tabsAdapter.notifyItemRangeChanged(0, tabs.size, true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTabsForSelectionChanged(itemId: String) {
|
||||
view.tabsTray.apply {
|
||||
val tabs = view.context.components.core.store.state.getNormalOrPrivateTabs(
|
||||
isPrivateModeSelected
|
||||
)
|
||||
|
||||
val selectedBrowserTabIndex = tabs.indexOfFirst { it.id == itemId }
|
||||
|
||||
tabsAdapter.notifyItemChanged(
|
||||
selectedBrowserTabIndex, true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTabTrayViewAccessibility(count: Int) {
|
||||
view.tab_layout.getTabAt(0)?.contentDescription = if (count == 1) {
|
||||
view.context?.getString(R.string.open_tab_tray_single)
|
||||
} else {
|
||||
String.format(view.context.getString(R.string.open_tab_tray_plural), count.toString())
|
||||
}
|
||||
|
||||
val isListTabView = view.context.settings().listTabView
|
||||
val columnCount = if (isListTabView) 1 else getNumberOfGridColumns(view.context)
|
||||
val rowCount = count.toDouble().div(columnCount).roundToInt()
|
||||
|
||||
view.tabsTray.updateAccessibilityCollectionInfo(rowCount, columnCount)
|
||||
}
|
||||
|
||||
private fun updateTabCounter(count: Int): String {
|
||||
if (count > MAX_VISIBLE_TABS) {
|
||||
counter_text.updatePadding(bottom = INFINITE_CHAR_PADDING_BOTTOM)
|
||||
return SO_MANY_TABS_OPEN
|
||||
}
|
||||
return NumberFormat.getInstance().format(count.toLong())
|
||||
}
|
||||
|
||||
fun setTopOffset(landscape: Boolean) {
|
||||
val topOffset = if (landscape) {
|
||||
0
|
||||
} else {
|
||||
view.resources.getDimensionPixelSize(R.dimen.tab_tray_top_offset)
|
||||
}
|
||||
|
||||
behavior.expandedOffset = topOffset
|
||||
}
|
||||
|
||||
fun dismissMenu() {
|
||||
menu?.dismiss()
|
||||
}
|
||||
|
||||
private fun toggleFabText(private: Boolean) {
|
||||
if (private) {
|
||||
fabView.new_tab_button.extend()
|
||||
fabView.new_tab_button.contentDescription =
|
||||
view.context.getString(R.string.add_private_tab)
|
||||
} else {
|
||||
fabView.new_tab_button.shrink()
|
||||
fabView.new_tab_button.contentDescription =
|
||||
view.context.getString(R.string.add_tab)
|
||||
}
|
||||
}
|
||||
|
||||
fun onBackPressed(): Boolean {
|
||||
return interactor.onBackPressed()
|
||||
}
|
||||
|
||||
fun scrollToSelectedBrowserTab(selectedTabId: String? = null) {
|
||||
view.tabsTray.apply {
|
||||
val recyclerViewIndex = getSelectedBrowserTabViewIndex(selectedTabId)
|
||||
layoutManager?.scrollToPosition(recyclerViewIndex)
|
||||
smoothScrollBy(
|
||||
0,
|
||||
-resources.getDimensionPixelSize(R.dimen.tab_tray_tab_item_height) / 2
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSelectedBrowserTabViewIndex(sessionId: String? = null): Int {
|
||||
val tabs = if (isPrivateModeSelected) {
|
||||
view.context.components.core.store.state.privateTabs
|
||||
} else {
|
||||
view.context.components.core.store.state.normalTabs
|
||||
}
|
||||
|
||||
return if (sessionId != null) {
|
||||
tabs.indexOfFirst { it.id == sessionId }
|
||||
} else {
|
||||
tabs.indexOfFirst { it.id == view.context.components.core.store.state.selectedTabId }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAB_COUNT_SHOW_CFR = 6
|
||||
private const val DEFAULT_TAB_ID = 0
|
||||
private const val PRIVATE_TAB_ID = 1
|
||||
|
||||
// Minimum number of list items for which to show the tabs tray as expanded.
|
||||
private const val EXPAND_AT_LIST_SIZE = 4
|
||||
|
||||
// Minimum number of grid items for which to show the tabs tray as expanded.
|
||||
private const val EXPAND_AT_GRID_SIZE = 3
|
||||
private const val SLIDE_OFFSET = 0
|
||||
private const val SELECTION_DELAY = 500
|
||||
private const val NORMAL_HANDLE_PERCENT_WIDTH = 0.1F
|
||||
private const val COLUMN_WIDTH_DP = 180
|
||||
|
||||
// The remaining padding offset needed to provide a 16dp column spacing between the grid items.
|
||||
const val GRID_ITEM_PARENT_PADDING = 8
|
||||
}
|
||||
}
|
@ -1,223 +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.tabtray
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.appcompat.widget.AppCompatImageButton
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.android.synthetic.main.tab_tray_grid_item.view.*
|
||||
import mozilla.components.browser.state.selector.findTabOrCustomTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.browser.tabstray.TabViewHolder
|
||||
import mozilla.components.browser.tabstray.TabsTrayStyling
|
||||
import mozilla.components.browser.tabstray.thumbnail.TabThumbnailView
|
||||
import mozilla.components.browser.toolbar.MAX_URI_LENGTH
|
||||
import mozilla.components.concept.base.images.ImageLoadRequest
|
||||
import mozilla.components.concept.base.images.ImageLoader
|
||||
import mozilla.components.concept.engine.mediasession.MediaSession
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import mozilla.components.concept.tabstray.TabsTray
|
||||
import mozilla.components.support.base.observer.Observable
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.components.metrics.MetricController
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.increaseTapArea
|
||||
import org.mozilla.fenix.ext.removeAndDisable
|
||||
import org.mozilla.fenix.ext.removeTouchDelegate
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.ext.showAndEnable
|
||||
import org.mozilla.fenix.ext.toShortUrl
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* A RecyclerView ViewHolder implementation for "tab" items.
|
||||
*/
|
||||
class TabTrayViewHolder(
|
||||
itemView: View,
|
||||
private val imageLoader: ImageLoader,
|
||||
private val store: BrowserStore = itemView.context.components.core.store,
|
||||
private val metrics: MetricController = itemView.context.components.analytics.metrics
|
||||
) : TabViewHolder(itemView) {
|
||||
|
||||
private val faviconView: ImageView? =
|
||||
itemView.findViewById(R.id.mozac_browser_tabstray_favicon_icon)
|
||||
private val titleView: TextView = itemView.findViewById(R.id.mozac_browser_tabstray_title)
|
||||
private val closeView: AppCompatImageButton =
|
||||
itemView.findViewById(R.id.mozac_browser_tabstray_close)
|
||||
private val thumbnailView: TabThumbnailView =
|
||||
itemView.findViewById(R.id.mozac_browser_tabstray_thumbnail)
|
||||
|
||||
@VisibleForTesting
|
||||
internal val urlView: TextView? = itemView.findViewById(R.id.mozac_browser_tabstray_url)
|
||||
private val playPauseButtonView: ImageButton = itemView.findViewById(R.id.play_pause_button)
|
||||
private val closeButton: AppCompatImageButton = itemView.findViewById(R.id.mozac_browser_tabstray_close)
|
||||
|
||||
override var tab: Tab? = null
|
||||
|
||||
/**
|
||||
* Displays the data of the given session and notifies the given observable about events.
|
||||
*/
|
||||
@Suppress("ComplexMethod", "LongMethod")
|
||||
override fun bind(
|
||||
tab: Tab,
|
||||
isSelected: Boolean,
|
||||
styling: TabsTrayStyling,
|
||||
observable: Observable<TabsTray.Observer>
|
||||
) {
|
||||
this.tab = tab
|
||||
|
||||
updateTitle(tab)
|
||||
updateUrl(tab)
|
||||
updateFavicon(tab)
|
||||
updateCloseButtonDescription(tab.title)
|
||||
updateSelectedTabIndicator(isSelected)
|
||||
|
||||
if (tab.thumbnail != null) {
|
||||
thumbnailView.setImageBitmap(tab.thumbnail)
|
||||
} else {
|
||||
loadIntoThumbnailView(thumbnailView, tab.id)
|
||||
}
|
||||
|
||||
if (itemView.context.settings().gridTabView) {
|
||||
closeButton.increaseTapArea(GRID_ITEM_CLOSE_BUTTON_EXTRA_DPS)
|
||||
}
|
||||
|
||||
// Media state
|
||||
playPauseButtonView.increaseTapArea(PLAY_PAUSE_BUTTON_EXTRA_DPS)
|
||||
|
||||
with(playPauseButtonView) {
|
||||
invalidate()
|
||||
val sessionState = store.state.findTabOrCustomTab(tab.id)
|
||||
when (sessionState?.mediaSessionState?.playbackState) {
|
||||
MediaSession.PlaybackState.PAUSED -> {
|
||||
showAndEnable()
|
||||
contentDescription =
|
||||
context.getString(R.string.mozac_feature_media_notification_action_play)
|
||||
setImageDrawable(
|
||||
AppCompatResources.getDrawable(context, R.drawable.media_state_play)
|
||||
)
|
||||
}
|
||||
|
||||
MediaSession.PlaybackState.PLAYING -> {
|
||||
showAndEnable()
|
||||
contentDescription =
|
||||
context.getString(R.string.mozac_feature_media_notification_action_pause)
|
||||
setImageDrawable(
|
||||
AppCompatResources.getDrawable(context, R.drawable.media_state_pause)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
removeTouchDelegate()
|
||||
removeAndDisable()
|
||||
}
|
||||
}
|
||||
|
||||
setOnClickListener {
|
||||
when (sessionState?.mediaSessionState?.playbackState) {
|
||||
MediaSession.PlaybackState.PLAYING -> {
|
||||
metrics.track(Event.TabMediaPause)
|
||||
sessionState.mediaSessionState?.controller?.pause()
|
||||
}
|
||||
|
||||
MediaSession.PlaybackState.PAUSED -> {
|
||||
metrics.track(Event.TabMediaPlay)
|
||||
sessionState.mediaSessionState?.controller?.play()
|
||||
}
|
||||
else -> throw AssertionError(
|
||||
"Play/Pause button clicked without play/pause state."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closeView.setOnClickListener {
|
||||
observable.notifyObservers { onTabClosed(tab) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFavicon(tab: Tab) {
|
||||
if (tab.icon != null) {
|
||||
faviconView?.visibility = View.VISIBLE
|
||||
faviconView?.setImageBitmap(tab.icon)
|
||||
} else {
|
||||
faviconView?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTitle(tab: Tab) {
|
||||
val title = if (tab.title.isNotEmpty()) {
|
||||
tab.title
|
||||
} else {
|
||||
tab.url
|
||||
}
|
||||
titleView.text = title
|
||||
}
|
||||
|
||||
private fun updateUrl(tab: Tab) {
|
||||
// Truncate to MAX_URI_LENGTH to prevent the UI from locking up for
|
||||
// extremely large URLs such as data URIs or bookmarklets. The same
|
||||
// is done in the toolbar and awesomebar:
|
||||
// https://github.com/mozilla-mobile/fenix/issues/1824
|
||||
// https://github.com/mozilla-mobile/android-components/issues/6985
|
||||
urlView?.text = tab.url
|
||||
.toShortUrl(itemView.context.components.publicSuffixList)
|
||||
.take(MAX_URI_LENGTH)
|
||||
}
|
||||
|
||||
override fun updateSelectedTabIndicator(showAsSelected: Boolean) {
|
||||
if (itemView.context.settings().gridTabView) {
|
||||
itemView.tab_tray_grid_item.background = if (showAsSelected) {
|
||||
AppCompatResources.getDrawable(itemView.context, R.drawable.tab_tray_grid_item_selected_border)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val color = if (showAsSelected) {
|
||||
R.color.tab_tray_item_selected_background_normal_theme
|
||||
} else {
|
||||
R.color.tab_tray_item_background_normal_theme
|
||||
}
|
||||
itemView.setBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
itemView.context,
|
||||
color
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateCloseButtonDescription(title: String) {
|
||||
closeView.contentDescription =
|
||||
closeView.context.getString(R.string.close_tab_title, title)
|
||||
}
|
||||
|
||||
private fun loadIntoThumbnailView(thumbnailView: ImageView, id: String) {
|
||||
val thumbnailSize = if (itemView.context.settings().gridTabView) {
|
||||
max(
|
||||
itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_grid_item_thumbnail_height),
|
||||
itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_grid_item_thumbnail_width)
|
||||
)
|
||||
} else {
|
||||
max(
|
||||
itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_list_item_thumbnail_height),
|
||||
itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_list_item_thumbnail_width)
|
||||
)
|
||||
}
|
||||
imageLoader.loadIntoView(thumbnailView, ImageLoadRequest(id, thumbnailSize))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PLAY_PAUSE_BUTTON_EXTRA_DPS = 24
|
||||
private const val GRID_ITEM_CLOSE_BUTTON_EXTRA_DPS = 24
|
||||
}
|
||||
}
|
@ -1,132 +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.tabtray
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_IDLE
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import mozilla.components.browser.tabstray.TabTouchCallback
|
||||
import mozilla.components.concept.tabstray.TabsTray
|
||||
import mozilla.components.support.base.observer.Observable
|
||||
import mozilla.components.support.ktx.android.content.getColorFromAttr
|
||||
import mozilla.components.support.ktx.android.content.getDrawableWithTint
|
||||
import mozilla.components.support.ktx.android.util.dpToPx
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.home.sessioncontrol.SwipeToDeleteCallback
|
||||
|
||||
/**
|
||||
* A callback for consumers to know when a [RecyclerView.ViewHolder] is about to be touched.
|
||||
* Return false if the default behaviour should be ignored.
|
||||
*/
|
||||
typealias OnViewHolderTouched = (RecyclerView.ViewHolder) -> Boolean
|
||||
|
||||
/**
|
||||
* An [ItemTouchHelper] for handling tab swiping to delete.
|
||||
*
|
||||
* @param onViewHolderTouched See [OnViewHolderTouched].
|
||||
*/
|
||||
class TabsTouchHelper(
|
||||
observable: Observable<TabsTray.Observer>,
|
||||
onViewHolderTouched: OnViewHolderTouched = { true },
|
||||
delegate: Callback = TouchCallback(observable, onViewHolderTouched)
|
||||
) : ItemTouchHelper(delegate)
|
||||
|
||||
/**
|
||||
* An [ItemTouchHelper.Callback] for drawing custom layouts on [RecyclerView.ViewHolder] interactions.
|
||||
*
|
||||
* @param onViewHolderTouched invoked when a tab is about to be swiped. See [OnViewHolderTouched].
|
||||
*/
|
||||
class TouchCallback(
|
||||
observable: Observable<TabsTray.Observer>,
|
||||
private val onViewHolderTouched: OnViewHolderTouched
|
||||
) : TabTouchCallback(observable) {
|
||||
|
||||
override fun getMovementFlags(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder
|
||||
): Int {
|
||||
if (!onViewHolderTouched.invoke(viewHolder)) {
|
||||
return ItemTouchHelper.Callback.makeFlag(ACTION_STATE_IDLE, 0)
|
||||
}
|
||||
|
||||
return super.getMovementFlags(recyclerView, viewHolder)
|
||||
}
|
||||
|
||||
override fun onChildDraw(
|
||||
c: Canvas,
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
dX: Float,
|
||||
dY: Float,
|
||||
actionState: Int,
|
||||
isCurrentlyActive: Boolean
|
||||
) {
|
||||
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
|
||||
|
||||
if (recyclerView.context.settings().gridTabView) {
|
||||
return
|
||||
}
|
||||
|
||||
val icon = recyclerView.context.getDrawableWithTint(
|
||||
R.drawable.ic_delete,
|
||||
recyclerView.context.getColorFromAttr(R.attr.destructive)
|
||||
)!!
|
||||
val background = AppCompatResources.getDrawable(
|
||||
recyclerView.context,
|
||||
R.drawable.swipe_delete_background
|
||||
)!!
|
||||
val itemView = viewHolder.itemView
|
||||
val iconLeft: Int
|
||||
val iconRight: Int
|
||||
val margin =
|
||||
SwipeToDeleteCallback.MARGIN.dpToPx(recyclerView.resources.displayMetrics)
|
||||
val iconWidth = icon.intrinsicWidth
|
||||
val iconHeight = icon.intrinsicHeight
|
||||
val cellHeight = itemView.bottom - itemView.top
|
||||
val iconTop = itemView.top + (cellHeight - iconHeight) / 2
|
||||
val iconBottom = iconTop + iconHeight
|
||||
|
||||
when {
|
||||
dX > 0 -> { // Swiping to the right
|
||||
iconLeft = itemView.left + margin
|
||||
iconRight = itemView.left + margin + iconWidth
|
||||
background.setBounds(
|
||||
itemView.left, itemView.top,
|
||||
(itemView.left + dX).toInt() + SwipeToDeleteCallback.BACKGROUND_CORNER_OFFSET,
|
||||
itemView.bottom
|
||||
)
|
||||
icon.setBounds(iconLeft, iconTop, iconRight, iconBottom)
|
||||
draw(background, icon, c)
|
||||
}
|
||||
dX < 0 -> { // Swiping to the left
|
||||
iconLeft = itemView.right - margin - iconWidth
|
||||
iconRight = itemView.right - margin
|
||||
background.setBounds(
|
||||
(itemView.right + dX).toInt() - SwipeToDeleteCallback.BACKGROUND_CORNER_OFFSET,
|
||||
itemView.top, itemView.right, itemView.bottom
|
||||
)
|
||||
icon.setBounds(iconLeft, iconTop, iconRight, iconBottom)
|
||||
draw(background, icon, c)
|
||||
}
|
||||
else -> { // View not swiped
|
||||
background.setBounds(0, 0, 0, 0)
|
||||
icon.setBounds(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun draw(
|
||||
background: Drawable,
|
||||
icon: Drawable,
|
||||
c: Canvas
|
||||
) {
|
||||
background.draw(c)
|
||||
icon.draw(c)
|
||||
}
|
||||
}
|
@ -1,171 +0,0 @@
|
||||
<?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:id="@+id/tab_wrapper"
|
||||
style="@style/BottomSheetModal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:backgroundTint="@color/foundation_normal_theme"
|
||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
||||
|
||||
<View
|
||||
android:id="@+id/handle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/bottom_sheet_handle_height"
|
||||
android:layout_marginTop="@dimen/bottom_sheet_handle_top_margin"
|
||||
android:background="@color/secondary_text_normal_theme"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_percent="0.1" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/infoBanner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/foundation_normal_theme"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@+id/topBar" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tab_tray_empty_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:gravity="center_horizontal"
|
||||
android:paddingTop="80dp"
|
||||
android:text="@string/no_open_tabs_description"
|
||||
android:textColor="?secondaryText"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/infoBanner" />
|
||||
|
||||
<View
|
||||
android:id="@+id/topBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:background="@color/foundation_normal_theme"
|
||||
android:importantForAccessibility="no"
|
||||
app:layout_constraintTop_toBottomOf="@+id/handle" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/exit_multi_select"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="0dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/tab_tray_close_multiselect_content_description"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/multiselect_title"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/multiselect_title"
|
||||
app:srcCompat="@drawable/ic_close"
|
||||
app:tint="@color/contrast_text_normal_theme" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/multiselect_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:focusableInTouchMode="true"
|
||||
android:textColor="@color/contrast_text_normal_theme"
|
||||
android:textSize="20sp"
|
||||
app:fontFamily="@font/metropolis_semibold"
|
||||
app:layout_constraintBottom_toBottomOf="@id/topBar"
|
||||
app:layout_constraintEnd_toStartOf="@id/collect_multi_select"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toEndOf="@+id/exit_multi_select"
|
||||
app:layout_constraintTop_toTopOf="@id/topBar"
|
||||
tools:text="3 selected" />
|
||||
|
||||
<include layout="@layout/tabstray_multiselect_items" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tab_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="80dp"
|
||||
android:background="@color/foundation_normal_theme"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/handle"
|
||||
app:layout_constraintWidth_percent="0.5"
|
||||
app:tabGravity="fill"
|
||||
app:tabIconTint="@color/tab_icon"
|
||||
app:tabIndicatorColor="@color/accent_normal_theme"
|
||||
app:tabMaxWidth="0dp"
|
||||
app:tabRippleColor="@android:color/transparent">
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:id="@+id/default_tab_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/tab_header_label"
|
||||
android:layout="@layout/tabs_tray_tab_counter"
|
||||
app:tabIconTint="@color/tab_icon" />
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:id="@+id/private_tab_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/tabs_header_private_tabs_title"
|
||||
android:icon="@drawable/ic_private_browsing" />
|
||||
|
||||
</com.google.android.material.tabs.TabLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/tab_tray_new_tab"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/add_tab"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tab_layout"
|
||||
app:layout_constraintEnd_toStartOf="@id/tab_tray_overflow"
|
||||
app:layout_constraintTop_toTopOf="@id/tab_layout"
|
||||
app:srcCompat="@drawable/ic_new"
|
||||
app:tint="@color/primary_text_normal_theme" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/tab_tray_overflow"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/open_tabs_menu"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tab_layout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/tab_layout"
|
||||
app:srcCompat="@drawable/ic_menu"
|
||||
app:tint="@color/tab_tray_heading_icon_menu_normal_theme" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:background="@color/tab_tray_item_divider_normal_theme"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/infoBanner" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/tabsTray"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="140dp"
|
||||
android:scrollbarStyle="outsideOverlay"
|
||||
android:scrollbars="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/divider" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,8 +0,0 @@
|
||||
<?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/. -->
|
||||
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
style="@style/NeutralButton"
|
||||
android:layout_margin="8dp"
|
||||
android:text="@string/tabs_tray_select_tabs" />
|
@ -1,318 +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.tabtray
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavDirections
|
||||
import io.mockk.Runs
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
import io.mockk.verifyAll
|
||||
import io.mockk.verifyOrder
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestCoroutineScope
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.browser.state.state.createTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.base.profiler.Profiler
|
||||
import mozilla.components.concept.storage.BookmarksStorage
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import mozilla.components.feature.tab.collections.TabCollection
|
||||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.helpers.DisableNavGraphProviderAssertionRule
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
|
||||
import org.mozilla.fenix.components.TabCollectionStorage
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.components.metrics.MetricController
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class DefaultTabTrayControllerTest {
|
||||
private val activity: HomeActivity = mockk(relaxed = true)
|
||||
private val profiler: Profiler? = mockk(relaxed = true)
|
||||
private val navController: NavController = mockk()
|
||||
private val browsingModeManager: BrowsingModeManager = mockk(relaxed = true)
|
||||
private val dismissTabTray: (() -> Unit) = mockk(relaxed = true)
|
||||
private val dismissTabTrayAndNavigateHome: ((String) -> Unit) = mockk(relaxed = true)
|
||||
private val registerCollectionStorageObserver: (() -> Unit) = mockk(relaxed = true)
|
||||
private val showChooseCollectionDialog: ((List<TabSessionState>) -> Unit) = mockk(relaxed = true)
|
||||
private val showAddNewCollectionDialog: ((List<TabSessionState>) -> Unit) = mockk(relaxed = true)
|
||||
private val tabCollectionStorage: TabCollectionStorage = mockk(relaxed = true)
|
||||
private val bookmarksStorage: BookmarksStorage = mockk(relaxed = true)
|
||||
private val tabCollection: TabCollection = mockk()
|
||||
private val cachedTabCollections: List<TabCollection> = listOf(tabCollection)
|
||||
private val currentDestination: NavDestination = mockk(relaxed = true)
|
||||
private val tabTrayFragmentStore: TabTrayDialogFragmentStore = mockk(relaxed = true)
|
||||
private val selectTabUseCase: TabsUseCases.SelectTabUseCase = mockk(relaxed = true)
|
||||
private val tabsUseCases: TabsUseCases = mockk(relaxed = true)
|
||||
private val showUndoSnackbarForTabs: (() -> Unit) = mockk(relaxed = true)
|
||||
private val showBookmarksSavedSnackbar: (() -> Unit) = mockk(relaxed = true)
|
||||
private val metrics: MetricController = mockk(relaxed = true)
|
||||
|
||||
private lateinit var controller: DefaultTabTrayController
|
||||
|
||||
private val tab1 = createTab(url = "http://firefox.com", id = "5678")
|
||||
private val tab2 = createTab(url = "http://mozilla.org", id = "1234")
|
||||
|
||||
@get:Rule
|
||||
val disableNavGraphProviderAssertionRule = DisableNavGraphProviderAssertionRule()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mockkStatic("org.mozilla.fenix.ext.SessionManagerKt")
|
||||
|
||||
val store = BrowserStore(
|
||||
BrowserState(
|
||||
tabs = listOf(tab1, tab2), selectedTabId = tab2.id
|
||||
)
|
||||
)
|
||||
|
||||
every { tabCollectionStorage.cachedTabCollections } returns cachedTabCollections
|
||||
every { navController.navigate(any<NavDirections>()) } just Runs
|
||||
every { navController.currentDestination } returns currentDestination
|
||||
every { currentDestination.id } returns R.id.browserFragment
|
||||
every { tabCollection.title } returns "Collection title"
|
||||
|
||||
controller = DefaultTabTrayController(
|
||||
activity = activity,
|
||||
profiler = profiler,
|
||||
browserStore = store,
|
||||
browsingModeManager = browsingModeManager,
|
||||
tabCollectionStorage = tabCollectionStorage,
|
||||
bookmarksStorage = bookmarksStorage,
|
||||
ioScope = TestCoroutineScope(),
|
||||
metrics = metrics,
|
||||
navController = navController,
|
||||
tabsUseCases = tabsUseCases,
|
||||
dismissTabTray = dismissTabTray,
|
||||
dismissTabTrayAndNavigateHome = dismissTabTrayAndNavigateHome,
|
||||
registerCollectionStorageObserver = registerCollectionStorageObserver,
|
||||
tabTrayDialogFragmentStore = tabTrayFragmentStore,
|
||||
selectTabUseCase = selectTabUseCase,
|
||||
showChooseCollectionDialog = showChooseCollectionDialog,
|
||||
showAddNewCollectionDialog = showAddNewCollectionDialog,
|
||||
showUndoSnackbarForTabs = showUndoSnackbarForTabs,
|
||||
showBookmarksSnackbar = showBookmarksSavedSnackbar
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleTabSettingsClicked() {
|
||||
controller.handleTabSettingsClicked()
|
||||
|
||||
verify {
|
||||
navController.navigate(
|
||||
TabTrayDialogFragmentDirections.actionGlobalTabSettingsFragment()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onNewTabTapped() {
|
||||
controller.handleNewTabTapped(private = false)
|
||||
|
||||
verifyOrder {
|
||||
browsingModeManager.mode = BrowsingMode.fromBoolean(false)
|
||||
navController.navigate(
|
||||
TabTrayDialogFragmentDirections.actionGlobalHome(
|
||||
focusOnAddressBar = true
|
||||
)
|
||||
)
|
||||
dismissTabTray()
|
||||
}
|
||||
|
||||
controller.handleNewTabTapped(private = true)
|
||||
|
||||
verifyOrder {
|
||||
browsingModeManager.mode = BrowsingMode.fromBoolean(true)
|
||||
navController.navigate(
|
||||
TabTrayDialogFragmentDirections.actionGlobalHome(
|
||||
focusOnAddressBar = true
|
||||
)
|
||||
)
|
||||
dismissTabTray()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onTabTrayDismissed() {
|
||||
controller.handleTabTrayDismissed()
|
||||
|
||||
verify {
|
||||
dismissTabTray()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onShareTabsClicked() {
|
||||
val navDirectionsSlot = slot<NavDirections>()
|
||||
every { navController.navigate(capture(navDirectionsSlot)) } just Runs
|
||||
|
||||
controller.handleShareTabsOfTypeClicked(private = false)
|
||||
|
||||
verify {
|
||||
navController.navigate(capture(navDirectionsSlot))
|
||||
}
|
||||
|
||||
assertTrue(navDirectionsSlot.isCaptured)
|
||||
assertEquals(R.id.action_global_shareFragment, navDirectionsSlot.captured.actionId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onCloseAllTabsClicked() {
|
||||
controller.handleCloseAllTabsClicked(private = false)
|
||||
|
||||
verify {
|
||||
dismissTabTrayAndNavigateHome(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleBackPressed() {
|
||||
every { tabTrayFragmentStore.state.mode } returns TabTrayDialogFragmentState.Mode.MultiSelect(
|
||||
setOf()
|
||||
)
|
||||
controller.handleBackPressed()
|
||||
verify {
|
||||
tabTrayFragmentStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onModeRequested() {
|
||||
val mode = TabTrayDialogFragmentState.Mode.MultiSelect(
|
||||
setOf()
|
||||
)
|
||||
every { tabTrayFragmentStore.state.mode } returns mode
|
||||
controller.onModeRequested()
|
||||
verify {
|
||||
tabTrayFragmentStore.state.mode
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleAddSelectedTab() {
|
||||
val tab = Tab("1234", "mozilla.org")
|
||||
controller.handleAddSelectedTab(tab)
|
||||
verify {
|
||||
tabTrayFragmentStore.dispatch(TabTrayDialogFragmentAction.AddItemForCollection(tab))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleRemoveSelectedTab() {
|
||||
val tab = Tab("1234", "mozilla.org")
|
||||
controller.handleRemoveSelectedTab(tab)
|
||||
verify {
|
||||
tabTrayFragmentStore.dispatch(TabTrayDialogFragmentAction.RemoveItemForCollection(tab))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleOpenTab() {
|
||||
val tab = Tab("1234", "mozilla.org")
|
||||
controller.handleOpenTab(tab)
|
||||
verify {
|
||||
selectTabUseCase.invoke(tab.id)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleEnterMultiselect() {
|
||||
controller.handleEnterMultiselect()
|
||||
verify {
|
||||
tabTrayFragmentStore.dispatch(TabTrayDialogFragmentAction.EnterMultiSelectMode)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onSaveToCollectionClicked() {
|
||||
val tab = Tab(tab2.id, tab2.content.url)
|
||||
|
||||
controller.handleSaveToCollectionClicked(setOf(tab))
|
||||
|
||||
verifyAll {
|
||||
metrics.track(Event.TabsTraySaveToCollectionPressed)
|
||||
registerCollectionStorageObserver()
|
||||
showChooseCollectionDialog(listOf(tab2))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleShareSelectedTabs() {
|
||||
val tab = Tab("1234", "mozilla.org")
|
||||
val navDirectionsSlot = slot<NavDirections>()
|
||||
every { navController.navigate(capture(navDirectionsSlot)) } just Runs
|
||||
|
||||
controller.handleShareSelectedTabsClicked(setOf(tab))
|
||||
|
||||
verify {
|
||||
navController.navigate(capture(navDirectionsSlot))
|
||||
}
|
||||
|
||||
assertTrue(navDirectionsSlot.isCaptured)
|
||||
assertEquals(R.id.action_global_shareFragment, navDirectionsSlot.captured.actionId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleDeleteSelectedTabs() {
|
||||
val tab = Tab("1234", "mozilla.org")
|
||||
|
||||
controller.handleDeleteSelectedTabs(setOf(tab))
|
||||
verifyAll {
|
||||
tabsUseCases.removeTabs(listOf(tab.id))
|
||||
tabTrayFragmentStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
|
||||
showUndoSnackbarForTabs()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleBookmarkSelectedTabs() {
|
||||
val tab = Tab("1234", "mozilla.org")
|
||||
coEvery { bookmarksStorage.getBookmarksWithUrl("mozilla.org") } returns listOf()
|
||||
|
||||
controller.handleBookmarkSelectedTabs(setOf(tab))
|
||||
verifyAll {
|
||||
tabTrayFragmentStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
|
||||
showBookmarksSavedSnackbar()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleRecentlyClosedClicked() {
|
||||
controller.handleRecentlyClosedClicked()
|
||||
val directions = TabTrayDialogFragmentDirections.actionGlobalRecentlyClosed()
|
||||
|
||||
verifyAll {
|
||||
navController.navigate(directions)
|
||||
metrics.track(Event.RecentlyClosedTabsOpened)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleGoToTabsSettingClicked() {
|
||||
controller.handleGoToTabsSettingClicked()
|
||||
val directions = TabTrayDialogFragmentDirections.actionGlobalTabSettingsFragment()
|
||||
|
||||
verifyAll {
|
||||
navController.navigate(directions)
|
||||
metrics.track(Event.TabsTrayCfrTapped)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,53 +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.tabtray
|
||||
|
||||
import android.widget.FrameLayout
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.tabtray.SaveToCollectionsButtonAdapter.Item
|
||||
import org.mozilla.fenix.tabtray.SaveToCollectionsButtonAdapter.ViewHolder
|
||||
import kotlin.random.Random
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class SaveToCollectionsButtonAdapterTest {
|
||||
|
||||
private lateinit var adapter: SaveToCollectionsButtonAdapter
|
||||
private lateinit var interactor: TabTrayInteractor
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
interactor = mockk(relaxed = true)
|
||||
adapter = SaveToCollectionsButtonAdapter(interactor)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create adapter only has one item in it`() {
|
||||
assertEquals(1, adapter.itemCount)
|
||||
assertTrue(adapter.currentList.first() is Item)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `viewholder click invokes interactor`() {
|
||||
val itemView = FrameLayout(testContext)
|
||||
val viewHolder = ViewHolder(itemView, interactor)
|
||||
|
||||
viewHolder.onClick(itemView)
|
||||
|
||||
verify { interactor.onEnterMultiselect() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `always use the same layout`() {
|
||||
assertEquals(ViewHolder.LAYOUT_ID, adapter.getItemViewType(Random.nextInt()))
|
||||
}
|
||||
}
|
@ -1,145 +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.tabtray
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.createTab
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotSame
|
||||
import org.junit.Test
|
||||
|
||||
class TabTrayDialogFragmentStoreTest {
|
||||
|
||||
@Test
|
||||
fun browserStateChange() = runBlocking {
|
||||
val initialState = emptyDefaultState()
|
||||
val store = TabTrayDialogFragmentStore(initialState)
|
||||
|
||||
val newBrowserState = BrowserState(
|
||||
listOf(
|
||||
createTab("https://www.mozilla.org", id = "13256")
|
||||
)
|
||||
)
|
||||
|
||||
store.dispatch(
|
||||
TabTrayDialogFragmentAction.BrowserStateChanged(
|
||||
newBrowserState
|
||||
)
|
||||
).join()
|
||||
|
||||
assertNotSame(initialState, store.state)
|
||||
assertEquals(
|
||||
store.state.browserState,
|
||||
newBrowserState
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun enterMultiselectMode() = runBlocking {
|
||||
val initialState = emptyDefaultState()
|
||||
val store = TabTrayDialogFragmentStore(initialState)
|
||||
|
||||
store.dispatch(
|
||||
TabTrayDialogFragmentAction.EnterMultiSelectMode
|
||||
).join()
|
||||
|
||||
assertNotSame(initialState, store.state)
|
||||
assertEquals(
|
||||
store.state.mode,
|
||||
TabTrayDialogFragmentState.Mode.MultiSelect(setOf())
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun exitMultiselectMode() = runBlocking {
|
||||
val initialState = TabTrayDialogFragmentState(
|
||||
browserState = BrowserState(),
|
||||
mode = TabTrayDialogFragmentState.Mode.MultiSelect(setOf())
|
||||
)
|
||||
val store = TabTrayDialogFragmentStore(initialState)
|
||||
|
||||
store.dispatch(
|
||||
TabTrayDialogFragmentAction.ExitMultiSelectMode
|
||||
).join()
|
||||
|
||||
assertNotSame(initialState, store.state)
|
||||
assertEquals(
|
||||
store.state.mode,
|
||||
TabTrayDialogFragmentState.Mode.Normal
|
||||
)
|
||||
assertEquals(
|
||||
store.state.mode.selectedItems,
|
||||
setOf<Tab>()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addItemForCollection() = runBlocking {
|
||||
val initialState = emptyDefaultState()
|
||||
val store = TabTrayDialogFragmentStore(initialState)
|
||||
|
||||
val tab = Tab(id = "1234", url = "mozilla.org")
|
||||
store.dispatch(
|
||||
TabTrayDialogFragmentAction.AddItemForCollection(tab)
|
||||
).join()
|
||||
|
||||
assertNotSame(initialState, store.state)
|
||||
assertEquals(
|
||||
store.state.mode,
|
||||
TabTrayDialogFragmentState.Mode.MultiSelect(setOf(tab))
|
||||
)
|
||||
assertEquals(
|
||||
store.state.mode.selectedItems,
|
||||
setOf(tab)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeItemForCollection() = runBlocking {
|
||||
val tab = Tab(id = "1234", url = "mozilla.org")
|
||||
val secondTab = Tab(id = "12345", url = "pocket.com")
|
||||
|
||||
val initialState = TabTrayDialogFragmentState(
|
||||
browserState = BrowserState(),
|
||||
mode = TabTrayDialogFragmentState.Mode.MultiSelect(setOf(tab, secondTab))
|
||||
)
|
||||
|
||||
val store = TabTrayDialogFragmentStore(initialState)
|
||||
|
||||
store.dispatch(
|
||||
TabTrayDialogFragmentAction.RemoveItemForCollection(tab)
|
||||
).join()
|
||||
|
||||
assertNotSame(initialState, store.state)
|
||||
assertEquals(
|
||||
store.state.mode,
|
||||
TabTrayDialogFragmentState.Mode.MultiSelect(setOf(secondTab))
|
||||
)
|
||||
assertEquals(
|
||||
store.state.mode.selectedItems,
|
||||
setOf(secondTab)
|
||||
)
|
||||
|
||||
store.dispatch(
|
||||
TabTrayDialogFragmentAction.RemoveItemForCollection(secondTab)
|
||||
).join()
|
||||
|
||||
assertEquals(
|
||||
store.state.mode,
|
||||
TabTrayDialogFragmentState.Mode.Normal
|
||||
)
|
||||
assertEquals(
|
||||
store.state.mode.selectedItems,
|
||||
setOf<Tab>()
|
||||
)
|
||||
}
|
||||
|
||||
private fun emptyDefaultState(): TabTrayDialogFragmentState = TabTrayDialogFragmentState(
|
||||
browserState = BrowserState(),
|
||||
mode = TabTrayDialogFragmentState.Mode.Normal
|
||||
)
|
||||
}
|
@ -1,136 +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.tabtray
|
||||
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import org.junit.Test
|
||||
|
||||
class TabTrayFragmentInteractorTest {
|
||||
private val controller = mockk<TabTrayController>(relaxed = true)
|
||||
private val interactor = TabTrayFragmentInteractor(controller)
|
||||
|
||||
@Test
|
||||
fun onShareSelectedTabsClicked() {
|
||||
val tab = Tab("1234", "mozilla.org")
|
||||
val tab2 = Tab("5678", "pocket.com")
|
||||
val selectedTabs = setOf(tab, tab2)
|
||||
interactor.onShareSelectedTabsClicked(selectedTabs)
|
||||
verify { controller.handleShareSelectedTabsClicked(selectedTabs) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onBookmarkSelectedTabs() {
|
||||
val tab = Tab("1234", "mozilla.org")
|
||||
val tab2 = Tab("5678", "pocket.com")
|
||||
val selectedTabs = setOf(tab, tab2)
|
||||
interactor.onBookmarkSelectedTabs(selectedTabs)
|
||||
verify { controller.handleBookmarkSelectedTabs(selectedTabs) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onDeleteSelectedTabs() {
|
||||
val tab = Tab("1234", "mozilla.org")
|
||||
val tab2 = Tab("5678", "pocket.com")
|
||||
val selectedTabs = setOf(tab, tab2)
|
||||
interactor.onDeleteSelectedTabs(selectedTabs)
|
||||
verify { controller.handleDeleteSelectedTabs(selectedTabs) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onNewTabTapped() {
|
||||
interactor.onNewTabTapped(private = true)
|
||||
verify { controller.handleNewTabTapped(true) }
|
||||
|
||||
interactor.onNewTabTapped(private = false)
|
||||
verify { controller.handleNewTabTapped(false) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onTabSettingsClicked() {
|
||||
interactor.onTabSettingsClicked()
|
||||
|
||||
verify {
|
||||
controller.handleTabSettingsClicked()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onTabTrayDismissed() {
|
||||
interactor.onTabTrayDismissed()
|
||||
verify { controller.handleTabTrayDismissed() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onShareTabsClicked() {
|
||||
interactor.onShareTabsOfTypeClicked(private = true)
|
||||
verify { controller.handleShareTabsOfTypeClicked(true) }
|
||||
|
||||
interactor.onShareTabsOfTypeClicked(private = false)
|
||||
verify { controller.handleShareTabsOfTypeClicked(false) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onSaveToCollectionClicked() {
|
||||
val tab = Tab("1234", "mozilla.org")
|
||||
interactor.onSaveToCollectionClicked(setOf(tab))
|
||||
verify { controller.handleSaveToCollectionClicked(setOf(tab)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onCloseAllTabsClicked() {
|
||||
interactor.onCloseAllTabsClicked(private = false)
|
||||
verify { controller.handleCloseAllTabsClicked(false) }
|
||||
|
||||
interactor.onCloseAllTabsClicked(private = true)
|
||||
verify { controller.handleCloseAllTabsClicked(true) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onBackPressed() {
|
||||
interactor.onBackPressed()
|
||||
verify { controller.handleBackPressed() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onModeRequested() {
|
||||
interactor.onModeRequested()
|
||||
verify { controller.onModeRequested() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onOpenTab() {
|
||||
val tab = Tab("1234", "mozilla.org")
|
||||
interactor.onOpenTab(tab)
|
||||
verify { controller.handleOpenTab(tab) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onAddSelectedTab() {
|
||||
val tab = Tab("1234", "mozilla.org")
|
||||
interactor.onAddSelectedTab(tab)
|
||||
verify { controller.handleAddSelectedTab(tab) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onRemoveSelectedTab() {
|
||||
val tab = Tab("1234", "mozilla.org")
|
||||
interactor.onRemoveSelectedTab(tab)
|
||||
verify { controller.handleRemoveSelectedTab(tab) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onEnterMultiselect() {
|
||||
interactor.onEnterMultiselect()
|
||||
verify { controller.handleEnterMultiselect() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onGoToTabsSettingClicked() {
|
||||
interactor.onGoToTabsSettings()
|
||||
verify { controller.handleGoToTabsSettingClicked() }
|
||||
}
|
||||
}
|
@ -1,140 +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.tabtray
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.ImageButton
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.ContentState
|
||||
import mozilla.components.browser.state.state.MediaSessionState
|
||||
import mozilla.components.browser.state.state.SessionState
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.browser.toolbar.MAX_URI_LENGTH
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import mozilla.components.concept.base.images.ImageLoadRequest
|
||||
import mozilla.components.concept.base.images.ImageLoader
|
||||
import mozilla.components.concept.engine.mediasession.MediaSession
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.MetricController
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class TabTrayViewHolderTest {
|
||||
|
||||
private lateinit var view: View
|
||||
@MockK private lateinit var imageLoader: ImageLoader
|
||||
@MockK private lateinit var store: BrowserStore
|
||||
@MockK private lateinit var sessionState: SessionState
|
||||
@MockK private lateinit var mediaSessionState: MediaSessionState
|
||||
@MockK private lateinit var metrics: MetricController
|
||||
private var state = BrowserState()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
MockKAnnotations.init(this)
|
||||
view = LayoutInflater.from(testContext)
|
||||
.inflate(R.layout.tab_tray_item, null, false)
|
||||
state = BrowserState()
|
||||
|
||||
every { imageLoader.loadIntoView(any(), any(), any(), any()) } just Runs
|
||||
every { store.state } answers { state }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `extremely long URLs are truncated to prevent slowing down the UI`() {
|
||||
val tabViewHolder = createViewHolder()
|
||||
|
||||
val extremelyLongUrl = "m".repeat(MAX_URI_LENGTH + 1)
|
||||
val tab = Tab(
|
||||
id = "123",
|
||||
url = extremelyLongUrl
|
||||
)
|
||||
tabViewHolder.bind(tab, false, mockk(), mockk())
|
||||
|
||||
assertEquals("m".repeat(MAX_URI_LENGTH), tabViewHolder.urlView?.text)
|
||||
verify { imageLoader.loadIntoView(any(), ImageLoadRequest("123", 92)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `show play button if media is paused in tab`() {
|
||||
val playPauseButtonView: ImageButton = view.findViewById(R.id.play_pause_button)
|
||||
val tabViewHolder = createViewHolder()
|
||||
|
||||
val tab = Tab(
|
||||
id = "123",
|
||||
url = "https://example.com"
|
||||
)
|
||||
|
||||
state = state.copy(
|
||||
tabs = listOf(
|
||||
TabSessionState(
|
||||
id = "123",
|
||||
content = ContentState(
|
||||
url = "https://example.com",
|
||||
searchTerms = "search terms"
|
||||
),
|
||||
mediaSessionState = mediaSessionState
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
every { mediaSessionState.playbackState } answers { MediaSession.PlaybackState.PAUSED }
|
||||
|
||||
tabViewHolder.bind(tab, false, mockk(), mockk())
|
||||
|
||||
assertEquals("Play", playPauseButtonView.contentDescription)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `show pause button if media is playing in tab`() {
|
||||
val playPauseButtonView: ImageButton = view.findViewById(R.id.play_pause_button)
|
||||
val tabViewHolder = createViewHolder()
|
||||
|
||||
val tab = Tab(
|
||||
id = "123",
|
||||
url = "https://example.com"
|
||||
)
|
||||
|
||||
state = state.copy(
|
||||
tabs = listOf(
|
||||
TabSessionState(
|
||||
id = "123",
|
||||
content = ContentState(
|
||||
url = "https://example.com",
|
||||
searchTerms = "search terms"
|
||||
),
|
||||
mediaSessionState = mediaSessionState
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
every { mediaSessionState.playbackState } answers { MediaSession.PlaybackState.PLAYING }
|
||||
|
||||
tabViewHolder.bind(tab, false, mockk(), mockk())
|
||||
|
||||
assertEquals("Pause", playPauseButtonView.contentDescription)
|
||||
}
|
||||
|
||||
private fun createViewHolder() = TabTrayViewHolder(
|
||||
view,
|
||||
imageLoader = imageLoader,
|
||||
store = store,
|
||||
metrics = metrics
|
||||
)
|
||||
}
|
@ -1,55 +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.tabtray
|
||||
|
||||
import android.widget.FrameLayout
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_IDLE
|
||||
import androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.mockk.mockk
|
||||
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.helpers.FenixRobolectricTestRunner
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class TabsTouchHelperTest {
|
||||
|
||||
@Test
|
||||
fun `movement flags remain unchanged if onSwipeToDelete is true`() {
|
||||
val recyclerView = RecyclerView(testContext)
|
||||
val layout = FrameLayout(testContext)
|
||||
val interactor: TabTrayInteractor = mockk(relaxed = true)
|
||||
val viewHolder = SaveToCollectionsButtonAdapter.ViewHolder(layout, interactor)
|
||||
val callback = TouchCallback(mockk()) { true }
|
||||
|
||||
assertEquals(0, callback.getDragDirs(recyclerView, viewHolder))
|
||||
assertEquals(ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT, callback.getSwipeDirs(recyclerView, viewHolder))
|
||||
|
||||
val actual = callback.getMovementFlags(recyclerView, viewHolder)
|
||||
val expected = makeMovementFlags(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `movement flags remain unchanged if onSwipeToDelete is false`() {
|
||||
val recyclerView = RecyclerView(testContext)
|
||||
val layout = FrameLayout(testContext)
|
||||
val interactor: TabTrayInteractor = mockk(relaxed = true)
|
||||
val viewHolder = SaveToCollectionsButtonAdapter.ViewHolder(layout, interactor)
|
||||
val callback = TouchCallback(mockk()) { false }
|
||||
|
||||
assertEquals(0, callback.getDragDirs(recyclerView, viewHolder))
|
||||
assertEquals(ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT, callback.getSwipeDirs(recyclerView, viewHolder))
|
||||
|
||||
val actual = callback.getMovementFlags(recyclerView, viewHolder)
|
||||
val expected = ItemTouchHelper.Callback.makeFlag(ACTION_STATE_IDLE, 0)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue