For #1539: Add bookmark multi-select features

nightly-build-test
Colin Lee 5 years ago
parent abb76b4bd4
commit bc1b7e0b43

@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- #1375 - Added setting for turning off history suggestions
- #1139 - Resolved a 170ms delay on cold start
- #176 - Added a swipe to delete gesture on home screen
- #1539 - Added bookmarks multi-select related features
### Changed
- #1429 - Updated site permissions ui for MVP

@ -126,7 +126,8 @@ class HomeFragment : Fragment(), CoroutineScope {
view.menuButton.setOnClickListener {
homeMenu?.menuBuilder?.build(requireContext())?.show(
anchor = it,
orientation = BrowserMenu.Orientation.DOWN)
orientation = BrowserMenu.Orientation.DOWN
)
}
val iconSize = resources.getDimension(R.dimen.preference_icon_drawable_size).toInt()
@ -186,6 +187,13 @@ class HomeFragment : Fragment(), CoroutineScope {
sessionObserver?.onSessionsRestored()
}
override fun onStop() {
sessionObserver?.let {
requireComponents.core.sessionManager.unregister(it)
}
super.onStop()
}
@SuppressWarnings("ComplexMethod")
private fun handleTabAction(action: TabAction) {
Do exhaustive when (action) {
@ -225,8 +233,10 @@ class HomeFragment : Fragment(), CoroutineScope {
is TabAction.PrivateBrowsingLearnMore -> {
requireComponents.useCases.tabsUseCases.addPrivateTab
.invoke(SupportUtils.getSumoURLForTopic(context!!, SupportUtils.SumoTopic.PRIVATE_BROWSING_MYTHS))
(activity as HomeActivity).openToBrowser(requireComponents.core.sessionManager.selectedSession?.id,
BrowserDirection.FromHome)
(activity as HomeActivity).openToBrowser(
requireComponents.core.sessionManager.selectedSession?.id,
BrowserDirection.FromHome
)
}
is TabAction.Add -> {
val directions = HomeFragmentDirections.actionHomeFragmentToSearchFragment(null)

@ -7,6 +7,7 @@ package org.mozilla.fenix.library.bookmarks
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observer
import kotlinx.android.extensions.LayoutContainer
@ -31,6 +32,7 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
private var tree: List<BookmarkNode> = listOf()
private var mode: BookmarkState.Mode = BookmarkState.Mode.Normal
var selected = setOf<BookmarkNode>()
private var isFirstRun = true
lateinit var job: Job
@ -42,6 +44,7 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
false
}
this.mode = mode
this.selected = if (mode is BookmarkState.Mode.Selecting) mode.selectedItems else setOf()
notifyDataSetChanged()
}
@ -53,10 +56,10 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
view, actionEmitter, job
)
BookmarkFolderViewHolder.viewType.ordinal -> BookmarkAdapter.BookmarkFolderViewHolder(
view, actionEmitter
view, actionEmitter, job
)
BookmarkSeparatorViewHolder.viewType.ordinal -> BookmarkAdapter.BookmarkSeparatorViewHolder(
view, actionEmitter
view, actionEmitter, job
)
else -> throw IllegalStateException("ViewType $viewType does not match to a ViewHolder")
}
@ -87,13 +90,24 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is BookmarkAdapter.BookmarkItemViewHolder -> holder.bind(tree[position], mode)
is BookmarkAdapter.BookmarkFolderViewHolder -> holder.bind(tree[position], mode)
is BookmarkAdapter.BookmarkSeparatorViewHolder -> holder.bind(tree[position])
is BookmarkAdapter.BookmarkItemViewHolder -> holder.bind(
tree[position],
mode,
tree[position] in selected
)
is BookmarkAdapter.BookmarkFolderViewHolder -> holder.bind(
tree[position],
mode,
tree[position] in selected
)
is BookmarkAdapter.BookmarkSeparatorViewHolder -> holder.bind(
tree[position], mode,
tree[position] in selected
)
}
}
class BookmarkItemViewHolder(
open class BookmarkNodeViewHolder(
view: View,
val actionEmitter: Observer<BookmarkAction>,
private val job: Job,
@ -104,20 +118,24 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
private var item: BookmarkNode? = null
private var mode: BookmarkState.Mode? = BookmarkState.Mode.Normal
open fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) {}
}
class BookmarkItemViewHolder(
view: View,
actionEmitter: Observer<BookmarkAction>,
job: Job,
override val containerView: View? = view
) :
BookmarkNodeViewHolder(view, actionEmitter, job, containerView) {
override fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) {
init {
bookmark_favicon.visibility = View.VISIBLE
bookmark_title.visibility = View.VISIBLE
bookmark_overflow.visibility = View.VISIBLE
bookmark_separator.visibility = View.GONE
bookmark_layout.isClickable = true
}
fun bind(item: BookmarkNode, mode: BookmarkState.Mode) {
this.item = item
this.mode = mode
val bookmarkItemMenu = BookmarkItemMenu(containerView!!.context, item) {
when (it) {
@ -153,34 +171,56 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
)
}
bookmark_title.text = item.title
updateUrl(item)
updateUrl(item, mode, selected)
}
private fun updateUrl(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) {
setClickListeners(mode, item, selected)
setColorsAndIcons(selected, item)
}
private fun updateUrl(item: BookmarkNode) {
private fun setColorsAndIcons(selected: Boolean, item: BookmarkNode) {
val backgroundTint =
if (selected) R.color.bookmark_selection_appbar_background else R.color.bookmark_favicon_background
val backgroundTintList = ContextCompat.getColorStateList(containerView!!.context, backgroundTint)
bookmark_favicon.backgroundTintList = backgroundTintList
if (selected) bookmark_favicon.setImageResource(R.drawable.mozac_ic_check)
if (!selected && item.url?.startsWith("http") == true) {
launch(Dispatchers.IO) {
val bitmap = BrowserIcons(bookmark_favicon.context, bookmark_layout.context.components.core.client)
.loadIcon(IconRequest(item.url!!)).await().bitmap
launch(Dispatchers.Main) {
bookmark_favicon.setImageBitmap(bitmap)
}
}
}
}
private fun setClickListeners(
mode: BookmarkState.Mode,
item: BookmarkNode,
selected: Boolean
) {
bookmark_layout.setOnClickListener {
if (mode == BookmarkState.Mode.Normal) {
actionEmitter.onNext(BookmarkAction.Open(item))
} else {
actionEmitter.onNext(BookmarkAction.Select(item))
if (selected) actionEmitter.onNext(BookmarkAction.Deselect(item)) else actionEmitter.onNext(
BookmarkAction.Select(item)
)
}
}
bookmark_layout.setOnLongClickListener {
if (mode == BookmarkState.Mode.Normal) {
actionEmitter.onNext(BookmarkAction.Select(item))
if (selected) actionEmitter.onNext(BookmarkAction.Deselect(item)) else actionEmitter.onNext(
BookmarkAction.Select(item)
)
true
} else false
}
if (item.url?.startsWith("http") == true) {
launch(Dispatchers.IO) {
val bitmap = BrowserIcons(bookmark_favicon.context, bookmark_layout.context.components.core.client)
.loadIcon(IconRequest(item.url!!)).await().bitmap
launch(Dispatchers.Main) {
bookmark_favicon.setImageBitmap(bitmap)
}
}
}
}
companion object {
@ -190,39 +230,57 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
class BookmarkFolderViewHolder(
view: View,
val actionEmitter: Observer<BookmarkAction>,
actionEmitter: Observer<BookmarkAction>,
job: Job,
override val containerView: View? = view
) :
RecyclerView.ViewHolder(view), LayoutContainer {
BookmarkNodeViewHolder(view, actionEmitter, job, containerView) {
override fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) {
init {
bookmark_favicon.setImageResource(R.drawable.ic_folder_icon)
bookmark_favicon.visibility = View.VISIBLE
bookmark_title.visibility = View.VISIBLE
bookmark_overflow.visibility = View.VISIBLE
bookmark_separator.visibility = View.GONE
bookmark_layout.isClickable = true
setClickListeners(mode, item, selected)
setMenu(item, containerView!!)
val backgroundTint =
if (selected) R.color.bookmark_selection_appbar_background else R.color.bookmark_favicon_background
val backgroundTintList = ContextCompat.getColorStateList(containerView.context, backgroundTint)
bookmark_favicon.backgroundTintList = backgroundTintList
val res = if (selected) R.drawable.mozac_ic_check else R.drawable.ic_folder_icon
bookmark_favicon.setImageResource(res)
bookmark_title?.text = item.title
}
fun bind(folder: BookmarkNode, mode: BookmarkState.Mode) {
val bookmarkItemMenu = BookmarkItemMenu(containerView!!.context, folder) {
private fun setMenu(
item: BookmarkNode,
containerView: View
) {
val bookmarkItemMenu = BookmarkItemMenu(containerView.context, item) {
when (it) {
is BookmarkItemMenu.Item.Edit -> {
actionEmitter.onNext(BookmarkAction.Edit(folder))
actionEmitter.onNext(BookmarkAction.Edit(item))
}
is BookmarkItemMenu.Item.Select -> {
actionEmitter.onNext(BookmarkAction.Select(folder))
actionEmitter.onNext(BookmarkAction.Select(item))
}
is BookmarkItemMenu.Item.Copy -> {
actionEmitter.onNext(BookmarkAction.Copy(folder))
actionEmitter.onNext(BookmarkAction.Copy(item))
}
is BookmarkItemMenu.Item.Delete -> {
actionEmitter.onNext(BookmarkAction.Delete(folder))
actionEmitter.onNext(BookmarkAction.Delete(item))
}
}
}
if (enumValues<BookmarkRoot>().all { it.id != folder.guid }) {
if (enumValues<BookmarkRoot>().all { it.id != item.guid }) {
bookmark_overflow.increaseTapArea(bookmarkOverflowExtraDips)
bookmark_overflow.setOnClickListener {
bookmarkItemMenu.menuBuilder.build(containerView.context).show(
@ -233,9 +291,30 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
} else {
bookmark_overflow.visibility = View.GONE
}
bookmark_title?.text = folder.title
}
private fun setClickListeners(
mode: BookmarkState.Mode,
item: BookmarkNode,
selected: Boolean
) {
bookmark_layout.setOnClickListener {
actionEmitter.onNext(BookmarkAction.Expand(folder))
if (mode == BookmarkState.Mode.Normal) {
actionEmitter.onNext(BookmarkAction.Expand(item))
} else {
if (selected) actionEmitter.onNext(BookmarkAction.Deselect(item)) else actionEmitter.onNext(
BookmarkAction.Select(item)
)
}
}
bookmark_layout.setOnLongClickListener {
if (mode == BookmarkState.Mode.Normal) {
if (selected) actionEmitter.onNext(BookmarkAction.Deselect(item)) else actionEmitter.onNext(
BookmarkAction.Select(item)
)
true
} else false
}
}
@ -246,24 +325,24 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
class BookmarkSeparatorViewHolder(
view: View,
val actionEmitter: Observer<BookmarkAction>,
actionEmitter: Observer<BookmarkAction>,
job: Job,
override val containerView: View? = view
) : RecyclerView.ViewHolder(view), LayoutContainer {
) : BookmarkNodeViewHolder(view, actionEmitter, job, containerView) {
override fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) {
init {
bookmark_favicon.visibility = View.GONE
bookmark_title.visibility = View.GONE
bookmark_overflow.increaseTapArea(bookmarkOverflowExtraDips)
bookmark_overflow.visibility = View.VISIBLE
bookmark_separator.visibility = View.VISIBLE
bookmark_layout.isClickable = false
}
fun bind(separator: BookmarkNode) {
val bookmarkItemMenu = BookmarkItemMenu(containerView!!.context, separator) {
val bookmarkItemMenu = BookmarkItemMenu(containerView!!.context, item) {
when (it) {
is BookmarkItemMenu.Item.Delete -> {
actionEmitter.onNext(BookmarkAction.Delete(separator))
actionEmitter.onNext(BookmarkAction.Delete(item))
}
}
}

@ -13,7 +13,9 @@ import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIView
import org.mozilla.fenix.mvi.ViewState
import org.mozilla.fenix.test.Mockable
@Mockable
class BookmarkComponent(
private val container: ViewGroup,
bus: ActionBusFactory,
@ -28,7 +30,28 @@ class BookmarkComponent(
override val reducer: Reducer<BookmarkState, BookmarkChange> = { state, change ->
when (change) {
is BookmarkChange.Change -> {
state.copy(tree = change.tree)
val mode =
if (state.mode is BookmarkState.Mode.Selecting) {
BookmarkState.Mode.Selecting(state.mode.selectedItems.filter {
it in change.tree
}.toSet())
} else state.mode
state.copy(tree = change.tree, mode = mode)
}
is BookmarkChange.IsSelected -> {
val selectedItems = if (state.mode is BookmarkState.Mode.Selecting) {
state.mode.selectedItems + change.newlySelectedItem
} else setOf(change.newlySelectedItem)
state.copy(mode = BookmarkState.Mode.Selecting(selectedItems))
}
is BookmarkChange.IsDeselected -> {
val selectedItems = if (state.mode is BookmarkState.Mode.Selecting) {
state.mode.selectedItems - change.newlyDeselectedItem
} else setOf()
val mode = if (selectedItems.isEmpty()) BookmarkState.Mode.Normal else BookmarkState.Mode.Selecting(
selectedItems
)
state.copy(mode = mode)
}
}
}
@ -44,7 +67,7 @@ class BookmarkComponent(
data class BookmarkState(val tree: BookmarkNode?, val mode: BookmarkState.Mode) : ViewState {
sealed class Mode {
object Normal : Mode()
data class Selecting(val selectedItems: List<BookmarkNode>) : Mode()
data class Selecting(val selectedItems: Set<BookmarkNode>) : Mode()
}
}
@ -57,11 +80,18 @@ sealed class BookmarkAction : Action {
data class OpenInNewTab(val item: BookmarkNode) : BookmarkAction()
data class OpenInPrivateTab(val item: BookmarkNode) : BookmarkAction()
data class Select(val item: BookmarkNode) : BookmarkAction()
data class Deselect(val item: BookmarkNode) : BookmarkAction()
data class Delete(val item: BookmarkNode) : BookmarkAction()
object ExitSelectMode : BookmarkAction()
object BackPressed : BookmarkAction()
object ModeChanged : BookmarkAction()
}
sealed class BookmarkChange : Change {
data class Change(val tree: BookmarkNode) : BookmarkChange()
data class IsSelected(val newlySelectedItem: BookmarkNode) : BookmarkChange()
data class IsDeselected(val newlyDeselectedItem: BookmarkNode) : BookmarkChange()
}
operator fun BookmarkNode.contains(item: BookmarkNode): Boolean {
return children?.contains(item) ?: false
}

@ -4,6 +4,12 @@
package org.mozilla.fenix.library.bookmarks
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
@ -11,7 +17,12 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.menu.ActionMenuItemView
import androidx.appcompat.widget.ActionMenuView
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.navigation.Navigation
import kotlinx.android.synthetic.main.fragment_bookmark.view.*
@ -20,6 +31,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.concept.storage.BookmarkNode
@ -32,6 +44,8 @@ import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.BrowsingModeManager
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.ext.getColorFromAttr
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.share
import org.mozilla.fenix.mvi.ActionBusFactory
@ -46,7 +60,7 @@ class BookmarkFragment : Fragment(), CoroutineScope, BackHandler, AccountObserve
private lateinit var job: Job
private lateinit var bookmarkComponent: BookmarkComponent
private lateinit var signInComponent: SignInComponent
private lateinit var currentRoot: BookmarkNode
private var currentRoot: BookmarkNode? = null
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
@ -61,12 +75,12 @@ class BookmarkFragment : Fragment(), CoroutineScope, BackHandler, AccountObserve
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
(activity as AppCompatActivity).title = getString(R.string.library_bookmarks)
setHasOptionsMenu(true)
}
override fun onResume() {
super.onResume()
(activity as AppCompatActivity).title = getString(R.string.library_bookmarks)
(activity as AppCompatActivity).supportActionBar?.show()
checkIfSignedIn()
}
@ -84,7 +98,67 @@ class BookmarkFragment : Fragment(), CoroutineScope, BackHandler, AccountObserve
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.library_menu, menu)
val toolbar = (activity as AppCompatActivity).findViewById<Toolbar>(R.id.navigationToolbar)
when (val mode = (bookmarkComponent.uiView as BookmarkUIView).mode) {
BookmarkState.Mode.Normal -> {
inflater.inflate(R.menu.library_menu, menu)
activity?.title =
if (currentRoot?.title in setOf(
"root",
null
)
) getString(R.string.library_bookmarks) else currentRoot!!.title
toolbar.setBackgroundColor(R.attr.toolbarColor.getColorFromAttr(context!!))
toolbar.setTitleTextColor(R.attr.toolbarTextColor.getColorFromAttr(context!!))
}
is BookmarkState.Mode.Selecting -> {
inflater.inflate(R.menu.bookmarks_select_multi, menu)
activity?.title = getString(R.string.bookmarks_multi_select_title, mode.selectedItems.size)
val colorFilter =
PorterDuffColorFilter(ContextCompat.getColor(context!!, R.color.off_white), PorterDuff.Mode.SRC_IN)
themeToolbar(toolbar, colorFilter)
}
}
}
private fun themeToolbar(
toolbar: Toolbar,
colorFilter: PorterDuffColorFilter
) {
toolbar.setBackgroundColor(ContextCompat.getColor(context!!, R.color.bookmark_multi_select_actionbar))
toolbar.setTitleTextColor(ContextCompat.getColor(context!!, R.color.off_white))
toolbar.overflowIcon?.colorFilter = colorFilter
(0 until toolbar.childCount).forEach {
when (val item = toolbar.getChildAt(it)) {
is ImageButton -> item.drawable.colorFilter = colorFilter
is ActionMenuView -> themeActionMenuView(item, colorFilter)
}
}
}
private fun themeActionMenuView(
item: ActionMenuView,
colorFilter: PorterDuffColorFilter
) {
(0 until item.childCount).forEach {
val innerChild = item.getChildAt(it)
if (innerChild is ActionMenuItemView) {
themeChildren(innerChild, item, colorFilter)
}
}
}
private fun themeChildren(
innerChild: ActionMenuItemView,
item: ActionMenuView,
colorFilter: PorterDuffColorFilter
) {
val drawables = innerChild.compoundDrawables
for (k in drawables.indices) {
drawables[k]?.let {
item.post { innerChild.compoundDrawables[k].colorFilter = colorFilter }
}
}
}
@SuppressWarnings("ComplexMethod")
@ -116,10 +190,15 @@ class BookmarkFragment : Fragment(), CoroutineScope, BackHandler, AccountObserve
)
}
is BookmarkAction.Select -> {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "1239")
getManagedEmitter<BookmarkChange>().onNext(BookmarkChange.IsSelected(it.item))
}
is BookmarkAction.Deselect -> {
getManagedEmitter<BookmarkChange>().onNext(BookmarkChange.IsDeselected(it.item))
}
is BookmarkAction.Copy -> {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "1239")
it.item.copyUrl(context!!)
FenixSnackbar.make(view!!, FenixSnackbar.LENGTH_LONG)
.setText(context!!.getString(R.string.url_copied)).show()
}
is BookmarkAction.Share -> {
it.item.url?.let { url -> requireContext().share(url) }
@ -139,15 +218,10 @@ class BookmarkFragment : Fragment(), CoroutineScope, BackHandler, AccountObserve
is BookmarkAction.Delete -> {
launch(IO) {
requireComponents.core.bookmarksStorage.deleteNode(it.item.guid)
requireComponents.core.bookmarksStorage.getTree(currentRoot.guid, false)
?.let { node ->
getManagedEmitter<BookmarkChange>().onNext(BookmarkChange.Change(node))
}
refreshBookmarks()
}
}
is BookmarkAction.ExitSelectMode -> {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "1239")
}
is BookmarkAction.ModeChanged -> activity?.invalidateOptionsMenu()
}
}
@ -162,6 +236,7 @@ class BookmarkFragment : Fragment(), CoroutineScope, BackHandler, AccountObserve
}
}
@SuppressWarnings("ComplexMethod")
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.libraryClose -> {
@ -173,6 +248,47 @@ class BookmarkFragment : Fragment(), CoroutineScope, BackHandler, AccountObserve
// TODO Library Search
true
}
R.id.open_bookmarks_in_new_tabs_multi_select -> {
getSelectedBookmarks().forEach { node ->
node.url?.let {
requireComponents.useCases.tabsUseCases.addTab.invoke(it)
}
}
(activity as HomeActivity).browsingModeManager.mode = BrowsingModeManager.Mode.Normal
Navigation.findNavController(requireActivity(), R.id.container)
.navigate(BookmarkFragmentDirections.actionBookmarkFragmentToHomeFragment())
true
}
R.id.share_bookmarks_multi_select -> {
ItsNotBrokenSnack(context!!).showSnackbar(issueNumber = "1539")
true
}
R.id.open_bookmarks_in_private_tabs_multi_select -> {
getSelectedBookmarks().forEach { node ->
node.url?.let {
requireComponents.useCases.tabsUseCases.addPrivateTab.invoke(it)
}
}
(activity as HomeActivity).browsingModeManager.mode = BrowsingModeManager.Mode.Private
Navigation.findNavController(requireActivity(), R.id.container)
.navigate(BookmarkFragmentDirections.actionBookmarkFragmentToHomeFragment())
true
}
R.id.delete_bookmarks_multi_select -> {
val deleteJob = launch(IO) {
delay(bookmarkDeletionDelay)
deleteSelectedBookmarks()
refreshBookmarks()
}
FenixSnackbar.make(view!!, FenixSnackbar.LENGTH_LONG)
.setText(getString(R.string.bookmark_deletion_multiple_snackbar_message))
.setAction(getString(R.string.bookmark_undo_deletion)) {
deleteJob.cancel()
}.show()
true
}
else -> super.onOptionsItemSelected(item)
}
}
@ -186,8 +302,8 @@ class BookmarkFragment : Fragment(), CoroutineScope, BackHandler, AccountObserve
currentRoot = requireComponents.core.bookmarksStorage.getTree(currentGuid) as BookmarkNode
launch(Main) {
if (currentGuid != BookmarkRoot.Root.id) (activity as HomeActivity).title = currentRoot.title
getManagedEmitter<BookmarkChange>().onNext(BookmarkChange.Change(currentRoot))
if (currentGuid != BookmarkRoot.Root.id) (activity as HomeActivity).title = currentRoot!!.title
getManagedEmitter<BookmarkChange>().onNext(BookmarkChange.Change(currentRoot!!))
}
}
}
@ -207,4 +323,29 @@ class BookmarkFragment : Fragment(), CoroutineScope, BackHandler, AccountObserve
override fun onProfileUpdated(profile: Profile) {
}
private fun getSelectedBookmarks() = (bookmarkComponent.uiView as BookmarkUIView).getSelected()
private suspend fun deleteSelectedBookmarks() {
getSelectedBookmarks().forEach {
requireComponents.core.bookmarksStorage.deleteNode(it.guid)
}
}
private suspend fun refreshBookmarks() {
requireComponents.core.bookmarksStorage.getTree(currentRoot!!.guid, false)
?.let { node ->
getManagedEmitter<BookmarkChange>().onNext(BookmarkChange.Change(node))
}
}
private fun BookmarkNode.copyUrl(context: Context) {
val clipBoard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val uri = Uri.parse(url)
clipBoard.primaryClip = ClipData.newRawUri("Uri", uri)
}
companion object {
private const val bookmarkDeletionDelay = 3000L
}
}

