You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
iceraven-browser/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt

452 lines
18 KiB
Kotlin

/* 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.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_bottom.view.*
import kotlinx.android.synthetic.main.component_tabstray_fab_bottom.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 mozilla.components.browser.session.Session
import mozilla.components.browser.state.selector.findTab
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 org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserFragmentDirections
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.metrics
import org.mozilla.fenix.ext.normalSessionSize
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
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() = if (tabTrayView.fabView.new_tab_button.isVisible) tabTrayView.fabView.new_tab_button
else null
private val collectionStorageObserver = object : TabCollectionStorage.Observer {
override fun onCollectionCreated(title: String, sessions: List<Session>) {
showCollectionSnackbar(sessions.size, true)
}
override fun onTabsAdded(tabCollection: TabCollection, sessions: List<Session>) {
showCollectionSnackbar(sessions.size)
}
}
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 invoke(session: Session) {
requireContext().components.analytics.metrics.track(Event.OpenedExistingTab)
requireComponents.useCases.tabsUseCases.selectTab(session)
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)
}
override fun invoke(session: Session) {
requireContext().components.analytics.metrics.track(Event.ClosedExistingTab)
showUndoSnackbarForTab(session.id)
removeIfNotLastTab(session.id)
}
}
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 sessionManager = view?.context?.components?.core?.sessionManager
val sessionToRemove = sessionManager?.findSessionById(sessionId)
if (sessionManager?.sessions?.filter { sessionToRemove?.private == it.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.expand()
tabTrayView.updateTabsTrayLayout()
currentOrientation = newConfig.orientation
}
}
@Suppress("LongMethod")
@OptIn(ExperimentalCoroutinesApi::class)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val activity = activity as HomeActivity
val isPrivate = activity.browsingModeManager.mode.isPrivate
if (requireContext().settings().useTopTabsTray) {
dialog?.window?.attributes?.windowAnimations = R.style.TopTabTrayAnimation
}
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,
sessionManager = activity.components.core.sessionManager,
browsingModeManager = activity.browsingModeManager,
tabCollectionStorage = activity.components.core.tabCollectionStorage,
navController = findNavController(),
dismissTabTray = ::dismissAllowingStateLoss,
dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome,
registerCollectionStorageObserver = ::registerCollectionStorageObserver,
tabTrayDialogFragmentStore = tabTrayDialogStore,
selectTabUseCase = selectTabUseCase,
showChooseCollectionDialog = ::showChooseCollectionDialog,
showAddNewCollectionDialog = ::showAddNewCollectionDialog
)
),
store = tabTrayDialogStore,
isPrivate = isPrivate,
startingInLandscape = requireContext().resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE,
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 ->
v.updatePadding(
left = insets.systemWindowInsetLeft,
right = insets.systemWindowInsetRight,
bottom = insets.systemWindowInsetBottom
)
tabTrayView.view.tab_wrapper.updatePadding(
bottom = insets.systemWindowInsetBottom
)
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 if (!(activity as HomeActivity).browsingModeManager.mode.isPrivate) {
dialog?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
private fun showUndoSnackbarForTab(sessionId: String) {
val store = requireComponents.core.store
val sessionManager = requireComponents.core.sessionManager
val tab = requireComponents.core.store.state.findTab(sessionId) ?: return
val session = sessionManager.findSessionById(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 isSelected = sessionId == requireComponents.core.store.state.selectedTabId ?: false
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),
{
sessionManager.add(session, isSelected, engineSessionState = tab.engineState.engineSessionState)
_tabTrayView?.scrollToTab(session.id)
},
operation = { },
elevation = ELEVATION,
anchorView = snackbarAnchor
)
}
private fun dismissTabTrayAndNavigateHome(sessionId: String) {
val directions = BrowserFragmentDirections.actionGlobalHome(sessionToDelete = sessionId)
findNavController().navigate(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().navigate(R.id.browserFragment)
}
}
private fun registerCollectionStorageObserver() {
requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this)
}
private fun showCollectionSnackbar(tabSize: Int, isNewCollection: Boolean = false) {
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().navigate(
TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = false)
)
}
snackbar.view.elevation = ELEVATION
snackbar.show()
}
}
override fun onBackPressed(): Boolean {
if (!tabTrayView.onBackPressed()) {
dismiss()
}
return true
}
private fun showChooseCollectionDialog(sessionList: List<Session>) {
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 CollectionsAdapter).getSelectedCollection()
val collection = tabCollectionStorage.cachedTabCollections[selectedCollection]
viewLifecycleOwner.lifecycleScope.launch(Main) {
tabCollectionStorage.addTabsToCollection(collection, sessionList)
it.metrics.track(
Event.CollectionTabsAdded(
it.components.core.sessionManager.normalSessionSize(),
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 =
CollectionsAdapter(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<Session>) {
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, _ ->
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
tabCollectionStorage.createCollection(
collectionNameEditText.text.toString(),
sessionList
)
it.metrics.track(
Event.CollectionSaved(
it.components.core.sessionManager.normalSessionSize(),
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()
}
}
}
companion object {
private const val ELEVATION = 80f
}
}