parent
4747f2c165
commit
3dc20543e3
|
@ -412,6 +412,7 @@ dependencies {
|
|||
testImplementation Deps.mockito_core
|
||||
androidTestImplementation Deps.mockito_android
|
||||
testImplementation Deps.mockk
|
||||
testImplementation Deps.assertk
|
||||
|
||||
debugImplementation Deps.flipper
|
||||
debugImplementation Deps.soLoader
|
||||
|
|
|
@ -137,3 +137,18 @@ private class FenixSnackbarCallback(
|
|||
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
|
||||
|
||||
import android.content.Context
|
||||
import androidx.navigation.NavController
|
||||
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.metrics.Event
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.isInExperiment
|
||||
import org.mozilla.fenix.test.Mockable
|
||||
|
||||
/**
|
||||
|
@ -21,4 +29,26 @@ class Services(
|
|||
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
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import androidx.core.content.getSystemService
|
||||
import mozilla.appservices.places.BookmarkRoot
|
||||
import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
|
||||
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 {
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
|
@ -15,27 +16,14 @@ import androidx.appcompat.view.menu.ActionMenuItemView
|
|||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.forEach
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Observer
|
||||
import org.mozilla.fenix.R
|
||||
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
|
||||
|
||||
/**
|
||||
* Shared base class for [org.mozilla.fenix.library.bookmarks.BookmarkUIView] and
|
||||
* [org.mozilla.fenix.library.history.HistoryUIView].
|
||||
*/
|
||||
abstract class LibraryPageUIView<S : ViewState, A : Action, C : Change>(
|
||||
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()
|
||||
open class LibraryPageView(
|
||||
container: ViewGroup
|
||||
) {
|
||||
protected val context: Context = container.context
|
||||
protected val activity = context.asActivity()
|
||||
|
||||
/**
|
||||
* 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.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.reactivex.Observer
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import kotlinx.android.synthetic.main.bookmark_row.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -30,7 +29,7 @@ import org.mozilla.fenix.ext.increaseTapArea
|
|||
import org.mozilla.fenix.utils.AdapterWithJob
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkAction>) :
|
||||
class BookmarkAdapter(val emptyView: View, val interactor: BookmarkViewInteractor) :
|
||||
AdapterWithJob<BookmarkAdapter.BookmarkNodeViewHolder>() {
|
||||
|
||||
private var tree: List<BookmarkNode> = listOf()
|
||||
|
@ -87,13 +86,13 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
|
|||
|
||||
return when (viewType) {
|
||||
BookmarkItemViewHolder.viewType.ordinal -> BookmarkItemViewHolder(
|
||||
view, actionEmitter, adapterJob
|
||||
view, interactor, adapterJob
|
||||
)
|
||||
BookmarkFolderViewHolder.viewType.ordinal -> BookmarkFolderViewHolder(
|
||||
view, actionEmitter, adapterJob
|
||||
view, interactor, adapterJob
|
||||
)
|
||||
BookmarkSeparatorViewHolder.viewType.ordinal -> BookmarkSeparatorViewHolder(
|
||||
view, actionEmitter, adapterJob
|
||||
view, interactor, adapterJob
|
||||
)
|
||||
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(
|
||||
view: View,
|
||||
val actionEmitter: Observer<BookmarkAction>,
|
||||
val interactor: BookmarkViewInteractor,
|
||||
private val job: Job,
|
||||
override val containerView: View? = view
|
||||
) :
|
||||
|
@ -134,11 +133,11 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
|
|||
|
||||
class BookmarkItemViewHolder(
|
||||
view: View,
|
||||
actionEmitter: Observer<BookmarkAction>,
|
||||
interactor: BookmarkViewInteractor,
|
||||
job: Job,
|
||||
override val containerView: View? = view
|
||||
) :
|
||||
BookmarkNodeViewHolder(view, actionEmitter, job, containerView) {
|
||||
BookmarkNodeViewHolder(view, interactor, job, containerView) {
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
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) {
|
||||
when (it) {
|
||||
is BookmarkItemMenu.Item.Edit -> {
|
||||
actionEmitter.onNext(BookmarkAction.Edit(item))
|
||||
interactor.edit(item)
|
||||
}
|
||||
is BookmarkItemMenu.Item.Select -> {
|
||||
actionEmitter.onNext(BookmarkAction.Select(item))
|
||||
interactor.select(item)
|
||||
}
|
||||
is BookmarkItemMenu.Item.Copy -> {
|
||||
actionEmitter.onNext(BookmarkAction.Copy(item))
|
||||
interactor.copy(item)
|
||||
}
|
||||
is BookmarkItemMenu.Item.Share -> {
|
||||
actionEmitter.onNext(BookmarkAction.Share(item))
|
||||
interactor.share(item)
|
||||
}
|
||||
is BookmarkItemMenu.Item.OpenInNewTab -> {
|
||||
actionEmitter.onNext(BookmarkAction.OpenInNewTab(item))
|
||||
interactor.openInNewTab(item)
|
||||
}
|
||||
is BookmarkItemMenu.Item.OpenInPrivateTab -> {
|
||||
actionEmitter.onNext(BookmarkAction.OpenInPrivateTab(item))
|
||||
interactor.openInPrivateTab(item)
|
||||
}
|
||||
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 {
|
||||
if (mode == BookmarkState.Mode.Normal) {
|
||||
actionEmitter.onNext(BookmarkAction.Open(item))
|
||||
interactor.open(item)
|
||||
} else {
|
||||
if (selected) actionEmitter.onNext(BookmarkAction.Deselect(item)) else actionEmitter.onNext(
|
||||
BookmarkAction.Select(item)
|
||||
)
|
||||
if (selected) interactor.deselect(item) else interactor.select(item)
|
||||
}
|
||||
}
|
||||
|
||||
bookmark_layout.setOnLongClickListener {
|
||||
if (mode == BookmarkState.Mode.Normal) {
|
||||
if (selected) actionEmitter.onNext(BookmarkAction.Deselect(item)) else actionEmitter.onNext(
|
||||
BookmarkAction.Select(item)
|
||||
)
|
||||
if (selected) interactor.deselect(item) else interactor.select(item)
|
||||
true
|
||||
} else false
|
||||
}
|
||||
|
@ -255,11 +250,11 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
|
|||
|
||||
class BookmarkFolderViewHolder(
|
||||
view: View,
|
||||
actionEmitter: Observer<BookmarkAction>,
|
||||
interactor: BookmarkViewInteractor,
|
||||
job: Job,
|
||||
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) {
|
||||
containerView?.context?.let {
|
||||
|
@ -304,13 +299,13 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
|
|||
val bookmarkItemMenu = BookmarkItemMenu(containerView.context, item) {
|
||||
when (it) {
|
||||
is BookmarkItemMenu.Item.Edit -> {
|
||||
actionEmitter.onNext(BookmarkAction.Edit(item))
|
||||
interactor.edit(item)
|
||||
}
|
||||
is BookmarkItemMenu.Item.Select -> {
|
||||
actionEmitter.onNext(BookmarkAction.Select(item))
|
||||
interactor.select(item)
|
||||
}
|
||||
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 {
|
||||
if (mode == BookmarkState.Mode.Normal) {
|
||||
actionEmitter.onNext(BookmarkAction.Expand(item))
|
||||
interactor.expand(item)
|
||||
} else {
|
||||
if (selected) actionEmitter.onNext(BookmarkAction.Deselect(item)) else actionEmitter.onNext(
|
||||
BookmarkAction.Select(item)
|
||||
)
|
||||
if (selected) interactor.deselect(item) else interactor.select(item)
|
||||
}
|
||||
}
|
||||
|
||||
bookmark_layout.setOnLongClickListener {
|
||||
if (mode == BookmarkState.Mode.Normal && !item.inRoots()) {
|
||||
if (selected) actionEmitter.onNext(BookmarkAction.Deselect(item)) else actionEmitter.onNext(
|
||||
BookmarkAction.Select(item)
|
||||
)
|
||||
if (selected) interactor.deselect(item) else interactor.select(item)
|
||||
true
|
||||
} else false
|
||||
}
|
||||
|
@ -361,10 +352,10 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
|
|||
|
||||
class BookmarkSeparatorViewHolder(
|
||||
view: View,
|
||||
actionEmitter: Observer<BookmarkAction>,
|
||||
interactor: BookmarkViewInteractor,
|
||||
job: Job,
|
||||
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) {
|
||||
|
||||
|
@ -379,7 +370,7 @@ class BookmarkAdapter(val emptyView: View, val actionEmitter: Observer<BookmarkA
|
|||
val bookmarkItemMenu = BookmarkItemMenu(containerView!!.context, item) {
|
||||
when (it) {
|
||||
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
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.graphics.PorterDuff.Mode.SRC_IN
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.os.Bundle
|
||||
|
@ -18,10 +15,11 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
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.whenStarted
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import kotlinx.android.synthetic.main.fragment_bookmark.view.*
|
||||
|
@ -32,19 +30,17 @@ import kotlinx.coroutines.isActive
|
|||
import kotlinx.coroutines.launch
|
||||
import mozilla.appservices.places.BookmarkRoot
|
||||
import mozilla.components.concept.storage.BookmarkNode
|
||||
import mozilla.components.concept.storage.BookmarkNodeType
|
||||
import mozilla.components.concept.sync.AccountObserver
|
||||
import mozilla.components.concept.sync.OAuthAccount
|
||||
import mozilla.components.concept.sync.Profile
|
||||
import mozilla.components.lib.state.ext.observe
|
||||
import mozilla.components.support.base.feature.BackHandler
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.BrowsingModeManager
|
||||
import org.mozilla.fenix.FenixViewModelProvider
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
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.MetricController
|
||||
import org.mozilla.fenix.ext.bookmarkStorage
|
||||
import org.mozilla.fenix.ext.components
|
||||
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.urlToTrimmedHost
|
||||
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
|
||||
|
||||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||
class BookmarkFragment : Fragment(), BackHandler, AccountObserver {
|
||||
|
||||
private lateinit var bookmarkComponent: BookmarkComponent
|
||||
private lateinit var signInComponent: SignInComponent
|
||||
private lateinit var bookmarkStore: BookmarkStore
|
||||
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
|
||||
private val navigation by lazy { findNavController() }
|
||||
private val onDestinationChangedListener =
|
||||
|
@ -69,36 +67,51 @@ class BookmarkFragment : Fragment(), BackHandler, AccountObserver {
|
|||
if (destination.id != R.id.bookmarkFragment ||
|
||||
args != null && BookmarkFragmentArgs.fromBundle(args).currentRoot != currentRoot?.guid
|
||||
)
|
||||
getManagedEmitter<BookmarkChange>().onNext(BookmarkChange.ClearSelection)
|
||||
bookmarkInteractor.deselectAll()
|
||||
}
|
||||
lateinit var initialJob: Job
|
||||
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? {
|
||||
val view = inflater.inflate(R.layout.fragment_bookmark, container, false)
|
||||
bookmarkComponent = BookmarkComponent(
|
||||
view.bookmark_layout,
|
||||
ActionBusFactory.get(this),
|
||||
FenixViewModelProvider.create(
|
||||
this,
|
||||
BookmarkViewModel::class.java,
|
||||
BookmarkViewModel.Companion::create
|
||||
)
|
||||
)
|
||||
signInComponent = SignInComponent(
|
||||
view.bookmark_layout,
|
||||
ActionBusFactory.get(this),
|
||||
FenixViewModelProvider.create(
|
||||
this,
|
||||
SignInViewModel::class.java
|
||||
) {
|
||||
SignInViewModel(SignInState(false))
|
||||
}
|
||||
|
||||
bookmarkStore = StoreProvider.get(this) {
|
||||
BookmarkStore(BookmarkState(null))
|
||||
}
|
||||
bookmarkInteractor = BookmarkFragmentInteractor(
|
||||
context!!,
|
||||
findNavController(),
|
||||
bookmarkStore,
|
||||
sharedViewModel,
|
||||
FenixSnackbarPresenter(view),
|
||||
::deleteMulti
|
||||
)
|
||||
|
||||
bookmarkView = BookmarkView(view.bookmark_layout, bookmarkInteractor)
|
||||
signInView = SignInView(view.bookmark_layout, bookmarkInteractor)
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
activity?.title = getString(R.string.library_bookmarks)
|
||||
|
@ -125,11 +138,8 @@ class BookmarkFragment : Fragment(), BackHandler, AccountObserver {
|
|||
|
||||
if (!isActive) return@launch
|
||||
launch(Main) {
|
||||
getManagedEmitter<BookmarkChange>().onNext(BookmarkChange.Change(currentRoot!!))
|
||||
|
||||
activity?.run {
|
||||
ViewModelProviders.of(this).get(BookmarksSharedViewModel::class.java)
|
||||
}!!.selectedFolder = currentRoot
|
||||
bookmarkInteractor.change(currentRoot!!)
|
||||
sharedViewModel.selectedFolder = currentRoot
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -137,8 +147,8 @@ class BookmarkFragment : Fragment(), BackHandler, AccountObserver {
|
|||
private fun checkIfSignedIn() {
|
||||
context?.components?.backgroundServices?.accountManager?.let {
|
||||
it.register(this, owner = this)
|
||||
it.authenticatedAccount()?.let { getManagedEmitter<SignInChange>().onNext(SignInChange.SignedIn) }
|
||||
?: getManagedEmitter<SignInChange>().onNext(SignInChange.SignedOut)
|
||||
it.authenticatedAccount()?.let { bookmarkInteractor.signedIn() }
|
||||
?: bookmarkInteractor.signedOut()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -148,7 +158,7 @@ class BookmarkFragment : Fragment(), BackHandler, AccountObserver {
|
|||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
when (val mode = (bookmarkComponent.uiView as BookmarkUIView).mode) {
|
||||
when (val mode = bookmarkView.mode) {
|
||||
BookmarkState.Mode.Normal -> {
|
||||
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 {
|
||||
return when (item.itemId) {
|
||||
R.id.libraryClose -> {
|
||||
|
@ -346,7 +200,7 @@ class BookmarkFragment : Fragment(), BackHandler, AccountObserver {
|
|||
(activity as HomeActivity).browsingModeManager.mode = BrowsingModeManager.Mode.Normal
|
||||
(activity as HomeActivity).supportActionBar?.hide()
|
||||
nav(R.id.bookmarkFragment, BookmarkFragmentDirections.actionBookmarkFragmentToHomeFragment())
|
||||
metrics()?.track(Event.OpenedBookmarksInNewTabs)
|
||||
metrics?.track(Event.OpenedBookmarksInNewTabs)
|
||||
true
|
||||
}
|
||||
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).supportActionBar?.hide()
|
||||
nav(R.id.bookmarkFragment, BookmarkFragmentDirections.actionBookmarkFragmentToHomeFragment())
|
||||
metrics()?.track(Event.OpenedBookmarksInPrivateTabs)
|
||||
metrics?.track(Event.OpenedBookmarksInPrivateTabs)
|
||||
true
|
||||
}
|
||||
R.id.delete_bookmarks_multi_select -> {
|
||||
val selectedBookmarks = 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
|
||||
)
|
||||
deleteMulti(getSelectedBookmarks())
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed(): Boolean = (bookmarkComponent.uiView as BookmarkUIView).onBackPressed()
|
||||
override fun onBackPressed(): Boolean = bookmarkView.onBackPressed()
|
||||
|
||||
override fun onAuthenticated(account: OAuthAccount) {
|
||||
getManagedEmitter<SignInChange>().onNext(SignInChange.SignedIn)
|
||||
bookmarkInteractor.signedIn()
|
||||
lifecycleScope.launch {
|
||||
refreshBookmarks()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLoggedOut() {
|
||||
getManagedEmitter<SignInChange>().onNext(SignInChange.SignedOut)
|
||||
bookmarkInteractor.signedOut()
|
||||
}
|
||||
|
||||
override fun onAuthenticationProblems() {
|
||||
|
@ -424,7 +252,23 @@ class BookmarkFragment : Fragment(), BackHandler, AccountObserver {
|
|||
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()) {
|
||||
selected.forEach {
|
||||
|
@ -432,20 +276,48 @@ class BookmarkFragment : Fragment(), BackHandler, AccountObserver {
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshBookmarks() {
|
||||
context?.bookmarkStorage()?.getTree(currentRoot!!.guid, false).withOptionalDesktopFolders(context)
|
||||
?.let { node ->
|
||||
var rootNode = node
|
||||
pendingBookmarksToDelete.forEach {
|
||||
rootNode -= it.guid
|
||||
}
|
||||
getManagedEmitter<BookmarkChange>().onNext(BookmarkChange.Change(rootNode))
|
||||
}
|
||||
}
|
||||
private fun deleteMulti(selected: Set<BookmarkNode>, eventType: Event = Event.RemoveBookmarks) {
|
||||
pendingBookmarksToDelete.addAll(selected)
|
||||
|
||||
override fun onPause() {
|
||||
invokePendingDeletion()
|
||||
super.onPause()
|
||||
var bookmarkTree = currentRoot
|
||||
pendingBookmarksToDelete.forEach {
|
||||
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() {
|
||||
|
@ -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
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import mozilla.components.concept.storage.BookmarkNode
|
||||
|
||||
class BookmarksSharedViewModel : ViewModel() {
|
||||
var signedIn = MutableLiveData<Boolean>().apply { postValue(true) }
|
||||
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.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Observer
|
||||
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.view.*
|
||||
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.OAuthAccount
|
||||
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.R
|
||||
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.withOptionalDesktopFolders
|
||||
import org.mozilla.fenix.library.bookmarks.BookmarksSharedViewModel
|
||||
import org.mozilla.fenix.library.bookmarks.SignInAction
|
||||
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
|
||||
import org.mozilla.fenix.library.bookmarks.SignInView
|
||||
|
||||
@SuppressWarnings("TooManyFunctions")
|
||||
class SelectBookmarkFolderFragment : Fragment(), AccountObserver {
|
||||
|
@ -52,8 +45,8 @@ class SelectBookmarkFolderFragment : Fragment(), AccountObserver {
|
|||
private val sharedViewModel: BookmarksSharedViewModel by activityViewModels()
|
||||
private var folderGuid: String? = null
|
||||
private var bookmarkNode: BookmarkNode? = null
|
||||
|
||||
private lateinit var signInComponent: SignInComponent
|
||||
private lateinit var signInView: SignInView
|
||||
private lateinit var bookmarkInteractor: SelectBookmarkFolderInteractor
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -62,32 +55,22 @@ class SelectBookmarkFolderFragment : Fragment(), AccountObserver {
|
|||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_select_bookmark_folder, container, false)
|
||||
signInComponent = SignInComponent(
|
||||
view.select_bookmark_layout,
|
||||
ActionBusFactory.get(this),
|
||||
FenixViewModelProvider.create(
|
||||
this,
|
||||
SignInViewModel::class.java
|
||||
) {
|
||||
SignInViewModel(SignInState(false))
|
||||
}
|
||||
|
||||
bookmarkInteractor = SelectBookmarkFolderInteractor(
|
||||
context!!,
|
||||
findNavController(),
|
||||
sharedViewModel
|
||||
)
|
||||
signInView = SignInView(view.select_bookmark_layout, bookmarkInteractor)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
getAutoDisposeObservable<SignInAction>()
|
||||
.subscribe {
|
||||
when (it) {
|
||||
is SignInAction.ClickedSignIn -> {
|
||||
requireComponents.services.accountsAuthFeature.beginAuthentication(requireContext())
|
||||
view?.let {
|
||||
(activity as HomeActivity).openToBrowser(BrowserDirection.FromBookmarksFolderSelect)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
sharedViewModel.signedIn.observe(this@SelectBookmarkFolderFragment, Observer<Boolean> {
|
||||
signInView.update(it)
|
||||
})
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
@ -118,8 +101,8 @@ class SelectBookmarkFolderFragment : Fragment(), AccountObserver {
|
|||
private fun checkIfSignedIn() {
|
||||
val accountManager = requireComponents.backgroundServices.accountManager
|
||||
accountManager.register(this, owner = this)
|
||||
accountManager.authenticatedAccount()?.let { getManagedEmitter<SignInChange>().onNext(SignInChange.SignedIn) }
|
||||
?: getManagedEmitter<SignInChange>().onNext(SignInChange.SignedOut)
|
||||
accountManager.authenticatedAccount()?.let { bookmarkInteractor.signedIn() }
|
||||
?: bookmarkInteractor.signedOut()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
|
@ -151,11 +134,11 @@ class SelectBookmarkFolderFragment : Fragment(), AccountObserver {
|
|||
}
|
||||
|
||||
override fun onAuthenticated(account: OAuthAccount) {
|
||||
getManagedEmitter<SignInChange>().onNext(SignInChange.SignedIn)
|
||||
bookmarkInteractor.signedIn()
|
||||
}
|
||||
|
||||
override fun onLoggedOut() {
|
||||
getManagedEmitter<SignInChange>().onNext(SignInChange.SignedOut)
|
||||
bookmarkInteractor.signedOut()
|
||||
}
|
||||
|
||||
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.lifecycle.lifecycleScope
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.Preference.OnPreferenceClickListener
|
||||
import androidx.preference.PreferenceCategory
|
||||
|
@ -25,10 +26,8 @@ import kotlinx.coroutines.launch
|
|||
import mozilla.components.concept.sync.AccountObserver
|
||||
import mozilla.components.concept.sync.OAuthAccount
|
||||
import mozilla.components.concept.sync.Profile
|
||||
import mozilla.components.support.ktx.android.content.hasCamera
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.Config
|
||||
import org.mozilla.fenix.Experiments
|
||||
import org.mozilla.fenix.FenixApplication
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
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.getPreferenceKey
|
||||
import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.isInExperiment
|
||||
import org.mozilla.fenix.utils.ItsNotBrokenSnack
|
||||
|
||||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||
|
@ -225,21 +223,7 @@ class SettingsFragment : PreferenceFragmentCompat(), AccountObserver {
|
|||
|
||||
private fun getClickListenerForSignIn(): OnPreferenceClickListener {
|
||||
return OnPreferenceClickListener {
|
||||
// Do not navigate to pairing UI if camera not available or pairing is disabled
|
||||
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)
|
||||
}
|
||||
context!!.components.services.launchPairingSignIn(context!!, findNavController())
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,10 @@
|
|||
android:id="@+id/action_global_crash_reporter"
|
||||
app:destination="@id/crashReporterFragment" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_global_turn_on_sync"
|
||||
app:destination="@id/turnOnSyncFragment" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/homeFragment"
|
||||
android:name="org.mozilla.fenix.home.HomeFragment"
|
||||
|
|
|
@ -4,22 +4,15 @@
|
|||
|
||||
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.spyk
|
||||
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.BookmarkNodeType
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.TestApplication
|
||||
import org.mozilla.fenix.TestUtils.setRxSchedulers
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
|
@ -28,20 +21,16 @@ import org.robolectric.annotation.Config
|
|||
internal class BookmarkAdapterTest {
|
||||
|
||||
private lateinit var bookmarkAdapter: BookmarkAdapter
|
||||
private lateinit var emitter: Observer<BookmarkAction>
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
setRxSchedulers()
|
||||
emitter = TestObserver()
|
||||
bookmarkAdapter = spyk(
|
||||
BookmarkAdapter(mockk(), emitter), recordPrivateCalls = true
|
||||
BookmarkAdapter(mockk(relaxed = true), mockk())
|
||||
)
|
||||
every { bookmarkAdapter.notifyDataSetChanged() } just Runs
|
||||
}
|
||||
|
||||
@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(
|
||||
BookmarkNodeType.FOLDER, "123", null, 0, "Mobile", null, listOf(
|
||||
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(null, BookmarkState.Mode.Normal)
|
||||
verifyOrder {
|
||||
bookmarkAdapter.updateData(tree, BookmarkState.Mode.Normal)
|
||||
bookmarkAdapter setProperty "tree" value tree.children
|
||||
bookmarkAdapter setProperty "mode" value BookmarkState.Mode.Normal
|
||||
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 setProperty "tree" value listOf<BookmarkNode?>()
|
||||
bookmarkAdapter setProperty "mode" value BookmarkState.Mode.Normal
|
||||
bookmarkAdapter.notifyItemRangeRemoved(0, 3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 mockito = "2.24.5"
|
||||
const val mockk = "1.9.kotlin12"
|
||||
const val assertk = "0.19"
|
||||
const val flipper = "0.21.0"
|
||||
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_android = "org.mockito:mockito-android:${Versions.mockito}"
|
||||
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_noop = "com.facebook.flipper:flipper-noop:${Versions.flipper}"
|
||||
|
|
Loading…
Reference in New Issue