@ -12,6 +12,7 @@ import io.reactivex.Observer
import io.reactivex.functions.Consumer
import kotlinx.android.synthetic.main.component_bookmark.view.*
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.support.base.feature.BackHandler
import org.mozilla.fenix.R
import org.mozilla.fenix.mvi.UIView
@ -44,7 +45,10 @@ class BookmarkUIView(
override fun updateView() = Consumer<BookmarkState> {
canGoBack = !(listOf(null, BookmarkRoot.Root.id).contains(it.tree?.guid))
bookmarkAdapter.updateData(it.tree, it.mode)
mode = it.mode
if (it.mode != mode) {
mode = it.mode
actionEmitter.onNext(BookmarkAction.ModeChanged)
}
}
override fun onBackPressed(): Boolean {
@ -53,4 +57,6 @@ class BookmarkUIView(
true
} else false
}
fun getSelected(): Set<BookmarkNode> = bookmarkAdapter.selected
}

@ -8,7 +8,7 @@
android:id="@+id/bookmark_folders_sign_in"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/sign_in_button"
android:text="@string/bookmark_sign_in_button"
android:padding="10dp"
android:layout_marginTop="32dp"
android:textSize="14sp"

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<menu 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">
<item
android:id="@+id/share_bookmarks_multi_select"
android:icon="@drawable/ic_send"
android:iconTint="?attr/iconColor"
android:title="@string/bookmark_add_folder"
app:showAsAction="ifRoom"
tools:targetApi="o" />
<item
android:id="@+id/open_bookmarks_in_new_tabs_multi_select"
android:icon="@drawable/ic_new"
android:iconTint="?attr/iconColor"
android:title="@string/bookmark_menu_open_in_new_tab_button"
app:showAsAction="never"
tools:targetApi="o" />
<item
android:id="@+id/open_bookmarks_in_private_tabs_multi_select"
android:icon="@drawable/ic_new"
android:iconTint="?attr/iconColor"
android:title="@string/bookmark_menu_open_in_private_tab_button"
app:showAsAction="never"
tools:targetApi="o" />
<item
android:id="@+id/delete_bookmarks_multi_select"
android:icon="@drawable/ic_new"
android:iconTint="?attr/iconColor"
android:title="@string/bookmark_menu_delete_button"
app:showAsAction="never"
tools:targetApi="o" />
</menu>

