parent
4747f2c165
commit
3dc20543e3
|
@ -412,6 +412,7 @@ dependencies {
|
||||||
testImplementation Deps.mockito_core
|
testImplementation Deps.mockito_core
|
||||||
androidTestImplementation Deps.mockito_android
|
androidTestImplementation Deps.mockito_android
|
||||||
testImplementation Deps.mockk
|
testImplementation Deps.mockk
|
||||||
|
testImplementation Deps.assertk
|
||||||
|
|
||||||
debugImplementation Deps.flipper
|
debugImplementation Deps.flipper
|
||||||
debugImplementation Deps.soLoader
|
debugImplementation Deps.soLoader
|
||||||
|
|
|
@ -137,3 +137,18 @@ private class FenixSnackbarCallback(
|
||||||
private const val animateOutDuration = 150L
|
private const val animateOutDuration = 150L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FenixSnackbarPresenter(
|
||||||
|
private val view: View
|
||||||
|
) {
|
||||||
|
fun present(
|
||||||
|
text: String,
|
||||||
|
length: Int = FenixSnackbar.LENGTH_LONG,
|
||||||
|
action: (() -> Unit)? = null,
|
||||||
|
actionName: String? = null
|
||||||
|
) {
|
||||||
|
FenixSnackbar.make(view, length).setText(text).let {
|
||||||
|
if (action != null && actionName != null) it.setAction(actionName, action) else it
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,8 +4,16 @@
|
||||||
|
|
||||||
package org.mozilla.fenix.components
|
package org.mozilla.fenix.components
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.navigation.NavController
|
||||||
import mozilla.components.service.fxa.manager.FxaAccountManager
|
import mozilla.components.service.fxa.manager.FxaAccountManager
|
||||||
|
import mozilla.components.support.ktx.android.content.hasCamera
|
||||||
|
import org.mozilla.fenix.Experiments
|
||||||
|
import org.mozilla.fenix.NavGraphDirections
|
||||||
import org.mozilla.fenix.components.features.FirefoxAccountsAuthFeature
|
import org.mozilla.fenix.components.features.FirefoxAccountsAuthFeature
|
||||||
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.isInExperiment
|
||||||
import org.mozilla.fenix.test.Mockable
|
import org.mozilla.fenix.test.Mockable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -21,4 +29,26 @@ class Services(
|
||||||
redirectUrl = BackgroundServices.REDIRECT_URL
|
redirectUrl = BackgroundServices.REDIRECT_URL
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launches the sign in and pairing custom tab from any screen in the app.
|
||||||
|
* @param context the current Context
|
||||||
|
* @param navController the navController to use for navigation
|
||||||
|
*/
|
||||||
|
fun launchPairingSignIn(context: Context, navController: NavController) {
|
||||||
|
// Do not navigate to pairing UI if camera not available or pairing is disabled
|
||||||
|
if (context.hasCamera() && !context.isInExperiment(Experiments.asFeatureFxAPairingDisabled)
|
||||||
|
) {
|
||||||
|
val directions = NavGraphDirections.actionGlobalTurnOnSync()
|
||||||
|
navController.navigate(directions)
|
||||||
|
} else {
|
||||||
|
context.components.services.accountsAuthFeature.beginAuthentication(context)
|
||||||
|
// TODO The sign-in web content populates session history,
|
||||||
|
// so pressing "back" after signing in won't take us back into the settings screen, but rather up the
|
||||||
|
// session history stack.
|
||||||
|
// We could auto-close this tab once we get to the end of the authentication process?
|
||||||
|
// Via an interceptor, perhaps.
|
||||||
|
context.components.analytics.metrics.track(Event.SyncAuthSignIn)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,10 @@
|
||||||
|
|
||||||
package org.mozilla.fenix.ext
|
package org.mozilla.fenix.ext
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
import mozilla.appservices.places.BookmarkRoot
|
import mozilla.appservices.places.BookmarkRoot
|
||||||
import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
|
import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
|
||||||
import mozilla.components.concept.storage.BookmarkNode
|
import mozilla.components.concept.storage.BookmarkNode
|
||||||
|
@ -132,3 +135,13 @@ operator fun BookmarkNode?.minus(child: String): BookmarkNode {
|
||||||
operator fun BookmarkNode?.minus(children: Set<BookmarkNode>): BookmarkNode {
|
operator fun BookmarkNode?.minus(children: Set<BookmarkNode>): BookmarkNode {
|
||||||
return this!!.copy(children = this.children?.filter { it !in children })
|
return this!!.copy(children = this.children?.filter { it !in children })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the URL of the given bookmarkNode into the copy and paste buffer.
|
||||||
|
* @param context the current Context
|
||||||
|
*/
|
||||||
|
fun BookmarkNode.copyUrl(context: Context) {
|
||||||
|
context.getSystemService<ClipboardManager>()?.apply {
|
||||||
|
primaryClip = ClipData.newPlainText(url, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
package org.mozilla.fenix.library
|
package org.mozilla.fenix.library
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.ColorFilter
|
import android.graphics.ColorFilter
|
||||||
import android.graphics.PorterDuff
|
import android.graphics.PorterDuff
|
||||||
import android.graphics.PorterDuffColorFilter
|
import android.graphics.PorterDuffColorFilter
|
||||||
|
@ -15,27 +16,14 @@ import androidx.appcompat.view.menu.ActionMenuItemView
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.forEach
|
import androidx.core.view.forEach
|
||||||
import io.reactivex.Observable
|
|
||||||
import io.reactivex.Observer
|
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.ext.asActivity
|
import org.mozilla.fenix.ext.asActivity
|
||||||
import org.mozilla.fenix.mvi.Action
|
|
||||||
import org.mozilla.fenix.mvi.Change
|
|
||||||
import org.mozilla.fenix.mvi.UIView
|
|
||||||
import org.mozilla.fenix.mvi.ViewState
|
|
||||||
|
|
||||||
/**
|
open class LibraryPageView(
|
||||||
* Shared base class for [org.mozilla.fenix.library.bookmarks.BookmarkUIView] and
|
container: ViewGroup
|
||||||
* [org.mozilla.fenix.library.history.HistoryUIView].
|
) {
|
||||||
*/
|
protected val context: Context = container.context
|
||||||
abstract class LibraryPageUIView<S : ViewState, A : Action, C : Change>(
|
protected val activity = context.asActivity()
|
||||||
container: ViewGroup,
|
|
||||||
actionEmitter: Observer<A>,
|
|
||||||
changesObservable: Observable<C>
|
|
||||||
) : UIView<S, A, C>(container, actionEmitter, changesObservable) {
|
|
||||||
|
|
||||||
protected val context = container.context
|
|
||||||
protected val activity = context?.asActivity()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adjust the colors of the [Toolbar] on the top of the screen.
|
* Adjust the colors of the [Toolbar] on the top of the screen.
|
|
@ -11,7 +11,6 @@ import android.view.ViewGroup
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.reactivex.Observer
|
|
||||||
import kotlinx.android.extensions.LayoutContainer
|
import kotlinx.android.extensions.LayoutContainer
|
||||||
import kotlinx.android.synthetic.main.bookmark_row.*
|
import kotlinx.android.synthetic.main.bookmark_row.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -30,7 +29,7 @@ import org.mozilla.fenix.ext.increaseTapArea
|
||||||
import org.mozilla.fenix.utils.AdapterWithJob
|
import org.mozilla.fenix.utils.AdapterWithJob
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkAction>) :
|
class BookmarkAdapter(val emptyView: View, val interactor: BookmarkViewInteractor) :
|
||||||
AdapterWithJob<BookmarkAdapter.BookmarkNodeViewHolder>() {
|
AdapterWithJob<BookmarkAdapter.BookmarkNodeViewHolder>() {
|
||||||
|
|
||||||
private var tree: List<BookmarkNode> = listOf()
|
private var tree: List<BookmarkNode> = listOf()
|
||||||
|
@ -87,13 +86,13 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
|
||||||
|
|
||||||
return when (viewType) {
|
return when (viewType) {
|
||||||
BookmarkItemViewHolder.viewType.ordinal -> BookmarkItemViewHolder(
|
BookmarkItemViewHolder.viewType.ordinal -> BookmarkItemViewHolder(
|
||||||
view, actionEmitter, adapterJob
|
view, interactor, adapterJob
|
||||||
)
|
)
|
||||||
BookmarkFolderViewHolder.viewType.ordinal -> BookmarkFolderViewHolder(
|
BookmarkFolderViewHolder.viewType.ordinal -> BookmarkFolderViewHolder(
|
||||||
view, actionEmitter, adapterJob
|
view, interactor, adapterJob
|
||||||
)
|
)
|
||||||
BookmarkSeparatorViewHolder.viewType.ordinal -> BookmarkSeparatorViewHolder(
|
BookmarkSeparatorViewHolder.viewType.ordinal -> BookmarkSeparatorViewHolder(
|
||||||
view, actionEmitter, adapterJob
|
view, interactor, adapterJob
|
||||||
)
|
)
|
||||||
else -> throw IllegalStateException("ViewType $viewType does not match to a ViewHolder")
|
else -> throw IllegalStateException("ViewType $viewType does not match to a ViewHolder")
|
||||||
}
|
}
|
||||||
|
@ -120,7 +119,7 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
|
||||||
|
|
||||||
open class BookmarkNodeViewHolder(
|
open class BookmarkNodeViewHolder(
|
||||||
view: View,
|
view: View,
|
||||||
val actionEmitter: Observer<BookmarkAction>,
|
val interactor: BookmarkViewInteractor,
|
||||||
private val job: Job,
|
private val job: Job,
|
||||||
override val containerView: View? = view
|
override val containerView: View? = view
|
||||||
) :
|
) :
|
||||||
|
@ -134,11 +133,11 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
|
||||||
|
|
||||||
class BookmarkItemViewHolder(
|
class BookmarkItemViewHolder(
|
||||||
view: View,
|
view: View,
|
||||||
actionEmitter: Observer<BookmarkAction>,
|
interactor: BookmarkViewInteractor,
|
||||||
job: Job,
|
job: Job,
|
||||||
override val containerView: View? = view
|
override val containerView: View? = view
|
||||||
) :
|
) :
|
||||||
BookmarkNodeViewHolder(view, actionEmitter, job, containerView) {
|
BookmarkNodeViewHolder(view, interactor, job, containerView) {
|
||||||
|
|
||||||
@Suppress("ComplexMethod")
|
@Suppress("ComplexMethod")
|
||||||
override fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) {
|
override fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) {
|
||||||
|
@ -160,25 +159,25 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
|
||||||
val bookmarkItemMenu = BookmarkItemMenu(containerView.context, item) {
|
val bookmarkItemMenu = BookmarkItemMenu(containerView.context, item) {
|
||||||
when (it) {
|
when (it) {
|
||||||
is BookmarkItemMenu.Item.Edit -> {
|
is BookmarkItemMenu.Item.Edit -> {
|
||||||
actionEmitter.onNext(BookmarkAction.Edit(item))
|
interactor.edit(item)
|
||||||
}
|
}
|
||||||
is BookmarkItemMenu.Item.Select -> {
|
is BookmarkItemMenu.Item.Select -> {
|
||||||
actionEmitter.onNext(BookmarkAction.Select(item))
|
interactor.select(item)
|
||||||
}
|
}
|
||||||
is BookmarkItemMenu.Item.Copy -> {
|
is BookmarkItemMenu.Item.Copy -> {
|
||||||
actionEmitter.onNext(BookmarkAction.Copy(item))
|
interactor.copy(item)
|
||||||
}
|
}
|
||||||
is BookmarkItemMenu.Item.Share -> {
|
is BookmarkItemMenu.Item.Share -> {
|
||||||
actionEmitter.onNext(BookmarkAction.Share(item))
|
interactor.share(item)
|
||||||
}
|
}
|
||||||
is BookmarkItemMenu.Item.OpenInNewTab -> {
|
is BookmarkItemMenu.Item.OpenInNewTab -> {
|
||||||
actionEmitter.onNext(BookmarkAction.OpenInNewTab(item))
|
interactor.openInNewTab(item)
|
||||||
}
|
}
|
||||||
is BookmarkItemMenu.Item.OpenInPrivateTab -> {
|
is BookmarkItemMenu.Item.OpenInPrivateTab -> {
|
||||||
actionEmitter.onNext(BookmarkAction.OpenInPrivateTab(item))
|
interactor.openInPrivateTab(item)
|
||||||
}
|
}
|
||||||
is BookmarkItemMenu.Item.Delete -> {
|
is BookmarkItemMenu.Item.Delete -> {
|
||||||
actionEmitter.onNext(BookmarkAction.Delete(item))
|
interactor.delete(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -228,19 +227,15 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
|
||||||
) {
|
) {
|
||||||
bookmark_layout.setOnClickListener {
|
bookmark_layout.setOnClickListener {
|
||||||
if (mode == BookmarkState.Mode.Normal) {
|
if (mode == BookmarkState.Mode.Normal) {
|
||||||
actionEmitter.onNext(BookmarkAction.Open(item))
|
interactor.open(item)
|
||||||
} else {
|
} else {
|
||||||
if (selected) actionEmitter.onNext(BookmarkAction.Deselect(item)) else actionEmitter.onNext(
|
if (selected) interactor.deselect(item) else interactor.select(item)
|
||||||
BookmarkAction.Select(item)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bookmark_layout.setOnLongClickListener {
|
bookmark_layout.setOnLongClickListener {
|
||||||
if (mode == BookmarkState.Mode.Normal) {
|
if (mode == BookmarkState.Mode.Normal) {
|
||||||
if (selected) actionEmitter.onNext(BookmarkAction.Deselect(item)) else actionEmitter.onNext(
|
if (selected) interactor.deselect(item) else interactor.select(item)
|
||||||
BookmarkAction.Select(item)
|
|
||||||
)
|
|
||||||
true
|
true
|
||||||
} else false
|
} else false
|
||||||
}
|
}
|
||||||
|
@ -255,11 +250,11 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
|
||||||
|
|
||||||
class BookmarkFolderViewHolder(
|
class BookmarkFolderViewHolder(
|
||||||
view: View,
|
view: View,
|
||||||
actionEmitter: Observer<BookmarkAction>,
|
interactor: BookmarkViewInteractor,
|
||||||
job: Job,
|
job: Job,
|
||||||
override val containerView: View? = view
|
override val containerView: View? = view
|
||||||
) :
|
) :
|
||||||
BookmarkNodeViewHolder(view, actionEmitter, job, containerView) {
|
BookmarkNodeViewHolder(view, interactor, job, containerView) {
|
||||||
|
|
||||||
override fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) {
|
override fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) {
|
||||||
containerView?.context?.let {
|
containerView?.context?.let {
|
||||||
|
@ -304,13 +299,13 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
|
||||||
val bookmarkItemMenu = BookmarkItemMenu(containerView.context, item) {
|
val bookmarkItemMenu = BookmarkItemMenu(containerView.context, item) {
|
||||||
when (it) {
|
when (it) {
|
||||||
is BookmarkItemMenu.Item.Edit -> {
|
is BookmarkItemMenu.Item.Edit -> {
|
||||||
actionEmitter.onNext(BookmarkAction.Edit(item))
|
interactor.edit(item)
|
||||||
}
|
}
|
||||||
is BookmarkItemMenu.Item.Select -> {
|
is BookmarkItemMenu.Item.Select -> {
|
||||||
actionEmitter.onNext(BookmarkAction.Select(item))
|
interactor.select(item)
|
||||||
}
|
}
|
||||||
is BookmarkItemMenu.Item.Delete -> {
|
is BookmarkItemMenu.Item.Delete -> {
|
||||||
actionEmitter.onNext(BookmarkAction.Delete(item))
|
interactor.delete(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -336,19 +331,15 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
|
||||||
) {
|
) {
|
||||||
bookmark_layout.setOnClickListener {
|
bookmark_layout.setOnClickListener {
|
||||||
if (mode == BookmarkState.Mode.Normal) {
|
if (mode == BookmarkState.Mode.Normal) {
|
||||||
actionEmitter.onNext(BookmarkAction.Expand(item))
|
interactor.expand(item)
|
||||||
} else {
|
} else {
|
||||||
if (selected) actionEmitter.onNext(BookmarkAction.Deselect(item)) else actionEmitter.onNext(
|
if (selected) interactor.deselect(item) else interactor.select(item)
|
||||||
BookmarkAction.Select(item)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bookmark_layout.setOnLongClickListener {
|
bookmark_layout.setOnLongClickListener {
|
||||||
if (mode == BookmarkState.Mode.Normal && !item.inRoots()) {
|
if (mode == BookmarkState.Mode.Normal && !item.inRoots()) {
|
||||||
if (selected) actionEmitter.onNext(BookmarkAction.Deselect(item)) else actionEmitter.onNext(
|
if (selected) interactor.deselect(item) else interactor.select(item)
|
||||||
BookmarkAction.Select(item)
|
|
||||||
)
|
|
||||||
true
|
true
|
||||||
} else false
|
} else false
|
||||||
}
|
}
|
||||||
|
@ -361,10 +352,10 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
|
||||||
|
|
||||||
class BookmarkSeparatorViewHolder(
|
class BookmarkSeparatorViewHolder(
|
||||||
view: View,
|
view: View,
|
||||||
actionEmitter: Observer<BookmarkAction>,
|
interactor: BookmarkViewInteractor,
|
||||||
job: Job,
|
job: Job,
|
||||||
override val containerView: View? = view
|
override val containerView: View? = view
|
||||||
) : BookmarkNodeViewHolder(view, actionEmitter, job, containerView) {
|
) : BookmarkNodeViewHolder(view, interactor, job, containerView) {
|
||||||
|
|
||||||
override fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) {
|
override fun bind(item: BookmarkNode, mode: BookmarkState.Mode, selected: Boolean) {
|
||||||
|
|
||||||
|
@ -379,7 +370,7 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
|
||||||
val bookmarkItemMenu = BookmarkItemMenu(containerView!!.context, item) {
|
val bookmarkItemMenu = BookmarkItemMenu(containerView!!.context, item) {
|
||||||
when (it) {
|
when (it) {
|
||||||
is BookmarkItemMenu.Item.Delete -> {
|
is BookmarkItemMenu.Item.Delete -> {
|
||||||
actionEmitter.onNext(BookmarkAction.Delete(item))
|
interactor.delete(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,110 +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.library.bookmarks
|
|
||||||
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import mozilla.components.concept.storage.BookmarkNode
|
|
||||||
import org.mozilla.fenix.mvi.ViewState
|
|
||||||
import org.mozilla.fenix.mvi.Change
|
|
||||||
import org.mozilla.fenix.mvi.Action
|
|
||||||
import org.mozilla.fenix.mvi.ActionBusFactory
|
|
||||||
import org.mozilla.fenix.mvi.Reducer
|
|
||||||
import org.mozilla.fenix.mvi.UIComponent
|
|
||||||
import org.mozilla.fenix.mvi.UIComponentViewModelBase
|
|
||||||
import org.mozilla.fenix.mvi.UIComponentViewModelProvider
|
|
||||||
import org.mozilla.fenix.mvi.UIView
|
|
||||||
import org.mozilla.fenix.test.Mockable
|
|
||||||
|
|
||||||
@Mockable
|
|
||||||
class BookmarkComponent(
|
|
||||||
private val container: ViewGroup,
|
|
||||||
bus: ActionBusFactory,
|
|
||||||
viewModelProvider: UIComponentViewModelProvider<BookmarkState, BookmarkChange>
|
|
||||||
) :
|
|
||||||
UIComponent<BookmarkState, BookmarkAction, BookmarkChange>(
|
|
||||||
bus.getManagedEmitter(BookmarkAction::class.java),
|
|
||||||
bus.getSafeManagedObservable(BookmarkChange::class.java),
|
|
||||||
viewModelProvider
|
|
||||||
) {
|
|
||||||
override fun initView(): UIView<BookmarkState, BookmarkAction, BookmarkChange> =
|
|
||||||
BookmarkUIView(container, actionEmitter, changesObservable)
|
|
||||||
|
|
||||||
init {
|
|
||||||
bind()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class BookmarkState(val tree: BookmarkNode?, val mode: Mode) : ViewState {
|
|
||||||
sealed class Mode {
|
|
||||||
object Normal : Mode()
|
|
||||||
data class Selecting(val selectedItems: Set<BookmarkNode>) : Mode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class BookmarkAction : Action {
|
|
||||||
data class Open(val item: BookmarkNode) : BookmarkAction()
|
|
||||||
data class Expand(val folder: BookmarkNode) : BookmarkAction()
|
|
||||||
data class Edit(val item: BookmarkNode) : BookmarkAction()
|
|
||||||
data class Copy(val item: BookmarkNode) : BookmarkAction()
|
|
||||||
data class Share(val item: BookmarkNode) : BookmarkAction()
|
|
||||||
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 BackPressed : BookmarkAction()
|
|
||||||
object SwitchMode : BookmarkAction()
|
|
||||||
object DeselectAll : 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()
|
|
||||||
object ClearSelection : BookmarkChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun BookmarkNode.contains(item: BookmarkNode): Boolean {
|
|
||||||
return children?.contains(item) ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
class BookmarkViewModel(initialState: BookmarkState) :
|
|
||||||
UIComponentViewModelBase<BookmarkState, BookmarkChange>(initialState, reducer) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun create() = BookmarkViewModel(BookmarkState(null, BookmarkState.Mode.Normal))
|
|
||||||
|
|
||||||
val reducer: Reducer<BookmarkState, BookmarkChange> = { state, change ->
|
|
||||||
when (change) {
|
|
||||||
is BookmarkChange.Change -> {
|
|
||||||
val mode =
|
|
||||||
if (state.mode is BookmarkState.Mode.Selecting) {
|
|
||||||
val items = state.mode.selectedItems.filter {
|
|
||||||
it in change.tree
|
|
||||||
}.toSet()
|
|
||||||
if (items.isEmpty()) BookmarkState.Mode.Normal else BookmarkState.Mode.Selecting(items)
|
|
||||||
} 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)
|
|
||||||
}
|
|
||||||
is BookmarkChange.ClearSelection -> state.copy(mode = BookmarkState.Mode.Normal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,9 +4,6 @@
|
||||||
|
|
||||||
package org.mozilla.fenix.library.bookmarks
|
package org.mozilla.fenix.library.bookmarks
|
||||||
|
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.PorterDuff.Mode.SRC_IN
|
import android.graphics.PorterDuff.Mode.SRC_IN
|
||||||
import android.graphics.PorterDuffColorFilter
|
import android.graphics.PorterDuffColorFilter
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -18,10 +15,11 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.ViewModelProviders
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.whenStarted
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import kotlinx.android.synthetic.main.fragment_bookmark.view.*
|
import kotlinx.android.synthetic.main.fragment_bookmark.view.*
|
||||||
|
@ -32,19 +30,17 @@ import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import mozilla.appservices.places.BookmarkRoot
|
import mozilla.appservices.places.BookmarkRoot
|
||||||
import mozilla.components.concept.storage.BookmarkNode
|
import mozilla.components.concept.storage.BookmarkNode
|
||||||
import mozilla.components.concept.storage.BookmarkNodeType
|
|
||||||
import mozilla.components.concept.sync.AccountObserver
|
import mozilla.components.concept.sync.AccountObserver
|
||||||
import mozilla.components.concept.sync.OAuthAccount
|
import mozilla.components.concept.sync.OAuthAccount
|
||||||
import mozilla.components.concept.sync.Profile
|
import mozilla.components.concept.sync.Profile
|
||||||
|
import mozilla.components.lib.state.ext.observe
|
||||||
import mozilla.components.support.base.feature.BackHandler
|
import mozilla.components.support.base.feature.BackHandler
|
||||||
import org.mozilla.fenix.BrowserDirection
|
|
||||||
import org.mozilla.fenix.BrowsingModeManager
|
import org.mozilla.fenix.BrowsingModeManager
|
||||||
import org.mozilla.fenix.FenixViewModelProvider
|
|
||||||
import org.mozilla.fenix.HomeActivity
|
import org.mozilla.fenix.HomeActivity
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.components.FenixSnackbar
|
import org.mozilla.fenix.components.FenixSnackbarPresenter
|
||||||
|
import org.mozilla.fenix.components.StoreProvider
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
import org.mozilla.fenix.components.metrics.MetricController
|
|
||||||
import org.mozilla.fenix.ext.bookmarkStorage
|
import org.mozilla.fenix.ext.bookmarkStorage
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.minus
|
import org.mozilla.fenix.ext.minus
|
||||||
|
@ -52,16 +48,18 @@ import org.mozilla.fenix.ext.nav
|
||||||
import org.mozilla.fenix.ext.setRootTitles
|
import org.mozilla.fenix.ext.setRootTitles
|
||||||
import org.mozilla.fenix.ext.urlToTrimmedHost
|
import org.mozilla.fenix.ext.urlToTrimmedHost
|
||||||
import org.mozilla.fenix.ext.withOptionalDesktopFolders
|
import org.mozilla.fenix.ext.withOptionalDesktopFolders
|
||||||
import org.mozilla.fenix.mvi.ActionBusFactory
|
|
||||||
import org.mozilla.fenix.mvi.getAutoDisposeObservable
|
|
||||||
import org.mozilla.fenix.mvi.getManagedEmitter
|
|
||||||
import org.mozilla.fenix.utils.allowUndo
|
import org.mozilla.fenix.utils.allowUndo
|
||||||
|
|
||||||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||||
class BookmarkFragment : Fragment(), BackHandler, AccountObserver {
|
class BookmarkFragment : Fragment(), BackHandler, AccountObserver {
|
||||||
|
|
||||||
private lateinit var bookmarkComponent: BookmarkComponent
|
private lateinit var bookmarkStore: BookmarkStore
|
||||||
private lateinit var signInComponent: SignInComponent
|
private lateinit var bookmarkView: BookmarkView
|
||||||
|
private lateinit var signInView: SignInView
|
||||||
|
private lateinit var bookmarkInteractor: BookmarkFragmentInteractor
|
||||||
|
|
||||||
|
private val sharedViewModel: BookmarksSharedViewModel by activityViewModels()
|
||||||
|
|
||||||
var currentRoot: BookmarkNode? = null
|
var currentRoot: BookmarkNode? = null
|
||||||
private val navigation by lazy { findNavController() }
|
private val navigation by lazy { findNavController() }
|
||||||
private val onDestinationChangedListener =
|
private val onDestinationChangedListener =
|
||||||
|
@ -69,36 +67,51 @@ class BookmarkFragment : Fragment(), BackHandler, AccountObserver {
|
||||||
if (destination.id != R.id.bookmarkFragment ||
|
if (destination.id != R.id.bookmarkFragment ||
|
||||||
args != null && BookmarkFragmentArgs.fromBundle(args).currentRoot != currentRoot?.guid
|
args != null && BookmarkFragmentArgs.fromBundle(args).currentRoot != currentRoot?.guid
|
||||||
)
|
)
|
||||||
getManagedEmitter<BookmarkChange>().onNext(BookmarkChange.ClearSelection)
|
bookmarkInteractor.deselectAll()
|
||||||
}
|
}
|
||||||
lateinit var initialJob: Job
|
lateinit var initialJob: Job
|
||||||
private var pendingBookmarkDeletionJob: (suspend () -> Unit)? = null
|
private var pendingBookmarkDeletionJob: (suspend () -> Unit)? = null
|
||||||
private var pendingBookmarksToDelete: MutableSet<BookmarkNode> = HashSet()
|
private var pendingBookmarksToDelete: MutableSet<BookmarkNode> = mutableSetOf()
|
||||||
|
|
||||||
|
private val metrics
|
||||||
|
get() = context?.components?.analytics?.metrics
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
val view = inflater.inflate(R.layout.fragment_bookmark, container, false)
|
val view = inflater.inflate(R.layout.fragment_bookmark, container, false)
|
||||||
bookmarkComponent = BookmarkComponent(
|
|
||||||
view.bookmark_layout,
|
bookmarkStore = StoreProvider.get(this) {
|
||||||
ActionBusFactory.get(this),
|
BookmarkStore(BookmarkState(null))
|
||||||
FenixViewModelProvider.create(
|
}
|
||||||
this,
|
bookmarkInteractor = BookmarkFragmentInteractor(
|
||||||
BookmarkViewModel::class.java,
|
context!!,
|
||||||
BookmarkViewModel.Companion::create
|
findNavController(),
|
||||||
)
|
bookmarkStore,
|
||||||
)
|
sharedViewModel,
|
||||||
signInComponent = SignInComponent(
|
FenixSnackbarPresenter(view),
|
||||||
view.bookmark_layout,
|
::deleteMulti
|
||||||
ActionBusFactory.get(this),
|
|
||||||
FenixViewModelProvider.create(
|
|
||||||
this,
|
|
||||||
SignInViewModel::class.java
|
|
||||||
) {
|
|
||||||
SignInViewModel(SignInState(false))
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
bookmarkView = BookmarkView(view.bookmark_layout, bookmarkInteractor)
|
||||||
|
signInView = SignInView(view.bookmark_layout, bookmarkInteractor)
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
bookmarkStore.observe(view) {
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
whenStarted {
|
||||||
|
bookmarkView.update(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sharedViewModel.apply {
|
||||||
|
signedIn.observe(this@BookmarkFragment, Observer<Boolean> {
|
||||||
|
signInView.update(it)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
activity?.title = getString(R.string.library_bookmarks)
|
activity?.title = getString(R.string.library_bookmarks)
|
||||||
|
@ -125,11 +138,8 @@ class BookmarkFragment : Fragment(), BackHandler, AccountObserver {
|
||||||
|
|
||||||
if (!isActive) return@launch
|
if (!isActive) return@launch
|
||||||
launch(Main) {
|
launch(Main) {
|
||||||
getManagedEmitter<BookmarkChange>().onNext(BookmarkChange.Change(currentRoot!!))
|
bookmarkInteractor.change(currentRoot!!)
|
||||||
|
sharedViewModel.selectedFolder = currentRoot
|
||||||
activity?.run {
|
|
||||||
ViewModelProviders.of(this).get(BookmarksSharedViewModel::class.java)
|
|
||||||
}!!.selectedFolder = currentRoot
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -137,8 +147,8 @@ class BookmarkFragment : Fragment(), BackHandler, AccountObserver {
|
||||||
private fun checkIfSignedIn() {
|
private fun checkIfSignedIn() {
|
||||||
context?.components?.backgroundServices?.accountManager?.let {
|
context?.components?.backgroundServices?.accountManager?.let {
|
||||||
it.register(this, owner = this)
|
it.register(this, owner = this)
|
||||||
it.authenticatedAccount()?.let { getManagedEmitter<SignInChange>().onNext(SignInChange.SignedIn) }
|
it.authenticatedAccount()?.let { bookmarkInteractor.signedIn() }
|
||||||
?: getManagedEmitter<SignInChange>().onNext(SignInChange.SignedOut)
|
?: bookmarkInteractor.signedOut()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,7 +158,7 @@ class BookmarkFragment : Fragment(), BackHandler, AccountObserver {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
when (val mode = (bookmarkComponent.uiView as BookmarkUIView).mode) {
|
when (val mode = bookmarkView.mode) {
|
||||||
BookmarkState.Mode.Normal -> {
|
BookmarkState.Mode.Normal -> {
|
||||||
inflater.inflate(R.menu.bookmarks_menu, menu)
|
inflater.inflate(R.menu.bookmarks_menu, menu)
|
||||||
}
|
}
|
||||||
|
@ -165,162 +175,6 @@ class BookmarkFragment : Fragment(), BackHandler, AccountObserver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("ComplexMethod")
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
getAutoDisposeObservable<BookmarkAction>()
|
|
||||||
.subscribe {
|
|
||||||
when (it) {
|
|
||||||
is BookmarkAction.Open -> {
|
|
||||||
if (it.item.type == BookmarkNodeType.ITEM) {
|
|
||||||
it.item.url?.let { url ->
|
|
||||||
(activity as HomeActivity)
|
|
||||||
.openToBrowserAndLoad(
|
|
||||||
searchTermOrURL = url,
|
|
||||||
newTab = true,
|
|
||||||
from = BrowserDirection.FromBookmarks
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
metrics()?.track(Event.OpenedBookmark)
|
|
||||||
}
|
|
||||||
is BookmarkAction.Expand -> {
|
|
||||||
nav(
|
|
||||||
R.id.bookmarkFragment,
|
|
||||||
BookmarkFragmentDirections.actionBookmarkFragmentSelf(it.folder.guid)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is BookmarkAction.BackPressed -> {
|
|
||||||
navigation.popBackStack()
|
|
||||||
}
|
|
||||||
is BookmarkAction.Edit -> {
|
|
||||||
nav(
|
|
||||||
R.id.bookmarkFragment,
|
|
||||||
BookmarkFragmentDirections
|
|
||||||
.actionBookmarkFragmentToBookmarkEditFragment(it.item.guid)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is BookmarkAction.Select -> {
|
|
||||||
getManagedEmitter<BookmarkChange>().onNext(BookmarkChange.IsSelected(it.item))
|
|
||||||
}
|
|
||||||
is BookmarkAction.Deselect -> {
|
|
||||||
getManagedEmitter<BookmarkChange>().onNext(BookmarkChange.IsDeselected(it.item))
|
|
||||||
}
|
|
||||||
is BookmarkAction.Copy -> {
|
|
||||||
it.item.copyUrl(context!!)
|
|
||||||
FenixSnackbar.make(view!!, FenixSnackbar.LENGTH_LONG)
|
|
||||||
.setText(context!!.getString(R.string.url_copied)).show()
|
|
||||||
metrics()?.track(Event.CopyBookmark)
|
|
||||||
}
|
|
||||||
is BookmarkAction.Share -> {
|
|
||||||
it.item.url?.apply {
|
|
||||||
nav(
|
|
||||||
R.id.bookmarkFragment,
|
|
||||||
BookmarkFragmentDirections.actionBookmarkFragmentToShareFragment(
|
|
||||||
url = this,
|
|
||||||
title = it.item.title
|
|
||||||
)
|
|
||||||
)
|
|
||||||
metrics()?.track(Event.ShareBookmark)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is BookmarkAction.OpenInNewTab -> {
|
|
||||||
it.item.url?.let { url ->
|
|
||||||
(activity as HomeActivity).browsingModeManager.mode =
|
|
||||||
BrowsingModeManager.Mode.Normal
|
|
||||||
(activity as HomeActivity).openToBrowserAndLoad(
|
|
||||||
searchTermOrURL = url,
|
|
||||||
newTab = true,
|
|
||||||
from = BrowserDirection.FromBookmarks
|
|
||||||
)
|
|
||||||
metrics()?.track(Event.OpenedBookmarkInNewTab)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is BookmarkAction.OpenInPrivateTab -> {
|
|
||||||
it.item.url?.let { url ->
|
|
||||||
(activity as HomeActivity).browsingModeManager.mode =
|
|
||||||
BrowsingModeManager.Mode.Private
|
|
||||||
(activity as HomeActivity).openToBrowserAndLoad(
|
|
||||||
searchTermOrURL = url,
|
|
||||||
newTab = true,
|
|
||||||
from = BrowserDirection.FromBookmarks
|
|
||||||
)
|
|
||||||
metrics()?.track(Event.OpenedBookmarkInPrivateTab)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is BookmarkAction.Delete -> {
|
|
||||||
val bookmarkItem = it.item
|
|
||||||
if (pendingBookmarkDeletionJob == null) {
|
|
||||||
removeBookmarkWithUndo(bookmarkItem)
|
|
||||||
} else {
|
|
||||||
pendingBookmarkDeletionJob?.let {
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
it.invoke()
|
|
||||||
}.invokeOnCompletion {
|
|
||||||
removeBookmarkWithUndo(bookmarkItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is BookmarkAction.SwitchMode -> {
|
|
||||||
activity?.invalidateOptionsMenu()
|
|
||||||
}
|
|
||||||
is BookmarkAction.DeselectAll ->
|
|
||||||
getManagedEmitter<BookmarkChange>().onNext(BookmarkChange.ClearSelection)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getAutoDisposeObservable<SignInAction>()
|
|
||||||
.subscribe {
|
|
||||||
when (it) {
|
|
||||||
is SignInAction.ClickedSignIn -> {
|
|
||||||
context?.components?.services?.accountsAuthFeature?.beginAuthentication(requireContext())
|
|
||||||
(activity as HomeActivity).openToBrowser(BrowserDirection.FromBookmarks)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun removeBookmarkWithUndo(bookmarkNode: BookmarkNode) {
|
|
||||||
val bookmarkStorage = context.bookmarkStorage()
|
|
||||||
pendingBookmarksToDelete.add(bookmarkNode)
|
|
||||||
|
|
||||||
var bookmarkTree = currentRoot
|
|
||||||
pendingBookmarksToDelete.forEach {
|
|
||||||
bookmarkTree -= it.guid
|
|
||||||
}
|
|
||||||
|
|
||||||
getManagedEmitter<BookmarkChange>().onNext(BookmarkChange.Change(bookmarkTree!!))
|
|
||||||
|
|
||||||
val deleteOperation: (suspend () -> Unit) = {
|
|
||||||
bookmarkStorage?.deleteNode(bookmarkNode.guid)
|
|
||||||
when (bookmarkNode.type) {
|
|
||||||
BookmarkNodeType.FOLDER -> metrics()?.track(Event.RemoveBookmarkFolder)
|
|
||||||
BookmarkNodeType.ITEM -> metrics()?.track(Event.RemoveBookmark)
|
|
||||||
else -> { }
|
|
||||||
}
|
|
||||||
pendingBookmarkDeletionJob = null
|
|
||||||
refreshBookmarks()
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingBookmarkDeletionJob = deleteOperation
|
|
||||||
|
|
||||||
lifecycleScope.allowUndo(
|
|
||||||
view!!,
|
|
||||||
getString(
|
|
||||||
R.string.bookmark_deletion_snackbar_message,
|
|
||||||
bookmarkNode.url?.urlToTrimmedHost(context!!) ?: bookmarkNode.title
|
|
||||||
),
|
|
||||||
getString(R.string.bookmark_undo_deletion),
|
|
||||||
onCancel = {
|
|
||||||
pendingBookmarkDeletionJob = null
|
|
||||||
pendingBookmarksToDelete.remove(bookmarkNode)
|
|
||||||
refreshBookmarks()
|
|
||||||
},
|
|
||||||
operation = deleteOperation
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
return when (item.itemId) {
|
return when (item.itemId) {
|
||||||
R.id.libraryClose -> {
|
R.id.libraryClose -> {
|
||||||
|
@ -346,7 +200,7 @@ class BookmarkFragment : Fragment(), BackHandler, AccountObserver {
|
||||||
(activity as HomeActivity).browsingModeManager.mode = BrowsingModeManager.Mode.Normal
|
(activity as HomeActivity).browsingModeManager.mode = BrowsingModeManager.Mode.Normal
|
||||||
(activity as HomeActivity).supportActionBar?.hide()
|
(activity as HomeActivity).supportActionBar?.hide()
|
||||||
nav(R.id.bookmarkFragment, BookmarkFragmentDirections.actionBookmarkFragmentToHomeFragment())
|
nav(R.id.bookmarkFragment, BookmarkFragmentDirections.actionBookmarkFragmentToHomeFragment())
|
||||||
metrics()?.track(Event.OpenedBookmarksInNewTabs)
|
metrics?.track(Event.OpenedBookmarksInNewTabs)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.edit_bookmark_multi_select -> {
|
R.id.edit_bookmark_multi_select -> {
|
||||||
|
@ -368,54 +222,28 @@ class BookmarkFragment : Fragment(), BackHandler, AccountObserver {
|
||||||
(activity as HomeActivity).browsingModeManager.mode = BrowsingModeManager.Mode.Private
|
(activity as HomeActivity).browsingModeManager.mode = BrowsingModeManager.Mode.Private
|
||||||
(activity as HomeActivity).supportActionBar?.hide()
|
(activity as HomeActivity).supportActionBar?.hide()
|
||||||
nav(R.id.bookmarkFragment, BookmarkFragmentDirections.actionBookmarkFragmentToHomeFragment())
|
nav(R.id.bookmarkFragment, BookmarkFragmentDirections.actionBookmarkFragmentToHomeFragment())
|
||||||
metrics()?.track(Event.OpenedBookmarksInPrivateTabs)
|
metrics?.track(Event.OpenedBookmarksInPrivateTabs)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.delete_bookmarks_multi_select -> {
|
R.id.delete_bookmarks_multi_select -> {
|
||||||
val selectedBookmarks = getSelectedBookmarks()
|
deleteMulti(getSelectedBookmarks())
|
||||||
pendingBookmarksToDelete.addAll(selectedBookmarks)
|
|
||||||
|
|
||||||
var bookmarkTree = currentRoot
|
|
||||||
pendingBookmarksToDelete.forEach {
|
|
||||||
bookmarkTree -= it.guid
|
|
||||||
}
|
|
||||||
getManagedEmitter<BookmarkChange>().onNext(BookmarkChange.Change(bookmarkTree!!))
|
|
||||||
|
|
||||||
val deleteOperation: (suspend () -> Unit) = {
|
|
||||||
deleteSelectedBookmarks(selectedBookmarks)
|
|
||||||
pendingBookmarkDeletionJob = null
|
|
||||||
// Since this runs in a coroutine, we can't depend on the fragment still being attached.
|
|
||||||
metrics()?.track(Event.RemoveBookmarks)
|
|
||||||
refreshBookmarks()
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingBookmarkDeletionJob = deleteOperation
|
|
||||||
|
|
||||||
lifecycleScope.allowUndo(
|
|
||||||
view!!, getString(R.string.bookmark_deletion_multiple_snackbar_message),
|
|
||||||
getString(R.string.bookmark_undo_deletion), {
|
|
||||||
pendingBookmarksToDelete.removeAll(selectedBookmarks)
|
|
||||||
pendingBookmarkDeletionJob = null
|
|
||||||
refreshBookmarks()
|
|
||||||
}, operation = deleteOperation
|
|
||||||
)
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed(): Boolean = (bookmarkComponent.uiView as BookmarkUIView).onBackPressed()
|
override fun onBackPressed(): Boolean = bookmarkView.onBackPressed()
|
||||||
|
|
||||||
override fun onAuthenticated(account: OAuthAccount) {
|
override fun onAuthenticated(account: OAuthAccount) {
|
||||||
getManagedEmitter<SignInChange>().onNext(SignInChange.SignedIn)
|
bookmarkInteractor.signedIn()
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
refreshBookmarks()
|
refreshBookmarks()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoggedOut() {
|
override fun onLoggedOut() {
|
||||||
getManagedEmitter<SignInChange>().onNext(SignInChange.SignedOut)
|
bookmarkInteractor.signedOut()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAuthenticationProblems() {
|
override fun onAuthenticationProblems() {
|
||||||
|
@ -424,7 +252,23 @@ class BookmarkFragment : Fragment(), BackHandler, AccountObserver {
|
||||||
override fun onProfileUpdated(profile: Profile) {
|
override fun onProfileUpdated(profile: Profile) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSelectedBookmarks() = (bookmarkComponent.uiView as BookmarkUIView).getSelected()
|
private fun getSelectedBookmarks() = bookmarkView.getSelected()
|
||||||
|
|
||||||
|
private suspend fun refreshBookmarks() {
|
||||||
|
context?.bookmarkStorage()?.getTree(bookmarkStore.state.tree!!.guid, false).withOptionalDesktopFolders(context)
|
||||||
|
?.let { node ->
|
||||||
|
var rootNode = node
|
||||||
|
pendingBookmarksToDelete.forEach {
|
||||||
|
rootNode -= it.guid
|
||||||
|
}
|
||||||
|
bookmarkInteractor.change(rootNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
invokePendingDeletion()
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun deleteSelectedBookmarks(selected: Set<BookmarkNode> = getSelectedBookmarks()) {
|
private suspend fun deleteSelectedBookmarks(selected: Set<BookmarkNode> = getSelectedBookmarks()) {
|
||||||
selected.forEach {
|
selected.forEach {
|
||||||
|
@ -432,20 +276,48 @@ class BookmarkFragment : Fragment(), BackHandler, AccountObserver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun refreshBookmarks() {
|
private fun deleteMulti(selected: Set<BookmarkNode>, eventType: Event = Event.RemoveBookmarks) {
|
||||||
context?.bookmarkStorage()?.getTree(currentRoot!!.guid, false).withOptionalDesktopFolders(context)
|
pendingBookmarksToDelete.addAll(selected)
|
||||||
?.let { node ->
|
|
||||||
var rootNode = node
|
|
||||||
pendingBookmarksToDelete.forEach {
|
|
||||||
rootNode -= it.guid
|
|
||||||
}
|
|
||||||
getManagedEmitter<BookmarkChange>().onNext(BookmarkChange.Change(rootNode))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
var bookmarkTree = currentRoot
|
||||||
invokePendingDeletion()
|
pendingBookmarksToDelete.forEach {
|
||||||
super.onPause()
|
bookmarkTree -= it.guid
|
||||||
|
}
|
||||||
|
bookmarkInteractor.change(bookmarkTree!!)
|
||||||
|
|
||||||
|
val deleteOperation: (suspend () -> Unit) = {
|
||||||
|
deleteSelectedBookmarks(selected)
|
||||||
|
pendingBookmarkDeletionJob = null
|
||||||
|
// Since this runs in a coroutine, we can't depend upon the fragment still being attached
|
||||||
|
metrics?.track(Event.RemoveBookmarks)
|
||||||
|
refreshBookmarks()
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingBookmarkDeletionJob = deleteOperation
|
||||||
|
|
||||||
|
val message = when (eventType) {
|
||||||
|
is Event.RemoveBookmarks -> {
|
||||||
|
getString(R.string.bookmark_deletion_multiple_snackbar_message)
|
||||||
|
}
|
||||||
|
is Event.RemoveBookmarkFolder,
|
||||||
|
is Event.RemoveBookmark -> {
|
||||||
|
val bookmarkNode = selected.first()
|
||||||
|
getString(
|
||||||
|
R.string.bookmark_deletion_snackbar_message,
|
||||||
|
bookmarkNode.url?.urlToTrimmedHost(context!!) ?: bookmarkNode.title
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> throw IllegalStateException("Illegal event type in deleteMulti")
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.allowUndo(
|
||||||
|
view!!, message,
|
||||||
|
getString(R.string.bookmark_undo_deletion), {
|
||||||
|
pendingBookmarksToDelete.removeAll(selected)
|
||||||
|
pendingBookmarkDeletionJob = null
|
||||||
|
refreshBookmarks()
|
||||||
|
}, operation = deleteOperation
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun invokePendingDeletion() {
|
private fun invokePendingDeletion() {
|
||||||
|
@ -457,14 +329,4 @@ class BookmarkFragment : Fragment(), BackHandler, AccountObserver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun BookmarkNode.copyUrl(context: Context) {
|
|
||||||
context.getSystemService<ClipboardManager>()?.apply {
|
|
||||||
primaryClip = ClipData.newPlainText(url, url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun metrics(): MetricController? {
|
|
||||||
return context?.components?.analytics?.metrics
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,181 @@
|
||||||
|
/* 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.Context
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import mozilla.components.concept.storage.BookmarkNode
|
||||||
|
import mozilla.components.concept.storage.BookmarkNodeType
|
||||||
|
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.FenixSnackbarPresenter
|
||||||
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
|
import org.mozilla.fenix.components.metrics.MetricController
|
||||||
|
import org.mozilla.fenix.ext.asActivity
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.ext.copyUrl
|
||||||
|
import org.mozilla.fenix.ext.nav
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interactor for the Bookmarks screen.
|
||||||
|
* Provides implementations for the BookmarkViewInteractor.
|
||||||
|
*
|
||||||
|
* @property context The current Android Context
|
||||||
|
* @property navController The Android Navigation NavController
|
||||||
|
* @property bookmarkStore The BookmarkStore
|
||||||
|
* @property sharedViewModel The shared ViewModel used between the Bookmarks screens
|
||||||
|
* @property snackbarPresenter A presenter for the FenixSnackBar
|
||||||
|
* @property deleteBookmarkNodes A lambda function for deleting bookmark nodes with undo
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("TooManyFunctions")
|
||||||
|
class BookmarkFragmentInteractor(
|
||||||
|
private val context: Context,
|
||||||
|
private val navController: NavController,
|
||||||
|
private val bookmarkStore: BookmarkStore,
|
||||||
|
private val sharedViewModel: BookmarksSharedViewModel,
|
||||||
|
private val snackbarPresenter: FenixSnackbarPresenter,
|
||||||
|
private val deleteBookmarkNodes: (Set<BookmarkNode>, Event) -> Unit
|
||||||
|
) : BookmarkViewInteractor, SignInInteractor {
|
||||||
|
|
||||||
|
val activity: HomeActivity?
|
||||||
|
get() = context.asActivity() as? HomeActivity
|
||||||
|
val metrics: MetricController
|
||||||
|
get() = context.components.analytics.metrics
|
||||||
|
|
||||||
|
override fun change(node: BookmarkNode) {
|
||||||
|
bookmarkStore.dispatch(BookmarkAction.Change(node))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun open(item: BookmarkNode) {
|
||||||
|
require(item.type == BookmarkNodeType.ITEM)
|
||||||
|
item.url?.let { url ->
|
||||||
|
activity!!
|
||||||
|
.openToBrowserAndLoad(
|
||||||
|
searchTermOrURL = url,
|
||||||
|
newTab = true,
|
||||||
|
from = BrowserDirection.FromBookmarks
|
||||||
|
)
|
||||||
|
}
|
||||||
|
metrics.track(Event.OpenedBookmark)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun expand(folder: BookmarkNode) {
|
||||||
|
require(folder.type == BookmarkNodeType.FOLDER)
|
||||||
|
navController.nav(
|
||||||
|
R.id.bookmarkFragment,
|
||||||
|
BookmarkFragmentDirections.actionBookmarkFragmentSelf(folder.guid)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun switchMode(mode: BookmarkState.Mode) {
|
||||||
|
activity?.invalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun edit(node: BookmarkNode) {
|
||||||
|
navController.nav(
|
||||||
|
R.id.bookmarkFragment,
|
||||||
|
BookmarkFragmentDirections
|
||||||
|
.actionBookmarkFragmentToBookmarkEditFragment(node.guid)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun select(node: BookmarkNode) {
|
||||||
|
bookmarkStore.dispatch(BookmarkAction.Select(node))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deselect(node: BookmarkNode) {
|
||||||
|
bookmarkStore.dispatch(BookmarkAction.Deselect(node))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deselectAll() {
|
||||||
|
bookmarkStore.dispatch(BookmarkAction.DeselectAll)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun copy(item: BookmarkNode) {
|
||||||
|
require(item.type == BookmarkNodeType.ITEM)
|
||||||
|
item.copyUrl(activity!!)
|
||||||
|
snackbarPresenter.present(context.getString(R.string.url_copied))
|
||||||
|
metrics.track(Event.CopyBookmark)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun share(item: BookmarkNode) {
|
||||||
|
require(item.type == BookmarkNodeType.ITEM)
|
||||||
|
item.url?.apply {
|
||||||
|
navController.nav(
|
||||||
|
R.id.bookmarkFragment,
|
||||||
|
BookmarkFragmentDirections.actionBookmarkFragmentToShareFragment(
|
||||||
|
url = this,
|
||||||
|
title = item.title
|
||||||
|
)
|
||||||
|
)
|
||||||
|
metrics.track(Event.ShareBookmark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun openInNewTab(item: BookmarkNode) {
|
||||||
|
require(item.type == BookmarkNodeType.ITEM)
|
||||||
|
item.url?.let { url ->
|
||||||
|
activity?.browsingModeManager?.mode =
|
||||||
|
BrowsingModeManager.Mode.Normal
|
||||||
|
activity?.openToBrowserAndLoad(
|
||||||
|
searchTermOrURL = url,
|
||||||
|
newTab = true,
|
||||||
|
from = BrowserDirection.FromBookmarks
|
||||||
|
)
|
||||||
|
metrics.track(Event.OpenedBookmarkInNewTab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun openInPrivateTab(item: BookmarkNode) {
|
||||||
|
require(item.type == BookmarkNodeType.ITEM)
|
||||||
|
item.url?.let { url ->
|
||||||
|
activity?.browsingModeManager?.mode =
|
||||||
|
BrowsingModeManager.Mode.Private
|
||||||
|
activity?.openToBrowserAndLoad(
|
||||||
|
searchTermOrURL = url,
|
||||||
|
newTab = true,
|
||||||
|
from = BrowserDirection.FromBookmarks
|
||||||
|
)
|
||||||
|
metrics.track(Event.OpenedBookmarkInPrivateTab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun delete(node: BookmarkNode) {
|
||||||
|
val eventType = when (node.type) {
|
||||||
|
BookmarkNodeType.ITEM -> {
|
||||||
|
Event.RemoveBookmark
|
||||||
|
}
|
||||||
|
BookmarkNodeType.FOLDER -> {
|
||||||
|
Event.RemoveBookmarkFolder
|
||||||
|
}
|
||||||
|
BookmarkNodeType.SEPARATOR -> {
|
||||||
|
throw IllegalStateException("Cannot delete separators")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deleteBookmarkNodes(setOf(node), eventType)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteMulti(nodes: Set<BookmarkNode>) {
|
||||||
|
deleteBookmarkNodes(nodes, Event.RemoveBookmarks)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun backPressed() {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clickedSignIn() {
|
||||||
|
context.components.services.launchPairingSignIn(context, navController)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun signedIn() {
|
||||||
|
sharedViewModel.signedIn.postValue(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun signedOut() {
|
||||||
|
sharedViewModel.signedIn.postValue(false)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
/* 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 mozilla.components.concept.storage.BookmarkNode
|
||||||
|
import mozilla.components.lib.state.Action
|
||||||
|
import mozilla.components.lib.state.State
|
||||||
|
import mozilla.components.lib.state.Store
|
||||||
|
|
||||||
|
class BookmarkStore(
|
||||||
|
initalState: BookmarkState
|
||||||
|
) : Store<BookmarkState, BookmarkAction>(
|
||||||
|
initalState, ::bookmarkStateReducer
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The complete state of the bookmarks tree and multi-selection mode
|
||||||
|
* @property tree The current tree of bookmarks, if one is loaded
|
||||||
|
* @property mode The current bookmark multi-selection mode
|
||||||
|
*/
|
||||||
|
data class BookmarkState(val tree: BookmarkNode?, val mode: Mode = Mode.Normal) : State {
|
||||||
|
sealed class Mode {
|
||||||
|
object Normal : Mode()
|
||||||
|
data class Selecting(val selectedItems: Set<BookmarkNode>) : Mode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions to dispatch through the `BookmarkStore` to modify `BookmarkState` through the reducer.
|
||||||
|
*/
|
||||||
|
sealed class BookmarkAction : Action {
|
||||||
|
data class Change(val tree: BookmarkNode) : BookmarkAction()
|
||||||
|
data class Select(val item: BookmarkNode) : BookmarkAction()
|
||||||
|
data class Deselect(val item: BookmarkNode) : BookmarkAction()
|
||||||
|
object DeselectAll : BookmarkAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduces the bookmarks state from the current state and an action performed on it.
|
||||||
|
* @param state the current bookmarks state
|
||||||
|
* @param action the action to perform
|
||||||
|
* @return the new bookmarks state
|
||||||
|
*/
|
||||||
|
fun bookmarkStateReducer(state: BookmarkState, action: BookmarkAction): BookmarkState {
|
||||||
|
return when (action) {
|
||||||
|
is BookmarkAction.Change -> {
|
||||||
|
val mode =
|
||||||
|
if (state.mode is BookmarkState.Mode.Selecting) {
|
||||||
|
val items = state.mode.selectedItems.filter {
|
||||||
|
it in action.tree
|
||||||
|
}.toSet()
|
||||||
|
if (items.isEmpty()) BookmarkState.Mode.Normal else BookmarkState.Mode.Selecting(items)
|
||||||
|
} else state.mode
|
||||||
|
state.copy(tree = action.tree, mode = mode)
|
||||||
|
}
|
||||||
|
is BookmarkAction.Select -> {
|
||||||
|
val selectedItems = if (state.mode is BookmarkState.Mode.Selecting) {
|
||||||
|
state.mode.selectedItems + action.item
|
||||||
|
} else setOf(action.item)
|
||||||
|
state.copy(mode = BookmarkState.Mode.Selecting(selectedItems))
|
||||||
|
}
|
||||||
|
is BookmarkAction.Deselect -> {
|
||||||
|
val selectedItems = if (state.mode is BookmarkState.Mode.Selecting) {
|
||||||
|
state.mode.selectedItems - action.item
|
||||||
|
} else setOf()
|
||||||
|
val mode =
|
||||||
|
if (selectedItems.isEmpty()) BookmarkState.Mode.Normal else BookmarkState.Mode.Selecting(selectedItems)
|
||||||
|
state.copy(mode = mode)
|
||||||
|
}
|
||||||
|
BookmarkAction.DeselectAll -> {
|
||||||
|
state.copy(mode = BookmarkState.Mode.Normal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun BookmarkNode.contains(item: BookmarkNode): Boolean {
|
||||||
|
return children?.contains(item) ?: false
|
||||||
|
}
|
|
@ -1,107 +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.library.bookmarks
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import io.reactivex.Observable
|
|
||||||
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.ext.getColorIntFromAttr
|
|
||||||
import org.mozilla.fenix.library.LibraryPageUIView
|
|
||||||
|
|
||||||
class BookmarkUIView(
|
|
||||||
container: ViewGroup,
|
|
||||||
actionEmitter: Observer<BookmarkAction>,
|
|
||||||
changesObservable: Observable<BookmarkChange>
|
|
||||||
) :
|
|
||||||
LibraryPageUIView<BookmarkState, BookmarkAction, BookmarkChange>(container, actionEmitter, changesObservable),
|
|
||||||
BackHandler {
|
|
||||||
|
|
||||||
var mode: BookmarkState.Mode = BookmarkState.Mode.Normal
|
|
||||||
private set
|
|
||||||
var tree: BookmarkNode? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
private var canGoBack = false
|
|
||||||
|
|
||||||
override val view: LinearLayout = LayoutInflater.from(container.context)
|
|
||||||
.inflate(R.layout.component_bookmark, container, true) as LinearLayout
|
|
||||||
|
|
||||||
private val bookmarkAdapter: BookmarkAdapter
|
|
||||||
|
|
||||||
init {
|
|
||||||
view.bookmark_list.apply {
|
|
||||||
bookmarkAdapter = BookmarkAdapter(view.bookmarks_empty_view, actionEmitter)
|
|
||||||
adapter = bookmarkAdapter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateView() = Consumer<BookmarkState> {
|
|
||||||
canGoBack = !(listOf(null, BookmarkRoot.Root.id).contains(it.tree?.guid))
|
|
||||||
if (it.tree != tree) {
|
|
||||||
tree = it.tree
|
|
||||||
}
|
|
||||||
if (it.mode != mode) {
|
|
||||||
mode = it.mode
|
|
||||||
actionEmitter.onNext(BookmarkAction.SwitchMode)
|
|
||||||
}
|
|
||||||
when (val modeCopy = it.mode) {
|
|
||||||
is BookmarkState.Mode.Normal -> setUIForNormalMode(it.tree)
|
|
||||||
is BookmarkState.Mode.Selecting -> setUIForSelectingMode(it.tree, modeCopy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed(): Boolean {
|
|
||||||
return when {
|
|
||||||
mode is BookmarkState.Mode.Selecting -> {
|
|
||||||
actionEmitter.onNext(BookmarkAction.DeselectAll)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
canGoBack -> {
|
|
||||||
actionEmitter.onNext(BookmarkAction.BackPressed)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSelected(): Set<BookmarkNode> = bookmarkAdapter.selected
|
|
||||||
|
|
||||||
private fun setUIForSelectingMode(
|
|
||||||
root: BookmarkNode?,
|
|
||||||
mode: BookmarkState.Mode.Selecting
|
|
||||||
) {
|
|
||||||
bookmarkAdapter.updateData(root, mode)
|
|
||||||
activity?.title =
|
|
||||||
context.getString(R.string.bookmarks_multi_select_title, mode.selectedItems.size)
|
|
||||||
setToolbarColors(
|
|
||||||
R.color.white_color,
|
|
||||||
R.attr.accentHighContrast.getColorIntFromAttr(context!!)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setUIForNormalMode(root: BookmarkNode?) {
|
|
||||||
bookmarkAdapter.updateData(root, BookmarkState.Mode.Normal)
|
|
||||||
setTitle(root)
|
|
||||||
setToolbarColors(
|
|
||||||
R.attr.primaryText.getColorIntFromAttr(context!!),
|
|
||||||
R.attr.foundation.getColorIntFromAttr(context)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setTitle(root: BookmarkNode?) {
|
|
||||||
activity?.title = when (root?.guid) {
|
|
||||||
BookmarkRoot.Mobile.id, null -> context.getString(R.string.library_bookmarks)
|
|
||||||
else -> root.title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,216 @@
|
||||||
|
/* 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.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import kotlinx.android.extensions.LayoutContainer
|
||||||
|
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.ext.getColorIntFromAttr
|
||||||
|
import org.mozilla.fenix.library.LibraryPageView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for the Bookmarks view.
|
||||||
|
* This interface is implemented by objects that want to respond to user interaction on the bookmarks management UI.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("TooManyFunctions")
|
||||||
|
interface BookmarkViewInteractor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swaps the head of the bookmarks tree, replacing it with a new, updated bookmarks tree.
|
||||||
|
*
|
||||||
|
* @param node the head node of the new bookmarks tree
|
||||||
|
*/
|
||||||
|
fun change(node: BookmarkNode)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a tab for a bookmark item.
|
||||||
|
*
|
||||||
|
* @param item the bookmark item to open
|
||||||
|
*/
|
||||||
|
fun open(item: BookmarkNode)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expands a bookmark folder in the bookmarks tree, providing a view of a different folder elsewhere in the tree.
|
||||||
|
*
|
||||||
|
* @param folder the bookmark folder to expand
|
||||||
|
*/
|
||||||
|
fun expand(folder: BookmarkNode)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switches the current bookmark multi-selection mode.
|
||||||
|
*
|
||||||
|
* @param mode the multi-select mode to switch to
|
||||||
|
*/
|
||||||
|
fun switchMode(mode: BookmarkState.Mode)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens up an interface to edit a bookmark node.
|
||||||
|
*
|
||||||
|
* @param node the bookmark node to edit
|
||||||
|
*/
|
||||||
|
fun edit(node: BookmarkNode)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects a bookmark node in multi-selection.
|
||||||
|
*
|
||||||
|
* @param node the bookmark node to select
|
||||||
|
*/
|
||||||
|
fun select(node: BookmarkNode)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* De-selects a bookmark node in multi-selection.
|
||||||
|
*
|
||||||
|
* @param node the bookmark node to deselect
|
||||||
|
*/
|
||||||
|
fun deselect(node: BookmarkNode)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* De-selects all bookmark nodes, clearing the multi-selection mode.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
fun deselectAll()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the URL of a bookmark item to the copy-paste buffer.
|
||||||
|
*
|
||||||
|
* @param item the bookmark item to copy the URL from
|
||||||
|
*/
|
||||||
|
fun copy(item: BookmarkNode)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the share sheet for a bookmark item.
|
||||||
|
*
|
||||||
|
* @param item the bookmark item to share
|
||||||
|
*/
|
||||||
|
fun share(item: BookmarkNode)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a bookmark item in a new tab.
|
||||||
|
*
|
||||||
|
* @param item the bookmark item to open in a new tab
|
||||||
|
*/
|
||||||
|
fun openInNewTab(item: BookmarkNode)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a bookmark item in a private tab.
|
||||||
|
*
|
||||||
|
* @param item the bookmark item to open in a private tab
|
||||||
|
*/
|
||||||
|
fun openInPrivateTab(item: BookmarkNode)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a bookmark node.
|
||||||
|
*
|
||||||
|
* @param node the bookmark node to delete
|
||||||
|
*/
|
||||||
|
fun delete(node: BookmarkNode)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a set of bookmark nodes.
|
||||||
|
*
|
||||||
|
* @param nodes the set of bookmark nodes to delete
|
||||||
|
*/
|
||||||
|
fun deleteMulti(nodes: Set<BookmarkNode>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles back presses for the bookmark screen, so navigation up the tree is possible.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
fun backPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
class BookmarkView(
|
||||||
|
private val container: ViewGroup,
|
||||||
|
val interactor: BookmarkViewInteractor
|
||||||
|
) : LibraryPageView(container), LayoutContainer, BackHandler {
|
||||||
|
|
||||||
|
override val containerView: View?
|
||||||
|
get() = container
|
||||||
|
|
||||||
|
var mode: BookmarkState.Mode = BookmarkState.Mode.Normal
|
||||||
|
private set
|
||||||
|
var tree: BookmarkNode? = null
|
||||||
|
private set
|
||||||
|
private var canGoBack = false
|
||||||
|
|
||||||
|
val view: LinearLayout = LayoutInflater.from(container.context)
|
||||||
|
.inflate(R.layout.component_bookmark, container, true) as LinearLayout
|
||||||
|
|
||||||
|
private val bookmarkAdapter: BookmarkAdapter
|
||||||
|
|
||||||
|
init {
|
||||||
|
view.bookmark_list.apply {
|
||||||
|
bookmarkAdapter = BookmarkAdapter(view.bookmarks_empty_view, interactor)
|
||||||
|
adapter = bookmarkAdapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(state: BookmarkState) {
|
||||||
|
canGoBack = !(listOf(null, BookmarkRoot.Root.id).contains(state.tree?.guid))
|
||||||
|
if (state.tree != tree) {
|
||||||
|
tree = state.tree
|
||||||
|
}
|
||||||
|
if (state.mode != mode) {
|
||||||
|
mode = state.mode
|
||||||
|
interactor.switchMode(mode)
|
||||||
|
}
|
||||||
|
when (val modeCopy = state.mode) {
|
||||||
|
is BookmarkState.Mode.Normal -> setUIForNormalMode(state.tree)
|
||||||
|
is BookmarkState.Mode.Selecting -> setUIForSelectingMode(state.tree, modeCopy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed(): Boolean {
|
||||||
|
return when {
|
||||||
|
mode is BookmarkState.Mode.Selecting -> {
|
||||||
|
interactor.deselectAll()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
canGoBack -> {
|
||||||
|
interactor.backPressed()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSelected(): Set<BookmarkNode> = bookmarkAdapter.selected
|
||||||
|
|
||||||
|
private fun setUIForSelectingMode(
|
||||||
|
root: BookmarkNode?,
|
||||||
|
mode: BookmarkState.Mode.Selecting
|
||||||
|
) {
|
||||||
|
bookmarkAdapter.updateData(root, mode)
|
||||||
|
activity?.title =
|
||||||
|
context.getString(R.string.bookmarks_multi_select_title, mode.selectedItems.size)
|
||||||
|
setToolbarColors(
|
||||||
|
R.color.white_color,
|
||||||
|
R.attr.accentHighContrast.getColorIntFromAttr(context)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setUIForNormalMode(root: BookmarkNode?) {
|
||||||
|
bookmarkAdapter.updateData(root, BookmarkState.Mode.Normal)
|
||||||
|
setTitle(root)
|
||||||
|
setToolbarColors(
|
||||||
|
R.attr.primaryText.getColorIntFromAttr(context),
|
||||||
|
R.attr.foundation.getColorIntFromAttr(context)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setTitle(root: BookmarkNode?) {
|
||||||
|
activity?.title = when (root?.guid) {
|
||||||
|
BookmarkRoot.Mobile.id, null -> context.getString(R.string.library_bookmarks)
|
||||||
|
else -> root.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,9 +4,11 @@
|
||||||
|
|
||||||
package org.mozilla.fenix.library.bookmarks
|
package org.mozilla.fenix.library.bookmarks
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import mozilla.components.concept.storage.BookmarkNode
|
import mozilla.components.concept.storage.BookmarkNode
|
||||||
|
|
||||||
class BookmarksSharedViewModel : ViewModel() {
|
class BookmarksSharedViewModel : ViewModel() {
|
||||||
|
var signedIn = MutableLiveData<Boolean>().apply { postValue(true) }
|
||||||
var selectedFolder: BookmarkNode? = null
|
var selectedFolder: BookmarkNode? = null
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,59 +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.library.bookmarks
|
|
||||||
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import org.mozilla.fenix.mvi.ViewState
|
|
||||||
import org.mozilla.fenix.mvi.Change
|
|
||||||
import org.mozilla.fenix.mvi.Action
|
|
||||||
import org.mozilla.fenix.mvi.ActionBusFactory
|
|
||||||
import org.mozilla.fenix.mvi.Reducer
|
|
||||||
import org.mozilla.fenix.mvi.UIComponent
|
|
||||||
import org.mozilla.fenix.mvi.UIComponentViewModelBase
|
|
||||||
import org.mozilla.fenix.mvi.UIComponentViewModelProvider
|
|
||||||
import org.mozilla.fenix.mvi.UIView
|
|
||||||
|
|
||||||
class SignInComponent(
|
|
||||||
private val container: ViewGroup,
|
|
||||||
bus: ActionBusFactory,
|
|
||||||
viewModelProvider: UIComponentViewModelProvider<SignInState, SignInChange>
|
|
||||||
) : UIComponent<SignInState, SignInAction, SignInChange>(
|
|
||||||
bus.getManagedEmitter(SignInAction::class.java),
|
|
||||||
bus.getSafeManagedObservable(SignInChange::class.java),
|
|
||||||
viewModelProvider
|
|
||||||
) {
|
|
||||||
override fun initView(): UIView<SignInState, SignInAction, SignInChange> =
|
|
||||||
SignInUIView(container, actionEmitter, changesObservable)
|
|
||||||
|
|
||||||
init {
|
|
||||||
bind()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class SignInState(val signedIn: Boolean) : ViewState
|
|
||||||
|
|
||||||
sealed class SignInAction : Action {
|
|
||||||
object ClickedSignIn : SignInAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class SignInChange : Change {
|
|
||||||
object SignedIn : SignInChange()
|
|
||||||
object SignedOut : SignInChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
class SignInViewModel(
|
|
||||||
initialState: SignInState
|
|
||||||
) : UIComponentViewModelBase<SignInState, SignInChange>(initialState, reducer) {
|
|
||||||
companion object {
|
|
||||||
val reducer = object : Reducer<SignInState, SignInChange> {
|
|
||||||
override fun invoke(state: SignInState, change: SignInChange): SignInState {
|
|
||||||
return when (change) {
|
|
||||||
SignInChange.SignedIn -> state.copy(signedIn = true)
|
|
||||||
SignInChange.SignedOut -> state.copy(signedIn = false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,36 +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.library.bookmarks
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import com.google.android.material.button.MaterialButton
|
|
||||||
import io.reactivex.Observable
|
|
||||||
import io.reactivex.Observer
|
|
||||||
import io.reactivex.functions.Consumer
|
|
||||||
import org.mozilla.fenix.R
|
|
||||||
import org.mozilla.fenix.mvi.UIView
|
|
||||||
|
|
||||||
class SignInUIView(
|
|
||||||
container: ViewGroup,
|
|
||||||
actionEmitter: Observer<SignInAction>,
|
|
||||||
changesObservable: Observable<SignInChange>
|
|
||||||
) : UIView<SignInState, SignInAction, SignInChange>(container, actionEmitter, changesObservable) {
|
|
||||||
|
|
||||||
override val view: MaterialButton = LayoutInflater.from(container.context)
|
|
||||||
.inflate(R.layout.component_sign_in, container, true)
|
|
||||||
.findViewById(R.id.bookmark_folders_sign_in)
|
|
||||||
|
|
||||||
init {
|
|
||||||
view.setOnClickListener {
|
|
||||||
actionEmitter.onNext(SignInAction.ClickedSignIn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateView() = Consumer<SignInState> {
|
|
||||||
view.visibility = if (it.signedIn) View.GONE else View.VISIBLE
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
/* 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.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
|
import kotlinx.android.extensions.LayoutContainer
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
|
||||||
|
interface SignInInteractor {
|
||||||
|
fun clickedSignIn()
|
||||||
|
fun signedIn()
|
||||||
|
fun signedOut()
|
||||||
|
}
|
||||||
|
|
||||||
|
class SignInView(
|
||||||
|
private val container: ViewGroup,
|
||||||
|
private val interactor: SignInInteractor
|
||||||
|
) : LayoutContainer {
|
||||||
|
|
||||||
|
override val containerView: View?
|
||||||
|
get() = container
|
||||||
|
|
||||||
|
val view: MaterialButton = LayoutInflater.from(container.context)
|
||||||
|
.inflate(R.layout.component_sign_in, container, true)
|
||||||
|
.findViewById(R.id.bookmark_folders_sign_in)
|
||||||
|
|
||||||
|
init {
|
||||||
|
view.setOnClickListener {
|
||||||
|
interactor.clickedSignIn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(signedIn: Boolean) {
|
||||||
|
view.visibility = if (signedIn) View.GONE else View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,7 +16,9 @@ import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
import kotlinx.android.synthetic.main.fragment_select_bookmark_folder.*
|
import kotlinx.android.synthetic.main.fragment_select_bookmark_folder.*
|
||||||
import kotlinx.android.synthetic.main.fragment_select_bookmark_folder.view.*
|
import kotlinx.android.synthetic.main.fragment_select_bookmark_folder.view.*
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
|
@ -27,8 +29,6 @@ import mozilla.components.concept.storage.BookmarkNode
|
||||||
import mozilla.components.concept.sync.AccountObserver
|
import mozilla.components.concept.sync.AccountObserver
|
||||||
import mozilla.components.concept.sync.OAuthAccount
|
import mozilla.components.concept.sync.OAuthAccount
|
||||||
import mozilla.components.concept.sync.Profile
|
import mozilla.components.concept.sync.Profile
|
||||||
import org.mozilla.fenix.BrowserDirection
|
|
||||||
import org.mozilla.fenix.FenixViewModelProvider
|
|
||||||
import org.mozilla.fenix.HomeActivity
|
import org.mozilla.fenix.HomeActivity
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.ext.getColorFromAttr
|
import org.mozilla.fenix.ext.getColorFromAttr
|
||||||
|
@ -37,14 +37,7 @@ import org.mozilla.fenix.ext.requireComponents
|
||||||
import org.mozilla.fenix.ext.setRootTitles
|
import org.mozilla.fenix.ext.setRootTitles
|
||||||
import org.mozilla.fenix.ext.withOptionalDesktopFolders
|
import org.mozilla.fenix.ext.withOptionalDesktopFolders
|
||||||
import org.mozilla.fenix.library.bookmarks.BookmarksSharedViewModel
|
import org.mozilla.fenix.library.bookmarks.BookmarksSharedViewModel
|
||||||
import org.mozilla.fenix.library.bookmarks.SignInAction
|
import org.mozilla.fenix.library.bookmarks.SignInView
|
||||||
import org.mozilla.fenix.library.bookmarks.SignInChange
|
|
||||||
import org.mozilla.fenix.library.bookmarks.SignInComponent
|
|
||||||
import org.mozilla.fenix.library.bookmarks.SignInState
|
|
||||||
import org.mozilla.fenix.library.bookmarks.SignInViewModel
|
|
||||||
import org.mozilla.fenix.mvi.ActionBusFactory
|
|
||||||
import org.mozilla.fenix.mvi.getAutoDisposeObservable
|
|
||||||
import org.mozilla.fenix.mvi.getManagedEmitter
|
|
||||||
|
|
||||||
@SuppressWarnings("TooManyFunctions")
|
@SuppressWarnings("TooManyFunctions")
|
||||||
class SelectBookmarkFolderFragment : Fragment(), AccountObserver {
|
class SelectBookmarkFolderFragment : Fragment(), AccountObserver {
|
||||||
|
@ -52,8 +45,8 @@ class SelectBookmarkFolderFragment : Fragment(), AccountObserver {
|
||||||
private val sharedViewModel: BookmarksSharedViewModel by activityViewModels()
|
private val sharedViewModel: BookmarksSharedViewModel by activityViewModels()
|
||||||
private var folderGuid: String? = null
|
private var folderGuid: String? = null
|
||||||
private var bookmarkNode: BookmarkNode? = null
|
private var bookmarkNode: BookmarkNode? = null
|
||||||
|
private lateinit var signInView: SignInView
|
||||||
private lateinit var signInComponent: SignInComponent
|
private lateinit var bookmarkInteractor: SelectBookmarkFolderInteractor
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -62,32 +55,22 @@ class SelectBookmarkFolderFragment : Fragment(), AccountObserver {
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
val view = inflater.inflate(R.layout.fragment_select_bookmark_folder, container, false)
|
val view = inflater.inflate(R.layout.fragment_select_bookmark_folder, container, false)
|
||||||
signInComponent = SignInComponent(
|
|
||||||
view.select_bookmark_layout,
|
bookmarkInteractor = SelectBookmarkFolderInteractor(
|
||||||
ActionBusFactory.get(this),
|
context!!,
|
||||||
FenixViewModelProvider.create(
|
findNavController(),
|
||||||
this,
|
sharedViewModel
|
||||||
SignInViewModel::class.java
|
|
||||||
) {
|
|
||||||
SignInViewModel(SignInState(false))
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
signInView = SignInView(view.select_bookmark_layout, bookmarkInteractor)
|
||||||
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onStart()
|
super.onViewCreated(view, savedInstanceState)
|
||||||
getAutoDisposeObservable<SignInAction>()
|
sharedViewModel.signedIn.observe(this@SelectBookmarkFolderFragment, Observer<Boolean> {
|
||||||
.subscribe {
|
signInView.update(it)
|
||||||
when (it) {
|
})
|
||||||
is SignInAction.ClickedSignIn -> {
|
|
||||||
requireComponents.services.accountsAuthFeature.beginAuthentication(requireContext())
|
|
||||||
view?.let {
|
|
||||||
(activity as HomeActivity).openToBrowser(BrowserDirection.FromBookmarksFolderSelect)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
@ -118,8 +101,8 @@ class SelectBookmarkFolderFragment : Fragment(), AccountObserver {
|
||||||
private fun checkIfSignedIn() {
|
private fun checkIfSignedIn() {
|
||||||
val accountManager = requireComponents.backgroundServices.accountManager
|
val accountManager = requireComponents.backgroundServices.accountManager
|
||||||
accountManager.register(this, owner = this)
|
accountManager.register(this, owner = this)
|
||||||
accountManager.authenticatedAccount()?.let { getManagedEmitter<SignInChange>().onNext(SignInChange.SignedIn) }
|
accountManager.authenticatedAccount()?.let { bookmarkInteractor.signedIn() }
|
||||||
?: getManagedEmitter<SignInChange>().onNext(SignInChange.SignedOut)
|
?: bookmarkInteractor.signedOut()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
@ -151,11 +134,11 @@ class SelectBookmarkFolderFragment : Fragment(), AccountObserver {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAuthenticated(account: OAuthAccount) {
|
override fun onAuthenticated(account: OAuthAccount) {
|
||||||
getManagedEmitter<SignInChange>().onNext(SignInChange.SignedIn)
|
bookmarkInteractor.signedIn()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoggedOut() {
|
override fun onLoggedOut() {
|
||||||
getManagedEmitter<SignInChange>().onNext(SignInChange.SignedOut)
|
bookmarkInteractor.signedOut()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onProfileUpdated(profile: Profile) {
|
override fun onProfileUpdated(profile: Profile) {
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
/* 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.selectfolder
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.library.bookmarks.BookmarksSharedViewModel
|
||||||
|
import org.mozilla.fenix.library.bookmarks.SignInInteractor
|
||||||
|
|
||||||
|
class SelectBookmarkFolderInteractor(
|
||||||
|
private val context: Context,
|
||||||
|
private val navController: NavController,
|
||||||
|
private val sharedViewModel: BookmarksSharedViewModel
|
||||||
|
) : SignInInteractor {
|
||||||
|
|
||||||
|
override fun clickedSignIn() {
|
||||||
|
context.components.services.launchPairingSignIn(context, navController)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun signedIn() {
|
||||||
|
sharedViewModel.signedIn.postValue(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun signedOut() {
|
||||||
|
sharedViewModel.signedIn.postValue(false)
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.Navigation
|
import androidx.navigation.Navigation
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.Preference.OnPreferenceClickListener
|
import androidx.preference.Preference.OnPreferenceClickListener
|
||||||
import androidx.preference.PreferenceCategory
|
import androidx.preference.PreferenceCategory
|
||||||
|
@ -25,10 +26,8 @@ import kotlinx.coroutines.launch
|
||||||
import mozilla.components.concept.sync.AccountObserver
|
import mozilla.components.concept.sync.AccountObserver
|
||||||
import mozilla.components.concept.sync.OAuthAccount
|
import mozilla.components.concept.sync.OAuthAccount
|
||||||
import mozilla.components.concept.sync.Profile
|
import mozilla.components.concept.sync.Profile
|
||||||
import mozilla.components.support.ktx.android.content.hasCamera
|
|
||||||
import org.mozilla.fenix.BrowserDirection
|
import org.mozilla.fenix.BrowserDirection
|
||||||
import org.mozilla.fenix.Config
|
import org.mozilla.fenix.Config
|
||||||
import org.mozilla.fenix.Experiments
|
|
||||||
import org.mozilla.fenix.FenixApplication
|
import org.mozilla.fenix.FenixApplication
|
||||||
import org.mozilla.fenix.HomeActivity
|
import org.mozilla.fenix.HomeActivity
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
|
@ -56,7 +55,6 @@ import org.mozilla.fenix.components.metrics.Event
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.getPreferenceKey
|
import org.mozilla.fenix.ext.getPreferenceKey
|
||||||
import org.mozilla.fenix.ext.requireComponents
|
import org.mozilla.fenix.ext.requireComponents
|
||||||
import org.mozilla.fenix.isInExperiment
|
|
||||||
import org.mozilla.fenix.utils.ItsNotBrokenSnack
|
import org.mozilla.fenix.utils.ItsNotBrokenSnack
|
||||||
|
|
||||||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||||
|
@ -225,21 +223,7 @@ class SettingsFragment : PreferenceFragmentCompat(), AccountObserver {
|
||||||
|
|
||||||
private fun getClickListenerForSignIn(): OnPreferenceClickListener {
|
private fun getClickListenerForSignIn(): OnPreferenceClickListener {
|
||||||
return OnPreferenceClickListener {
|
return OnPreferenceClickListener {
|
||||||
// Do not navigate to pairing UI if camera not available or pairing is disabled
|
context!!.components.services.launchPairingSignIn(context!!, findNavController())
|
||||||
if (context?.hasCamera() == true &&
|
|
||||||
context?.isInExperiment(Experiments.asFeatureFxAPairingDisabled) == false
|
|
||||||
) {
|
|
||||||
val directions = SettingsFragmentDirections.actionSettingsFragmentToTurnOnSyncFragment()
|
|
||||||
Navigation.findNavController(view!!).navigate(directions)
|
|
||||||
} else {
|
|
||||||
requireComponents.services.accountsAuthFeature.beginAuthentication(requireContext())
|
|
||||||
// TODO The sign-in web content populates session history,
|
|
||||||
// so pressing "back" after signing in won't take us back into the settings screen, but rather up the
|
|
||||||
// session history stack.
|
|
||||||
// We could auto-close this tab once we get to the end of the authentication process?
|
|
||||||
// Via an interceptor, perhaps.
|
|
||||||
requireComponents.analytics.metrics.track(Event.SyncAuthSignIn)
|
|
||||||
}
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,10 @@
|
||||||
android:id="@+id/action_global_crash_reporter"
|
android:id="@+id/action_global_crash_reporter"
|
||||||
app:destination="@id/crashReporterFragment" />
|
app:destination="@id/crashReporterFragment" />
|
||||||
|
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_global_turn_on_sync"
|
||||||
|
app:destination="@id/turnOnSyncFragment" />
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/homeFragment"
|
android:id="@+id/homeFragment"
|
||||||
android:name="org.mozilla.fenix.home.HomeFragment"
|
android:name="org.mozilla.fenix.home.HomeFragment"
|
||||||
|
|
|
@ -4,22 +4,15 @@
|
||||||
|
|
||||||
package org.mozilla.fenix.library.bookmarks
|
package org.mozilla.fenix.library.bookmarks
|
||||||
|
|
||||||
import io.mockk.Runs
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.just
|
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.spyk
|
import io.mockk.spyk
|
||||||
import io.mockk.verifyOrder
|
import io.mockk.verifyOrder
|
||||||
import io.mockk.verifySequence
|
|
||||||
import io.reactivex.Observer
|
|
||||||
import io.reactivex.observers.TestObserver
|
|
||||||
import mozilla.components.concept.storage.BookmarkNode
|
import mozilla.components.concept.storage.BookmarkNode
|
||||||
import mozilla.components.concept.storage.BookmarkNodeType
|
import mozilla.components.concept.storage.BookmarkNodeType
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.mozilla.fenix.TestApplication
|
import org.mozilla.fenix.TestApplication
|
||||||
import org.mozilla.fenix.TestUtils.setRxSchedulers
|
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
@ -28,20 +21,16 @@ import org.robolectric.annotation.Config
|
||||||
internal class BookmarkAdapterTest {
|
internal class BookmarkAdapterTest {
|
||||||
|
|
||||||
private lateinit var bookmarkAdapter: BookmarkAdapter
|
private lateinit var bookmarkAdapter: BookmarkAdapter
|
||||||
private lateinit var emitter: Observer<BookmarkAction>
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
setRxSchedulers()
|
|
||||||
emitter = TestObserver()
|
|
||||||
bookmarkAdapter = spyk(
|
bookmarkAdapter = spyk(
|
||||||
BookmarkAdapter(mockk(), emitter), recordPrivateCalls = true
|
BookmarkAdapter(mockk(relaxed = true), mockk())
|
||||||
)
|
)
|
||||||
every { bookmarkAdapter.notifyDataSetChanged() } just Runs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `update adapter from tree of bookmark nodes`() {
|
fun `update adapter from tree of bookmark nodes, null tree returns empty list`() {
|
||||||
val tree = BookmarkNode(
|
val tree = BookmarkNode(
|
||||||
BookmarkNodeType.FOLDER, "123", null, 0, "Mobile", null, listOf(
|
BookmarkNodeType.FOLDER, "123", null, 0, "Mobile", null, listOf(
|
||||||
BookmarkNode(BookmarkNodeType.ITEM, "456", "123", 0, "Mozilla", "http://mozilla.org", null),
|
BookmarkNode(BookmarkNodeType.ITEM, "456", "123", 0, "Mozilla", "http://mozilla.org", null),
|
||||||
|
@ -58,21 +47,12 @@ internal class BookmarkAdapterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
bookmarkAdapter.updateData(tree, BookmarkState.Mode.Normal)
|
bookmarkAdapter.updateData(tree, BookmarkState.Mode.Normal)
|
||||||
|
bookmarkAdapter.updateData(null, BookmarkState.Mode.Normal)
|
||||||
verifyOrder {
|
verifyOrder {
|
||||||
bookmarkAdapter.updateData(tree, BookmarkState.Mode.Normal)
|
bookmarkAdapter.updateData(tree, BookmarkState.Mode.Normal)
|
||||||
bookmarkAdapter setProperty "tree" value tree.children
|
|
||||||
bookmarkAdapter setProperty "mode" value BookmarkState.Mode.Normal
|
|
||||||
bookmarkAdapter.notifyItemRangeInserted(0, 3)
|
bookmarkAdapter.notifyItemRangeInserted(0, 3)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `passing null tree returns empty list`() {
|
|
||||||
bookmarkAdapter.updateData(null, BookmarkState.Mode.Normal)
|
|
||||||
verifySequence {
|
|
||||||
bookmarkAdapter.updateData(null, BookmarkState.Mode.Normal)
|
bookmarkAdapter.updateData(null, BookmarkState.Mode.Normal)
|
||||||
bookmarkAdapter setProperty "tree" value listOf<BookmarkNode?>()
|
bookmarkAdapter.notifyItemRangeRemoved(0, 3)
|
||||||
bookmarkAdapter setProperty "mode" value BookmarkState.Mode.Normal
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,291 @@
|
||||||
|
/* 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.Context
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavDestination
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.spyk
|
||||||
|
import io.mockk.verify
|
||||||
|
import io.mockk.verifyOrder
|
||||||
|
import mozilla.components.concept.storage.BookmarkNode
|
||||||
|
import mozilla.components.concept.storage.BookmarkNodeType
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mozilla.fenix.BrowserDirection
|
||||||
|
import org.mozilla.fenix.FenixApplication
|
||||||
|
import org.mozilla.fenix.HomeActivity
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.components.FenixSnackbarPresenter
|
||||||
|
import org.mozilla.fenix.components.Services
|
||||||
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
|
import org.mozilla.fenix.components.metrics.MetricController
|
||||||
|
import org.mozilla.fenix.ext.asActivity
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
|
|
||||||
|
class BookmarkFragmentInteractorTest {
|
||||||
|
|
||||||
|
private lateinit var interactor: BookmarkFragmentInteractor
|
||||||
|
|
||||||
|
private val context: Context = mockk(relaxed = true)
|
||||||
|
private val navController: NavController = mockk(relaxed = true)
|
||||||
|
private val bookmarkStore = spyk(BookmarkStore(BookmarkState(null)))
|
||||||
|
private val sharedViewModel: BookmarksSharedViewModel = mockk(relaxed = true)
|
||||||
|
private val snackbarPresenter: FenixSnackbarPresenter = mockk(relaxed = true)
|
||||||
|
private val deleteBookmarkNodes: (Set<BookmarkNode>, Event) -> Unit = mockk(relaxed = true)
|
||||||
|
|
||||||
|
private val applicationContext: FenixApplication = mockk(relaxed = true)
|
||||||
|
private val homeActivity: HomeActivity = mockk(relaxed = true)
|
||||||
|
private val metrics: MetricController = mockk(relaxed = true)
|
||||||
|
|
||||||
|
private val item = BookmarkNode(BookmarkNodeType.ITEM, "456", "123", 0, "Mozilla", "http://mozilla.org", null)
|
||||||
|
private val separator = BookmarkNode(BookmarkNodeType.SEPARATOR, "789", "123", 1, null, null, null)
|
||||||
|
private val subfolder = BookmarkNode(BookmarkNodeType.FOLDER, "987", "123", 0, "Subfolder", null, listOf())
|
||||||
|
private val childItem = BookmarkNode(
|
||||||
|
BookmarkNodeType.ITEM,
|
||||||
|
"987",
|
||||||
|
"123",
|
||||||
|
2,
|
||||||
|
"Firefox",
|
||||||
|
"https://www.mozilla.org/en-US/firefox/",
|
||||||
|
null
|
||||||
|
)
|
||||||
|
private val tree = BookmarkNode(
|
||||||
|
BookmarkNodeType.FOLDER, "123", null, 0, "Mobile", null, listOf(item, separator, childItem, subfolder)
|
||||||
|
)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
mockkStatic(
|
||||||
|
"org.mozilla.fenix.ext.ContextKt",
|
||||||
|
"androidx.core.content.ContextCompat",
|
||||||
|
"android.content.ClipData"
|
||||||
|
)
|
||||||
|
every { any<Context>().asActivity() } returns homeActivity
|
||||||
|
every { context.applicationContext } returns applicationContext
|
||||||
|
every { applicationContext.components.analytics.metrics } returns metrics
|
||||||
|
every { navController.currentDestination } returns NavDestination("").apply { id = R.id.bookmarkFragment }
|
||||||
|
every { bookmarkStore.dispatch(any()) } returns mockk()
|
||||||
|
|
||||||
|
interactor =
|
||||||
|
BookmarkFragmentInteractor(
|
||||||
|
context,
|
||||||
|
navController,
|
||||||
|
bookmarkStore,
|
||||||
|
sharedViewModel,
|
||||||
|
snackbarPresenter,
|
||||||
|
deleteBookmarkNodes
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `update bookmarks tree`() {
|
||||||
|
interactor.change(tree)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
bookmarkStore.dispatch(BookmarkAction.Change(tree))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `open a bookmark item`() {
|
||||||
|
interactor.open(item)
|
||||||
|
|
||||||
|
val url = item.url!!
|
||||||
|
verifyOrder {
|
||||||
|
homeActivity.openToBrowserAndLoad(
|
||||||
|
searchTermOrURL = url,
|
||||||
|
newTab = true,
|
||||||
|
from = BrowserDirection.FromBookmarks
|
||||||
|
)
|
||||||
|
}
|
||||||
|
metrics.track(Event.OpenedBookmark)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `expand a level of bookmarks`() {
|
||||||
|
interactor.expand(tree)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
navController.navigate(BookmarkFragmentDirections.actionBookmarkFragmentSelf(tree.guid))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `switch between bookmark selection modes`() {
|
||||||
|
interactor.switchMode(BookmarkState.Mode.Normal)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
homeActivity.invalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `press the edit bookmark button`() {
|
||||||
|
interactor.edit(item)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
navController.navigate(BookmarkFragmentDirections.actionBookmarkFragmentToBookmarkEditFragment(item.guid))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `select a bookmark item`() {
|
||||||
|
interactor.select(item)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
bookmarkStore.dispatch(BookmarkAction.Select(item))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deselect a bookmark item`() {
|
||||||
|
interactor.deselect(item)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
bookmarkStore.dispatch(BookmarkAction.Deselect(item))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deselectAll bookmark items`() {
|
||||||
|
interactor.deselectAll()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
bookmarkStore.dispatch(BookmarkAction.DeselectAll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `copy a bookmark item`() {
|
||||||
|
val clipboardManager: ClipboardManager = mockk(relaxed = true)
|
||||||
|
every { any<Context>().getSystemService<ClipboardManager>() } returns clipboardManager
|
||||||
|
every { ClipData.newPlainText(any(), any()) } returns mockk(relaxed = true)
|
||||||
|
|
||||||
|
interactor.copy(item)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
metrics.track(Event.CopyBookmark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `share a bookmark item`() {
|
||||||
|
interactor.share(item)
|
||||||
|
|
||||||
|
verifyOrder {
|
||||||
|
navController.navigate(
|
||||||
|
BookmarkFragmentDirections.actionBookmarkFragmentToShareFragment(
|
||||||
|
item.url,
|
||||||
|
item.title
|
||||||
|
)
|
||||||
|
)
|
||||||
|
metrics.track(Event.ShareBookmark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `open a bookmark item in a new tab`() {
|
||||||
|
interactor.openInNewTab(item)
|
||||||
|
|
||||||
|
val url = item.url!!
|
||||||
|
verifyOrder {
|
||||||
|
homeActivity.openToBrowserAndLoad(
|
||||||
|
searchTermOrURL = url,
|
||||||
|
newTab = true,
|
||||||
|
from = BrowserDirection.FromBookmarks
|
||||||
|
)
|
||||||
|
metrics.track(Event.OpenedBookmarkInNewTab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `open a bookmark item in a private tab`() {
|
||||||
|
interactor.openInPrivateTab(item)
|
||||||
|
|
||||||
|
val url = item.url!!
|
||||||
|
verifyOrder {
|
||||||
|
homeActivity.openToBrowserAndLoad(
|
||||||
|
searchTermOrURL = url,
|
||||||
|
newTab = true,
|
||||||
|
from = BrowserDirection.FromBookmarks
|
||||||
|
)
|
||||||
|
metrics.track(Event.OpenedBookmarkInPrivateTab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `delete a bookmark item`() {
|
||||||
|
interactor.delete(item)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
deleteBookmarkNodes(setOf(item), Event.RemoveBookmark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `delete a bookmark folder`() {
|
||||||
|
interactor.delete(subfolder)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
deleteBookmarkNodes(setOf(subfolder), Event.RemoveBookmarkFolder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `delete multiple bookmarks`() {
|
||||||
|
interactor.deleteMulti(setOf(item, subfolder))
|
||||||
|
|
||||||
|
verify {
|
||||||
|
deleteBookmarkNodes(setOf(item, subfolder), Event.RemoveBookmarks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `press the back button`() {
|
||||||
|
interactor.backPressed()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicked sign in on bookmarks screen`() {
|
||||||
|
val services: Services = mockk(relaxed = true)
|
||||||
|
every { context.components.services } returns services
|
||||||
|
|
||||||
|
interactor.clickedSignIn()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
context.components.services
|
||||||
|
services.launchPairingSignIn(context, navController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `got signed in signal on bookmarks screen`() {
|
||||||
|
interactor.signedIn()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
sharedViewModel.signedIn.postValue(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `got signed out signal on bookmarks screen`() {
|
||||||
|
interactor.signedOut()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
sharedViewModel.signedIn.postValue(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,58 +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.library.bookmarks
|
|
||||||
|
|
||||||
import androidx.fragment.app.testing.FragmentScenario
|
|
||||||
import androidx.fragment.app.testing.launchFragmentInContainer
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.Navigation
|
|
||||||
import io.mockk.Runs
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.just
|
|
||||||
import io.mockk.mockk
|
|
||||||
import mozilla.appservices.places.BookmarkRoot
|
|
||||||
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.TestApplication
|
|
||||||
import org.mozilla.fenix.TestUtils
|
|
||||||
import org.robolectric.RobolectricTestRunner
|
|
||||||
import org.robolectric.annotation.Config
|
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
|
||||||
@Config(application = TestApplication::class)
|
|
||||||
class BookmarkFragmentTest {
|
|
||||||
|
|
||||||
private lateinit var scenario: FragmentScenario<BookmarkFragment>
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
TestUtils.setRxSchedulers()
|
|
||||||
|
|
||||||
val mockNavController = mockk<NavController>()
|
|
||||||
every { mockNavController.addOnDestinationChangedListener(any()) } just Runs
|
|
||||||
|
|
||||||
val args = BookmarkFragmentArgs(BookmarkRoot.Mobile.id).toBundle()
|
|
||||||
scenario =
|
|
||||||
launchFragmentInContainer<BookmarkFragment>(fragmentArgs = args, themeResId = R.style.NormalTheme) {
|
|
||||||
BookmarkFragment().also { fragment ->
|
|
||||||
fragment.viewLifecycleOwnerLiveData.observeForever {
|
|
||||||
if (it != null) {
|
|
||||||
Navigation.setViewNavController(fragment.requireView(), mockNavController)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `test initial bookmarks fragment ui`() {
|
|
||||||
scenario.onFragment { fragment ->
|
|
||||||
assertEquals(fragment.getString(R.string.library_bookmarks), fragment.activity?.title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,161 @@
|
||||||
|
/* 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 assertk.assertThat
|
||||||
|
import assertk.assertions.isEqualTo
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import mozilla.components.concept.storage.BookmarkNode
|
||||||
|
import mozilla.components.concept.storage.BookmarkNodeType
|
||||||
|
import org.junit.Assert.assertSame
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class BookmarkStoreTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `change the tree of bookmarks starting from an empty tree`() = runBlocking {
|
||||||
|
val initialState = BookmarkState(null)
|
||||||
|
val store = BookmarkStore(initialState)
|
||||||
|
|
||||||
|
assertThat(BookmarkState(null, BookmarkState.Mode.Normal)).isEqualTo(store.state)
|
||||||
|
|
||||||
|
store.dispatch(BookmarkAction.Change(tree)).join()
|
||||||
|
|
||||||
|
assertThat(initialState.copy(tree = tree)).isEqualTo(store.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `change the tree of bookmarks starting from an existing tree`() = runBlocking {
|
||||||
|
val initialState = BookmarkState(tree)
|
||||||
|
val store = BookmarkStore(initialState)
|
||||||
|
|
||||||
|
assertThat(BookmarkState(tree, BookmarkState.Mode.Normal)).isEqualTo(store.state)
|
||||||
|
|
||||||
|
store.dispatch(BookmarkAction.Change(newTree)).join()
|
||||||
|
|
||||||
|
assertThat(initialState.copy(tree = newTree)).isEqualTo(store.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `change the tree of bookmarks to the same value`() = runBlocking {
|
||||||
|
val initialState = BookmarkState(tree)
|
||||||
|
val store = BookmarkStore(initialState)
|
||||||
|
|
||||||
|
assertThat(BookmarkState(tree, BookmarkState.Mode.Normal)).isEqualTo(store.state)
|
||||||
|
|
||||||
|
store.dispatch(BookmarkAction.Change(tree)).join()
|
||||||
|
|
||||||
|
assertSame(initialState, store.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ensure selected items remain selected after a tree change`() = runBlocking {
|
||||||
|
val initialState = BookmarkState(tree, BookmarkState.Mode.Selecting(setOf(item, subfolder)))
|
||||||
|
val store = BookmarkStore(initialState)
|
||||||
|
|
||||||
|
store.dispatch(BookmarkAction.Change(newTree)).join()
|
||||||
|
|
||||||
|
assertThat(BookmarkState(newTree, BookmarkState.Mode.Selecting(setOf(subfolder)))).isEqualTo(store.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `select and deselect bookmarks changes the mode`() = runBlocking {
|
||||||
|
val initialState = BookmarkState(tree)
|
||||||
|
val store = BookmarkStore(initialState)
|
||||||
|
|
||||||
|
store.dispatch(BookmarkAction.Select(childItem)).join()
|
||||||
|
|
||||||
|
assertThat(BookmarkState(tree, BookmarkState.Mode.Selecting(setOf(childItem)))).isEqualTo(store.state)
|
||||||
|
|
||||||
|
store.dispatch(BookmarkAction.Deselect(childItem)).join()
|
||||||
|
|
||||||
|
assertThat(BookmarkState(tree, BookmarkState.Mode.Normal)).isEqualTo(store.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `selecting the same item twice does nothing`() = runBlocking {
|
||||||
|
val initialState = BookmarkState(tree, BookmarkState.Mode.Selecting(setOf(item, subfolder)))
|
||||||
|
val store = BookmarkStore(initialState)
|
||||||
|
|
||||||
|
store.dispatch(BookmarkAction.Select(item)).join()
|
||||||
|
|
||||||
|
assertSame(initialState, store.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deselecting an unselected bookmark does nothing`() = runBlocking {
|
||||||
|
val initialState = BookmarkState(tree, BookmarkState.Mode.Selecting(setOf(childItem)))
|
||||||
|
val store = BookmarkStore(initialState)
|
||||||
|
|
||||||
|
store.dispatch(BookmarkAction.Deselect(item)).join()
|
||||||
|
|
||||||
|
assertSame(initialState, store.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deselecting while not in selecting mode does nothing`() = runBlocking {
|
||||||
|
val initialState = BookmarkState(tree, BookmarkState.Mode.Normal)
|
||||||
|
val store = BookmarkStore(initialState)
|
||||||
|
|
||||||
|
store.dispatch(BookmarkAction.Deselect(item)).join()
|
||||||
|
|
||||||
|
assertSame(initialState, store.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deselect all bookmarks changes the mode`() = runBlocking {
|
||||||
|
val initialState = BookmarkState(tree, BookmarkState.Mode.Selecting(setOf(item, childItem)))
|
||||||
|
val store = BookmarkStore(initialState)
|
||||||
|
|
||||||
|
store.dispatch(BookmarkAction.DeselectAll).join()
|
||||||
|
|
||||||
|
assertThat(initialState.copy(mode = BookmarkState.Mode.Normal)).isEqualTo(store.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deselect all bookmarks when none are selected`() = runBlocking {
|
||||||
|
val initialState = BookmarkState(tree, BookmarkState.Mode.Normal)
|
||||||
|
val store = BookmarkStore(initialState)
|
||||||
|
|
||||||
|
store.dispatch(BookmarkAction.DeselectAll)
|
||||||
|
|
||||||
|
assertSame(initialState, store.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deleting bookmarks changes the mode`() = runBlocking {
|
||||||
|
val initialState = BookmarkState(tree, BookmarkState.Mode.Selecting(setOf(item, childItem)))
|
||||||
|
val store = BookmarkStore(initialState)
|
||||||
|
|
||||||
|
store.dispatch(BookmarkAction.Change(newTree)).join()
|
||||||
|
|
||||||
|
assertThat(initialState.copy(tree = newTree, mode = BookmarkState.Mode.Normal)).isEqualTo(store.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val item = BookmarkNode(BookmarkNodeType.ITEM, "456", "123", 0, "Mozilla", "http://mozilla.org", null)
|
||||||
|
private val separator = BookmarkNode(BookmarkNodeType.SEPARATOR, "789", "123", 1, null, null, null)
|
||||||
|
private val subfolder = BookmarkNode(BookmarkNodeType.FOLDER, "987", "123", 0, "Subfolder", null, listOf())
|
||||||
|
private val childItem = BookmarkNode(
|
||||||
|
BookmarkNodeType.ITEM,
|
||||||
|
"987",
|
||||||
|
"123",
|
||||||
|
2,
|
||||||
|
"Firefox",
|
||||||
|
"https://www.mozilla.org/en-US/firefox/",
|
||||||
|
null
|
||||||
|
)
|
||||||
|
private val tree = BookmarkNode(
|
||||||
|
BookmarkNodeType.FOLDER, "123", null, 0, "Mobile", null, listOf(item, separator, childItem, subfolder)
|
||||||
|
)
|
||||||
|
private val newTree = BookmarkNode(
|
||||||
|
BookmarkNodeType.FOLDER,
|
||||||
|
"123",
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
"Mobile",
|
||||||
|
null,
|
||||||
|
listOf(separator, subfolder)
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,83 +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.library.bookmarks
|
|
||||||
|
|
||||||
import io.mockk.MockKAnnotations
|
|
||||||
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.Before
|
|
||||||
import org.junit.Test
|
|
||||||
import org.mozilla.fenix.TestUtils
|
|
||||||
import org.mozilla.fenix.TestUtils.bus
|
|
||||||
import org.mozilla.fenix.ext.minus
|
|
||||||
import org.mozilla.fenix.mvi.getManagedEmitter
|
|
||||||
|
|
||||||
class BookmarkViewModelTest {
|
|
||||||
|
|
||||||
private lateinit var bookmarkViewModel: BookmarkViewModel
|
|
||||||
private lateinit var bookmarkObserver: TestObserver<BookmarkState>
|
|
||||||
private lateinit var emitter: Observer<BookmarkChange>
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
MockKAnnotations.init(this)
|
|
||||||
TestUtils.setRxSchedulers()
|
|
||||||
|
|
||||||
bookmarkViewModel = BookmarkViewModel.create()
|
|
||||||
bookmarkObserver = bookmarkViewModel.state.test()
|
|
||||||
bus.getSafeManagedObservable(BookmarkChange::class.java)
|
|
||||||
.subscribe(bookmarkViewModel.changes::onNext)
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `select and delete 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.Change(tree - itemToSelect.guid))
|
|
||||||
|
|
||||||
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 - itemToSelect.guid, BookmarkState.Mode.Normal)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -48,6 +48,7 @@ object Versions {
|
||||||
const val junit = "4.12"
|
const val junit = "4.12"
|
||||||
const val mockito = "2.24.5"
|
const val mockito = "2.24.5"
|
||||||
const val mockk = "1.9.kotlin12"
|
const val mockk = "1.9.kotlin12"
|
||||||
|
const val assertk = "0.19"
|
||||||
const val flipper = "0.21.0"
|
const val flipper = "0.21.0"
|
||||||
const val soLoader = "0.5.1"
|
const val soLoader = "0.5.1"
|
||||||
|
|
||||||
|
@ -182,6 +183,7 @@ object Deps {
|
||||||
const val mockito_core = "org.mockito:mockito-core:${Versions.mockito}"
|
const val mockito_core = "org.mockito:mockito-core:${Versions.mockito}"
|
||||||
const val mockito_android = "org.mockito:mockito-android:${Versions.mockito}"
|
const val mockito_android = "org.mockito:mockito-android:${Versions.mockito}"
|
||||||
const val mockk = "io.mockk:mockk:${Versions.mockk}"
|
const val mockk = "io.mockk:mockk:${Versions.mockk}"
|
||||||
|
const val assertk = "com.willowtreeapps.assertk:assertk-jvm:${Versions.assertk}"
|
||||||
|
|
||||||
const val flipper = "com.facebook.flipper:flipper:${Versions.flipper}"
|
const val flipper = "com.facebook.flipper:flipper:${Versions.flipper}"
|
||||||
const val flipper_noop = "com.facebook.flipper:flipper-noop:${Versions.flipper}"
|
const val flipper_noop = "com.facebook.flipper:flipper-noop:${Versions.flipper}"
|
||||||
|
|
Loading…
Reference in New Issue