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.
266 lines
10 KiB
Kotlin
266 lines
10 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.library.bookmarks
|
|
|
|
import android.content.ClipData
|
|
import android.content.ClipboardManager
|
|
import android.content.res.Resources
|
|
import androidx.navigation.NavController
|
|
import androidx.navigation.NavDirections
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.launch
|
|
import mozilla.appservices.places.BookmarkRoot
|
|
import mozilla.components.concept.engine.EngineSession
|
|
import mozilla.components.concept.engine.prompt.ShareData
|
|
import mozilla.components.concept.storage.BookmarkNode
|
|
import mozilla.components.concept.storage.BookmarkNodeType
|
|
import mozilla.components.feature.tabs.TabsUseCases
|
|
import mozilla.components.service.fxa.sync.SyncReason
|
|
import org.mozilla.fenix.BrowserDirection
|
|
import org.mozilla.fenix.HomeActivity
|
|
import org.mozilla.fenix.R
|
|
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
|
import org.mozilla.fenix.ext.bookmarkStorage
|
|
import org.mozilla.fenix.ext.components
|
|
import org.mozilla.fenix.ext.nav
|
|
import org.mozilla.fenix.ext.navigateSafe
|
|
import org.mozilla.fenix.utils.Settings
|
|
|
|
/**
|
|
* [BookmarkFragment] controller.
|
|
* Delegated by View Interactors, handles container business logic and operates changes on it.
|
|
*/
|
|
@Suppress("TooManyFunctions")
|
|
interface BookmarkController {
|
|
fun handleBookmarkChanged(item: BookmarkNode)
|
|
fun handleBookmarkTapped(item: BookmarkNode)
|
|
fun handleBookmarkExpand(folder: BookmarkNode)
|
|
fun handleSelectionModeSwitch()
|
|
fun handleBookmarkEdit(node: BookmarkNode)
|
|
fun handleBookmarkSelected(node: BookmarkNode)
|
|
fun handleBookmarkDeselected(node: BookmarkNode)
|
|
fun handleAllBookmarksDeselected()
|
|
fun handleCopyUrl(item: BookmarkNode)
|
|
fun handleBookmarkSharing(item: BookmarkNode)
|
|
fun handleOpeningBookmark(item: BookmarkNode, mode: BrowsingMode)
|
|
fun handleBookmarkFolderOpening(folder: BookmarkNode, mode: BrowsingMode)
|
|
|
|
/**
|
|
* Handle bookmark nodes deletion
|
|
* @param nodes The set of nodes to be deleted.
|
|
* @param removeType Type of removal.
|
|
*/
|
|
fun handleBookmarkDeletion(nodes: Set<BookmarkNode>, removeType: BookmarkRemoveType)
|
|
fun handleBookmarkFolderDeletion(nodes: Set<BookmarkNode>)
|
|
fun handleRequestSync()
|
|
fun handleBackPressed()
|
|
fun handleSearch()
|
|
}
|
|
|
|
/**
|
|
* Type of bookmark nodes deleted.
|
|
*/
|
|
enum class BookmarkRemoveType {
|
|
SINGLE, MULTIPLE, FOLDER
|
|
}
|
|
|
|
@Suppress("TooManyFunctions", "LongParameterList")
|
|
class DefaultBookmarkController(
|
|
private val activity: HomeActivity,
|
|
private val navController: NavController,
|
|
private val clipboardManager: ClipboardManager?,
|
|
private val scope: CoroutineScope,
|
|
private val store: BookmarkFragmentStore,
|
|
private val sharedViewModel: BookmarksSharedViewModel,
|
|
private val tabsUseCases: TabsUseCases?,
|
|
private val loadBookmarkNode: suspend (String) -> BookmarkNode?,
|
|
private val showSnackbar: (String) -> Unit,
|
|
private val onOpenAllInTabsEmpty: () -> Unit,
|
|
private val deleteBookmarkNodes: (Set<BookmarkNode>, BookmarkRemoveType) -> Unit,
|
|
private val deleteBookmarkFolder: (Set<BookmarkNode>) -> Unit,
|
|
private val showTabTray: () -> Unit,
|
|
private val settings: Settings,
|
|
) : BookmarkController {
|
|
|
|
private val resources: Resources = activity.resources
|
|
|
|
override fun handleBookmarkChanged(item: BookmarkNode) {
|
|
sharedViewModel.selectedFolder = item
|
|
store.dispatch(BookmarkFragmentAction.Change(item))
|
|
}
|
|
|
|
override fun handleBookmarkTapped(item: BookmarkNode) {
|
|
val fromHomeFragment =
|
|
navController.previousBackStackEntry?.destination?.id == R.id.homeFragment
|
|
val isPrivate = activity.browsingModeManager.mode == BrowsingMode.Private
|
|
val flags = EngineSession.LoadUrlFlags.select(EngineSession.LoadUrlFlags.ALLOW_JAVASCRIPT_URL)
|
|
openInNewTabAndShow(
|
|
item.url!!,
|
|
isPrivate || fromHomeFragment,
|
|
BrowserDirection.FromBookmarks,
|
|
activity.browsingModeManager.mode,
|
|
flags,
|
|
)
|
|
}
|
|
|
|
override fun handleBookmarkExpand(folder: BookmarkNode) {
|
|
handleAllBookmarksDeselected()
|
|
scope.launch {
|
|
val node = loadBookmarkNode.invoke(folder.guid) ?: return@launch
|
|
sharedViewModel.selectedFolder = node
|
|
store.dispatch(BookmarkFragmentAction.Change(node))
|
|
}
|
|
}
|
|
|
|
override fun handleSelectionModeSwitch() {
|
|
activity.invalidateOptionsMenu()
|
|
}
|
|
|
|
override fun handleBookmarkEdit(node: BookmarkNode) {
|
|
navigateToGivenDirection(BookmarkFragmentDirections.actionBookmarkFragmentToBookmarkEditFragment(node.guid))
|
|
}
|
|
|
|
override fun handleBookmarkSelected(node: BookmarkNode) {
|
|
if (store.state.mode is BookmarkFragmentState.Mode.Syncing) {
|
|
return
|
|
}
|
|
|
|
if (node.inRoots()) {
|
|
showSnackbar(resources.getString(R.string.bookmark_cannot_edit_root))
|
|
} else {
|
|
store.dispatch(BookmarkFragmentAction.Select(node))
|
|
}
|
|
}
|
|
|
|
override fun handleBookmarkDeselected(node: BookmarkNode) {
|
|
store.dispatch(BookmarkFragmentAction.Deselect(node))
|
|
}
|
|
|
|
override fun handleAllBookmarksDeselected() {
|
|
store.dispatch(BookmarkFragmentAction.DeselectAll)
|
|
}
|
|
|
|
override fun handleCopyUrl(item: BookmarkNode) {
|
|
val urlClipData = ClipData.newPlainText(item.url, item.url)
|
|
clipboardManager?.setPrimaryClip(urlClipData)
|
|
showSnackbar(resources.getString(R.string.url_copied))
|
|
}
|
|
|
|
override fun handleBookmarkSharing(item: BookmarkNode) {
|
|
navigateToGivenDirection(
|
|
BookmarkFragmentDirections.actionGlobalShareFragment(
|
|
data = arrayOf(ShareData(url = item.url, title = item.title)),
|
|
),
|
|
)
|
|
}
|
|
|
|
override fun handleOpeningBookmark(item: BookmarkNode, mode: BrowsingMode) {
|
|
openInNewTab(item.url!!, mode)
|
|
showTabTray()
|
|
}
|
|
|
|
private suspend fun recursiveBookmarkFolderOpening(folder: BookmarkNode, firstLaunch: Boolean = false) {
|
|
val node = loadBookmarkNode.invoke(folder.guid) ?: return
|
|
if (!node.children.isNullOrEmpty()) {
|
|
if (firstLaunch) invokePendingDeletion.invoke()
|
|
|
|
node.children!!.iterator().forEach {
|
|
when (it.type) {
|
|
BookmarkNodeType.FOLDER -> recursiveBookmarkFolderOpening(it, mode = mode)
|
|
BookmarkNodeType.ITEM -> {
|
|
it.url?.let { url -> tabsUseCases?.addTab?.invoke(url, private = (mode == BrowsingMode.Private)) }
|
|
}
|
|
BookmarkNodeType.SEPARATOR -> {}
|
|
}
|
|
}.also {
|
|
if (firstLaunch) {
|
|
activity.browsingModeManager.mode = BrowsingMode.fromBoolean(mode == BrowsingMode.Private)
|
|
showTabTray()
|
|
}
|
|
}
|
|
} else if (firstLaunch) onOpenAllInTabsEmpty.invoke()
|
|
}
|
|
|
|
override fun handleBookmarkFolderOpening(folder: BookmarkNode, mode: BrowsingMode) {
|
|
// potentially heavy function with a lot of bookmarks to open => use a coroutine
|
|
scope.launch {
|
|
recursiveBookmarkFolderOpening(folder, true, mode)
|
|
}
|
|
}
|
|
|
|
override fun handleBookmarkDeletion(nodes: Set<BookmarkNode>, removeType: BookmarkRemoveType) {
|
|
deleteBookmarkNodes(nodes, removeType)
|
|
}
|
|
|
|
override fun handleBookmarkFolderDeletion(nodes: Set<BookmarkNode>) {
|
|
deleteBookmarkFolder(nodes)
|
|
}
|
|
|
|
override fun handleRequestSync() {
|
|
scope.launch {
|
|
store.dispatch(BookmarkFragmentAction.StartSync)
|
|
activity.components.backgroundServices.accountManager.syncNow(SyncReason.User)
|
|
// The current bookmark node we are viewing may be made invalid after syncing so we
|
|
// check if the current node is valid and if it isn't we find the nearest valid ancestor
|
|
// and open it
|
|
val validAncestorGuid = store.state.guidBackstack.findLast { guid ->
|
|
activity.bookmarkStorage.getBookmark(guid) != null
|
|
} ?: BookmarkRoot.Mobile.id
|
|
val node = activity.bookmarkStorage.getBookmark(validAncestorGuid)!!
|
|
handleBookmarkExpand(node)
|
|
store.dispatch(BookmarkFragmentAction.FinishSync)
|
|
}
|
|
}
|
|
|
|
override fun handleBackPressed() {
|
|
scope.launch {
|
|
val parentGuid = store.state.guidBackstack.findLast { guid ->
|
|
store.state.tree?.guid != guid && activity.bookmarkStorage.getBookmark(guid) != null
|
|
}
|
|
if (parentGuid == null) {
|
|
navController.popBackStack()
|
|
} else {
|
|
val parent = activity.bookmarkStorage.getBookmark(parentGuid)!!
|
|
handleBookmarkExpand(parent)
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun handleSearch() {
|
|
val directions = if (settings.showUnifiedSearchFeature) {
|
|
BookmarkFragmentDirections.actionGlobalSearchDialog(sessionId = null)
|
|
} else {
|
|
BookmarkFragmentDirections.actionBookmarkFragmentToBookmarkSearchDialogFragment()
|
|
}
|
|
|
|
navController.navigateSafe(R.id.bookmarkFragment, directions)
|
|
}
|
|
|
|
private fun openInNewTabAndShow(
|
|
searchTermOrURL: String,
|
|
newTab: Boolean,
|
|
from: BrowserDirection,
|
|
mode: BrowsingMode,
|
|
flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(),
|
|
) {
|
|
with(activity) {
|
|
browsingModeManager.mode = mode
|
|
openToBrowserAndLoad(searchTermOrURL, newTab, from, flags = flags)
|
|
}
|
|
}
|
|
|
|
private fun openInNewTab(
|
|
url: String,
|
|
mode: BrowsingMode,
|
|
) {
|
|
activity.browsingModeManager.mode = BrowsingMode.fromBoolean(mode == BrowsingMode.Private)
|
|
tabsUseCases?.addTab?.invoke(url, private = (mode == BrowsingMode.Private))
|
|
}
|
|
|
|
private fun navigateToGivenDirection(directions: NavDirections) {
|
|
navController.nav(R.id.bookmarkFragment, directions)
|
|
}
|
|
}
|