@ -121,6 +121,9 @@
<action
android:id="@+id/action_bookmarkFragment_to_bookmarkEditFragment"
app:destination="@id/bookmarkEditFragment" />
<action
android:id="@+id/action_bookmarkFragment_to_homeFragment"
app:destination="@id/homeFragment" />
</fragment>
<fragment

@ -58,6 +58,7 @@
<!-- Bookmarks Fragment -->
<attr name="bookmarksLabelColor" format="reference" />
<attr name="bookmarksEditTextColor" format="reference" />
<attr name="bookmarkSelectionTitleColor" format="reference"/>
<!-- Library Fragment -->
<attr name="libraryListItemTextColor" format="reference" />

@ -15,6 +15,7 @@
<!-- Bookmarks -->
<color name="bookmark_favicon_background">#DFDFE3</color>
<color name="bookmark_multi_select_actionbar">#592ACB</color>
<color name="bookmark_snackbar_background">#2E0EC1</color>
<color name="bookmark_selection_appbar_background">#2E0EC1</color>
@ -37,6 +38,7 @@
<color name="history_header_normal_theme">#696A6A</color>
<color name="history_title_normal_theme">@color/text_color_normal_theme</color>
<color name="history_url_normal_theme">#696A6A</color>
<color name="bookmark_selection_title_normal_theme">@color/off_white</color>
<color name="icons_normal_theme">#20123A</color>
<color name="disabled_icons_normal_theme">#8020233E</color>
<color name="status_bar_color_normal_theme">@color/off_white</color>
@ -103,6 +105,7 @@
<color name="history_header_private_theme">@color/photonGrey40</color>
<color name="history_title_private_theme">@color/off_white</color>
<color name="history_url_private_theme">@color/photonGrey40</color>
<color name="bookmark_selection_title_private_theme">@color/off_white</color>
<color name="unloaded_progress_private_theme">#4f4e75</color>
<!-- Library Colors -->

