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