@ -305,16 +305,35 @@
<string name="bookmark_menu_open_in_private_tab_button">Open in private tab</string>
<!-- Bookmark overflow menu delete button -->
<string name="bookmark_menu_delete_button">Delete</string>
<!-- Bookmark multi select title in app bar -->
<string name="bookmarks_multi_select_title">%1$d selected</string>
<!-- Bookmark editing screen title -->
<string name="edit_bookmark_fragment_title">Edit bookmark</string>
<string name="sign_in_button">Sign in to see synced bookmarks</string>
<!-- Bookmark sign in button message -->
<string name="bookmark_sign_in_button">Sign in to see synced bookmarks</string>
<!-- Bookmark URL editing field label -->
<string name="bookmark_url_label">URL</string>
<!-- Bookmark FOLDER editing field label -->
<string name="bookmark_folder_label">FOLDER</string>
<!-- Bookmark NAME editing field label -->
<string name="bookmark_name_label">NAME</string>
<!-- Bookmark add folder screen title -->
<string name="bookmark_add_folder_fragment_label">Add folder</string>
<!-- Bookmark select folder screen title -->
<string name="bookmark_select_folder_fragment_label">Select folder</string>
<!-- Bookmark editing error missing title -->
<string name="bookmark_empty_title_error">Must have a title</string>
<!-- Bookmark editing error missing or improper URL -->
<string name="bookmark_invalid_url_error">Invalid URL</string>
<!-- Bookmark screen message for empty bookmarks folder -->
<string name="bookmarks_empty_message">No bookmarks here</string>
<!-- Bookmark snackbar message on deletion -->
<string name="bookmark_deletion_snackbar_message">Deleted %1$s</string>
<!-- Bookmark snackbar message on deleting multiple bookmarks -->
<string name="bookmark_deletion_multiple_snackbar_message">Deleting selected bookmarks</string>
<!-- Bookmark undo button for deletion snackbar action -->
<string name="bookmark_undo_deletion">UNDO</string>
<!-- Message for copying the URL via long press on the toolbar -->
<string name="url_copied">URL copied</string>
</resources>

@ -76,6 +76,7 @@
<!-- Bookmark fragment colors -->
<item name="bookmarksLabelColor">@color/bookmarks_label_normal_theme</item>
<item name="bookmarksEditTextColor">@color/bookmarks_edit_normal_theme</item>
<item name="bookmarkSelectionTitleColor">@color/bookmark_selection_title_normal_theme</item>
<!-- Library Fragment -->
<item name="libraryListItemTextColor">@color/library_list_item_text_color_light_mode</item>
@ -148,6 +149,7 @@
<!-- Bookmark fragment colors -->
<item name="bookmarksLabelColor">@color/bookmarks_label_private_theme</item>
<item name="bookmarksEditTextColor">@color/bookmarks_edit_private_theme</item>
<item name="bookmarkSelectionTitleColor">@color/bookmark_selection_title_private_theme</item>
<!-- Library Fragment -->
<item name="libraryListItemTextColor">@color/off_white</item>
@ -241,23 +243,24 @@
<item name="android:progressDrawable">@drawable/progress_gradient</item>
</style>
<style name="QuickSettingsText">
<item name="android:textColor">?attr/toolbarTextColor</item>
<item name="android:textSize">14sp</item>
<item name="android:paddingStart">16dp</item>
<item name="android:gravity">center_vertical</item>
<item name="android:layout_alignParentStart">true</item>
</style>
<style name="QuickSettingsText.Icon">
<item name="android:drawablePadding">8dp</item>
</style>
<style name="QuickSettingsText.PermissionItemEnd">
<item name="android:layout_alignParentEnd">true</item>
<item name="android:paddingEnd">24dp</item>
<item name="android:gravity">end|center_vertical</item>
<item name="android:background">?android:attr/selectableItemBackground</item>
<item name="android:textColor">@color/photonBlue50</item>
</style>
<style name="QuickSettingsText">
<item name="android:textColor">?attr/toolbarTextColor</item>
<item name="android:textSize">14sp</item>
<item name="android:paddingStart">16dp</item>
<item name="android:gravity">center_vertical</item>
<item name="android:layout_alignParentStart">true</item>
</style>
<style name="QuickSettingsText.Icon">
<item name="android:drawablePadding">8dp</item>
</style>
<style name="QuickSettingsText.PermissionItemEnd">
<item name="android:layout_alignParentEnd">true</item>
<item name="android:paddingEnd">24dp</item>
<item name="android:gravity">end|center_vertical</item>
<item name="android:background">?android:attr/selectableItemBackground</item>
<item name="android:textColor">@color/photonBlue50</item>
</style>
</resources>

@ -0,0 +1,72 @@
/* 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.view.ViewGroup
import io.mockk.MockKAnnotations
import io.mockk.mockk
import io.mockk.spyk
import io.reactivex.Observer
import io.reactivex.observers.TestObserver
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mozilla.fenix.TestUtils
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.UIView
import org.mozilla.fenix.mvi.getManagedEmitter
class BookmarkComponentTest {
private lateinit var bookmarkComponent: BookmarkComponentTest.TestBookmarkComponent
private lateinit var bookmarkObserver: TestObserver<BookmarkState>
private lateinit var emitter: Observer<BookmarkChange>
@BeforeEach
fun setup() {
MockKAnnotations.init(this)
TestUtils.setRxSchedulers()
bookmarkComponent = spyk(
BookmarkComponentTest.TestBookmarkComponent(mockk(), TestUtils.bus),
recordPrivateCalls = true
)
bookmarkObserver = bookmarkComponent.internalRender(bookmarkComponent.reducer).test()
emitter = TestUtils.owner.getManagedEmitter()
}
@Test
fun `select and deselect a bookmark`() {
val itemToSelect = BookmarkNode(BookmarkNodeType.ITEM, "234", "123", 0, "Mozilla", "http://mozilla.org", null)
val separator = BookmarkNode(BookmarkNodeType.SEPARATOR, "345", "123", 1, null, null, null)
val innerFolder = BookmarkNode(BookmarkNodeType.FOLDER, "456", "123", 2, "Web Browsers", null, null)
val tree = BookmarkNode(
BookmarkNodeType.FOLDER, "123", BookmarkRoot.Mobile.id, 0, "Best Sites", null,
listOf(itemToSelect, separator, innerFolder)
)
emitter.onNext(BookmarkChange.Change(tree))
emitter.onNext(BookmarkChange.IsSelected(itemToSelect))
emitter.onNext(BookmarkChange.IsDeselected(itemToSelect))
bookmarkObserver.assertSubscribed().awaitCount(2).assertNoErrors()
.assertValues(
BookmarkState(null, BookmarkState.Mode.Normal),
BookmarkState(tree, BookmarkState.Mode.Normal),
BookmarkState(tree, BookmarkState.Mode.Selecting(setOf(itemToSelect))),
BookmarkState(tree, BookmarkState.Mode.Normal)
)
}
@Suppress("MemberVisibilityCanBePrivate")
class TestBookmarkComponent(container: ViewGroup, bus: ActionBusFactory) :
BookmarkComponent(container, bus) {
override val uiView: UIView<BookmarkState, BookmarkAction, BookmarkChange>
get() = mockk(relaxed = true)
}
}

@ -3,7 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
private object Versions {
const val kotlin = "1.3.11"
const val kotlin = "1.3.21"
const val coroutines = "1.2.0-alpha-2"
const val android_gradle_plugin = "3.3.2"
const val rxAndroid = "2.1.0"

Loading…
Cancel
Save