* For #4596: move code from CollectionCreationComponent to CollectionCreationStore Other than adding comments, no changes were made. The code will be updated in a following commit. This is in order to make the commit diff more readable. * For 4596: update CollectionCreateStore to libstate * For 4596: copied CollectionCreationUIView into CollectionCreationView Otherwise, no code was changed. The next commit will update this code. This is in order to make the commit diff more readable. * For 4596: update CollectionCreationView to LibState Note that the minimal changes possible to enable migration were made. Refactoring will happen in a later commit. * For 4596: updated CollectionCreationTabListAdapter to work with the new View * For 4596: updated SaveCollectionListAdapter to work with the new View * For 4596: implemented CollectionCreationController For now, it has an identical interface to the interactor. In a later commit several of its responsibilities will be moved around, some to the interactor and some to the reducer * For 4596: copied over previous reducer code No other changes were made. The code will be updated in the following commit. This is done to make changes more readable for the reviewer * For 4596: update reducer code param names Otherwise, no changes at this time * For 4596: add arguments to CreateCollectionFragment in nav_graph These will be used to replace the current CreateCollectionViewModel, which shares data between fragments in a way that doesn't fit within our architecture. * For 4596: pass arguments to collection via transaction instead of VM The VM will be removed in a later commit * For 4596: update BrowserToolbarController to share state to collection via its Direction * For 4596: removed CreateCollectionViewModel * For 4596: test tab retrieval in CreateCollectionFragment * For 4596: fix crashing CreateCollectionFragmentTest * For 4596: removed classes create collection classes used by old architecture * For 4596: collection interactor rename + kdoc * For 4596: moved collection interactor interface * For 4596: renamed CreateCollectionFragment All related classes followed the pattern of CollectionCreationX * For 4596: kdoc CollectionCreationController There's no effective difference between these calls and their interactor equivalent, so I linked to them * For 4596: fix bug that caused rename to not work * For 4596: removed unused collection actions These were unused before the LibState refactor * For 4596: kdoc StepChanged * For 4596: removed todos about moving logic to the reducer saveTabsToCollection: this could be moved, but that would involve creating a new action. SaveCollectionStep should probably be refactored out, so adding this layer of indirection seemed counterproductive handleBackPress: needs to be able to call dismiss(). The reducer doesn't (and shouldn't) be able to do that, so this needs to live here stepBack: called by handleBackPress. See above * For 4596: wrote tests for CollectionCreationController#stepback * For 4596: fixed tests broken by changes to collections * For 4596: small readability refactor for CollectionController#stepBack No change to functionality (see tests) * For 4596: broke apart CollectionView#update There's probably a lot more that could be done here, but smaller changes were made to reduce scope * For 4596: remove unnecessary todos It looks like we don't follow the suggested pattern in this project * For 4596: test CollectionCreationController#normalSessionSize * For 4596: updated naming in CollectionCreationController per reviewnightly-build-test
parent
40cda1d758
commit
aa8642f534
@ -1,98 +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.collections
|
|
||||||
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.Tab
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.TabCollection
|
|
||||||
import org.mozilla.fenix.mvi.Action
|
|
||||||
import org.mozilla.fenix.mvi.ActionBusFactory
|
|
||||||
import org.mozilla.fenix.mvi.Change
|
|
||||||
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.ViewState
|
|
||||||
|
|
||||||
enum class SaveCollectionStep {
|
|
||||||
SelectTabs,
|
|
||||||
SelectCollection,
|
|
||||||
NameCollection,
|
|
||||||
RenameCollection
|
|
||||||
}
|
|
||||||
|
|
||||||
data class CollectionCreationState(
|
|
||||||
val tabs: List<Tab> = emptyList(),
|
|
||||||
val selectedTabs: Set<Tab> = emptySet(),
|
|
||||||
val saveCollectionStep: SaveCollectionStep = SaveCollectionStep.SelectTabs,
|
|
||||||
val tabCollections: List<TabCollection> = emptyList(),
|
|
||||||
val selectedTabCollection: TabCollection? = null
|
|
||||||
) : ViewState
|
|
||||||
|
|
||||||
sealed class CollectionCreationChange : Change {
|
|
||||||
data class TabListChange(val tabs: List<Tab>) : CollectionCreationChange()
|
|
||||||
object AddAllTabs : CollectionCreationChange()
|
|
||||||
object RemoveAllTabs : CollectionCreationChange()
|
|
||||||
data class TabAdded(val tab: Tab) : CollectionCreationChange()
|
|
||||||
data class TabRemoved(val tab: Tab) : CollectionCreationChange()
|
|
||||||
data class StepChanged(val saveCollectionStep: SaveCollectionStep) : CollectionCreationChange()
|
|
||||||
data class CollectionSelected(val collection: TabCollection) : CollectionCreationChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class CollectionCreationAction : Action {
|
|
||||||
object Close : CollectionCreationAction()
|
|
||||||
object SelectAllTapped : CollectionCreationAction()
|
|
||||||
object DeselectAllTapped : CollectionCreationAction()
|
|
||||||
object AddNewCollection : CollectionCreationAction()
|
|
||||||
data class AddTabToSelection(val tab: Tab) : CollectionCreationAction()
|
|
||||||
data class RemoveTabFromSelection(val tab: Tab) : CollectionCreationAction()
|
|
||||||
data class SaveTabsToCollection(val tabs: List<Tab>) : CollectionCreationAction()
|
|
||||||
data class BackPressed(val backPressFrom: SaveCollectionStep) : CollectionCreationAction()
|
|
||||||
data class SaveCollectionName(val tabs: List<Tab>, val name: String) :
|
|
||||||
CollectionCreationAction()
|
|
||||||
data class RenameCollection(val collection: TabCollection, val name: String) :
|
|
||||||
CollectionCreationAction()
|
|
||||||
data class SelectCollection(val collection: TabCollection, val tabs: List<Tab>) :
|
|
||||||
CollectionCreationAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
class CollectionCreationComponent(
|
|
||||||
private val container: ViewGroup,
|
|
||||||
bus: ActionBusFactory,
|
|
||||||
viewModelProvider: UIComponentViewModelProvider<CollectionCreationState, CollectionCreationChange>
|
|
||||||
) : UIComponent<CollectionCreationState, CollectionCreationAction, CollectionCreationChange>(
|
|
||||||
bus.getManagedEmitter(CollectionCreationAction::class.java),
|
|
||||||
bus.getSafeManagedObservable(CollectionCreationChange::class.java),
|
|
||||||
viewModelProvider
|
|
||||||
) {
|
|
||||||
override fun initView() = CollectionCreationUIView(container, actionEmitter, changesObservable)
|
|
||||||
|
|
||||||
init {
|
|
||||||
bind()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CollectionCreationViewModel(
|
|
||||||
initialState: CollectionCreationState
|
|
||||||
) :
|
|
||||||
UIComponentViewModelBase<CollectionCreationState, CollectionCreationChange>(
|
|
||||||
initialState,
|
|
||||||
reducer
|
|
||||||
) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val reducer: Reducer<CollectionCreationState, CollectionCreationChange> = { state, change ->
|
|
||||||
when (change) {
|
|
||||||
is CollectionCreationChange.AddAllTabs -> state.copy(selectedTabs = state.tabs.toSet())
|
|
||||||
is CollectionCreationChange.RemoveAllTabs -> state.copy(selectedTabs = emptySet())
|
|
||||||
is CollectionCreationChange.TabListChange -> state.copy(tabs = change.tabs)
|
|
||||||
is CollectionCreationChange.TabAdded -> state.copy(selectedTabs = state.selectedTabs + change.tab)
|
|
||||||
is CollectionCreationChange.TabRemoved -> state.copy(selectedTabs = state.selectedTabs - change.tab)
|
|
||||||
is CollectionCreationChange.StepChanged -> state.copy(saveCollectionStep = change.saveCollectionStep)
|
|
||||||
is CollectionCreationChange.CollectionSelected -> state.copy(selectedTabCollection = change.collection)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,202 @@
|
|||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
@file:Suppress("TooManyFunctions")
|
||||||
|
|
||||||
|
package org.mozilla.fenix.collections
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import mozilla.components.browser.session.SessionManager
|
||||||
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
|
import mozilla.components.feature.tabs.TabsUseCases
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.components.Analytics
|
||||||
|
import org.mozilla.fenix.components.TabCollectionStorage
|
||||||
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
|
import org.mozilla.fenix.home.sessioncontrol.Tab
|
||||||
|
import org.mozilla.fenix.home.sessioncontrol.toSessionBundle
|
||||||
|
|
||||||
|
interface CollectionCreationController {
|
||||||
|
|
||||||
|
fun saveCollectionName(tabs: List<Tab>, name: String)
|
||||||
|
|
||||||
|
fun renameCollection(collection: TabCollection, name: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [CollectionCreationInteractor.onBackPressed]
|
||||||
|
*/
|
||||||
|
fun backPressed(fromStep: SaveCollectionStep)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [CollectionCreationInteractor.selectAllTapped]
|
||||||
|
*/
|
||||||
|
fun selectAllTabs()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [CollectionCreationInteractor.deselectAllTapped]
|
||||||
|
*/
|
||||||
|
fun deselectAllTabs()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [CollectionCreationInteractor.close]
|
||||||
|
*/
|
||||||
|
fun close()
|
||||||
|
|
||||||
|
fun selectCollection(collection: TabCollection, tabs: List<Tab>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See [CollectionCreationInteractor.saveTabsToCollection]
|
||||||
|
*/
|
||||||
|
fun saveTabsToCollection(tabs: List<Tab>)
|
||||||
|
|
||||||
|
fun addNewCollection()
|
||||||
|
|
||||||
|
fun addTabToSelection(tab: Tab)
|
||||||
|
|
||||||
|
fun removeTabFromSelection(tab: Tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultCollectionCreationController(
|
||||||
|
private val store: CollectionCreationStore,
|
||||||
|
private val dismiss: () -> Unit,
|
||||||
|
private val analytics: Analytics,
|
||||||
|
private val tabCollectionStorage: TabCollectionStorage,
|
||||||
|
private val tabsUseCases: TabsUseCases,
|
||||||
|
private val sessionManager: SessionManager,
|
||||||
|
private val lifecycleScope: CoroutineScope
|
||||||
|
) : CollectionCreationController {
|
||||||
|
override fun saveCollectionName(tabs: List<Tab>, name: String) {
|
||||||
|
dismiss()
|
||||||
|
|
||||||
|
val sessionBundle = tabs.toList().toSessionBundle(sessionManager)
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
tabCollectionStorage.createCollection(name, sessionBundle)
|
||||||
|
}
|
||||||
|
|
||||||
|
analytics.metrics.track(
|
||||||
|
Event.CollectionSaved(normalSessionSize(sessionManager), sessionBundle.size)
|
||||||
|
)
|
||||||
|
|
||||||
|
closeTabsIfNecessary(tabs, sessionManager, tabsUseCases)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun renameCollection(collection: TabCollection, name: String) {
|
||||||
|
dismiss()
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
tabCollectionStorage.renameCollection(collection, name)
|
||||||
|
analytics.metrics.track(Event.CollectionRenamed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun backPressed(fromStep: SaveCollectionStep) {
|
||||||
|
handleBackPress(fromStep)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun selectAllTabs() {
|
||||||
|
store.dispatch(CollectionCreationAction.AddAllTabs)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deselectAllTabs() {
|
||||||
|
store.dispatch(CollectionCreationAction.RemoveAllTabs)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun selectCollection(collection: TabCollection, tabs: List<Tab>) {
|
||||||
|
dismiss()
|
||||||
|
val sessionBundle = tabs.toList().toSessionBundle(sessionManager)
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
tabCollectionStorage
|
||||||
|
.addTabsToCollection(collection, sessionBundle)
|
||||||
|
}
|
||||||
|
|
||||||
|
analytics.metrics.track(
|
||||||
|
Event.CollectionTabsAdded(normalSessionSize(sessionManager), sessionBundle.size)
|
||||||
|
)
|
||||||
|
|
||||||
|
closeTabsIfNecessary(tabs, sessionManager, tabsUseCases)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveTabsToCollection(tabs: List<Tab>) {
|
||||||
|
store.dispatch(CollectionCreationAction.StepChanged(
|
||||||
|
saveCollectionStep = if (store.state.tabCollections.isEmpty()) {
|
||||||
|
SaveCollectionStep.NameCollection
|
||||||
|
} else {
|
||||||
|
SaveCollectionStep.SelectCollection
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addNewCollection() {
|
||||||
|
store.dispatch(CollectionCreationAction.StepChanged(SaveCollectionStep.NameCollection))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addTabToSelection(tab: Tab) {
|
||||||
|
store.dispatch(CollectionCreationAction.TabAdded(tab))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeTabFromSelection(tab: Tab) {
|
||||||
|
store.dispatch(CollectionCreationAction.TabRemoved(tab))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleBackPress(backFromStep: SaveCollectionStep) {
|
||||||
|
val newStep = stepBack(backFromStep)
|
||||||
|
if (newStep != null) {
|
||||||
|
store.dispatch(CollectionCreationAction.StepChanged(newStep))
|
||||||
|
} else {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
fun stepBack(
|
||||||
|
backFromStep: SaveCollectionStep
|
||||||
|
): SaveCollectionStep? {
|
||||||
|
/*
|
||||||
|
Will return the next valid state according to this diagram.
|
||||||
|
|
||||||
|
Name Collection -> Select Collection -> Select Tabs -> (dismiss fragment) <- Rename Collection
|
||||||
|
*/
|
||||||
|
|
||||||
|
val tabCollectionCount = store.state.tabCollections.size
|
||||||
|
val tabCount = store.state.tabs.size
|
||||||
|
|
||||||
|
return when (backFromStep) {
|
||||||
|
SaveCollectionStep.NameCollection -> if (tabCollectionCount > 0) {
|
||||||
|
SaveCollectionStep.SelectCollection
|
||||||
|
} else {
|
||||||
|
stepBack(SaveCollectionStep.SelectCollection)
|
||||||
|
}
|
||||||
|
SaveCollectionStep.SelectCollection -> if (tabCount > 1) {
|
||||||
|
SaveCollectionStep.SelectTabs
|
||||||
|
} else {
|
||||||
|
stepBack(SaveCollectionStep.SelectTabs)
|
||||||
|
}
|
||||||
|
SaveCollectionStep.SelectTabs, SaveCollectionStep.RenameCollection -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the number of currently active sessions that are neither custom nor private
|
||||||
|
*/
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
fun normalSessionSize(sessionManager: SessionManager): Int {
|
||||||
|
return sessionManager.sessions.filter { session ->
|
||||||
|
(!session.isCustomTabSession() && !session.private)
|
||||||
|
}.size
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun closeTabsIfNecessary(tabs: List<Tab>, sessionManager: SessionManager, tabsUseCases: TabsUseCases) {
|
||||||
|
// Only close the tabs if the user is not on the BrowserFragment
|
||||||
|
if (store.state.previousFragmentId == R.id.browserFragment) { return }
|
||||||
|
tabs.asSequence()
|
||||||
|
.mapNotNull { tab -> sessionManager.findSessionById(tab.sessionId) }
|
||||||
|
.forEach { session -> tabsUseCases.removeTab(session) }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,112 @@
|
|||||||
|
/* 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.collections
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.navigation.fragment.navArgs
|
||||||
|
import kotlinx.android.synthetic.main.fragment_create_collection.view.*
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import mozilla.components.browser.session.SessionManager
|
||||||
|
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
||||||
|
import mozilla.components.lib.state.ext.consumeFrom
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.components.StoreProvider
|
||||||
|
import org.mozilla.fenix.ext.requireComponents
|
||||||
|
import org.mozilla.fenix.ext.toTab
|
||||||
|
import org.mozilla.fenix.home.sessioncontrol.Tab
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
class CollectionCreationFragment : DialogFragment() {
|
||||||
|
private lateinit var collectionCreationView: CollectionCreationView
|
||||||
|
private lateinit var collectionCreationStore: CollectionCreationStore
|
||||||
|
private lateinit var collectionCreationInteractor: CollectionCreationInteractor
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
isCancelable = false
|
||||||
|
setStyle(STYLE_NO_TITLE, R.style.CreateCollectionDialogStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
val view = inflater.inflate(R.layout.fragment_create_collection, container, false)
|
||||||
|
val args: CollectionCreationFragmentArgs by navArgs()
|
||||||
|
|
||||||
|
val sessionManager = requireComponents.core.sessionManager
|
||||||
|
val publicSuffixList = requireComponents.publicSuffixList
|
||||||
|
val tabs = sessionManager.getTabs(args.tabIds, publicSuffixList)
|
||||||
|
val selectedTabs = sessionManager.getTabs(args.selectedTabIds, publicSuffixList)
|
||||||
|
.toSet()
|
||||||
|
val tabCollections = requireComponents.core.tabCollectionStorage.cachedTabCollections
|
||||||
|
val selectedTabCollection = args.selectedTabCollectionId
|
||||||
|
.let { id -> tabCollections.firstOrNull { it.id == id } }
|
||||||
|
|
||||||
|
collectionCreationStore = StoreProvider.get(this) {
|
||||||
|
CollectionCreationStore(
|
||||||
|
CollectionCreationState(
|
||||||
|
previousFragmentId = args.previousFragmentId,
|
||||||
|
tabs = tabs,
|
||||||
|
selectedTabs = selectedTabs,
|
||||||
|
saveCollectionStep = args.saveCollectionStep,
|
||||||
|
tabCollections = tabCollections,
|
||||||
|
selectedTabCollection = selectedTabCollection
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
collectionCreationInteractor = DefaultCollectionCreationInteractor(
|
||||||
|
DefaultCollectionCreationController(
|
||||||
|
collectionCreationStore,
|
||||||
|
::dismiss,
|
||||||
|
requireComponents.analytics,
|
||||||
|
requireComponents.core.tabCollectionStorage,
|
||||||
|
requireComponents.useCases.tabsUseCases,
|
||||||
|
requireComponents.core.sessionManager,
|
||||||
|
viewLifecycleOwner.lifecycleScope
|
||||||
|
)
|
||||||
|
)
|
||||||
|
collectionCreationView = CollectionCreationView(view.createCollectionWrapper, collectionCreationInteractor)
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
consumeFrom(collectionCreationStore) { newState ->
|
||||||
|
collectionCreationView.update(newState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
collectionCreationView.onResumed()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val dialog = super.onCreateDialog(savedInstanceState)
|
||||||
|
dialog.setOnKeyListener { _, keyCode, event ->
|
||||||
|
collectionCreationView.onKey(keyCode, event)
|
||||||
|
}
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
|
fun SessionManager.getTabs(tabIds: Array<String>?, publicSuffixList: PublicSuffixList): List<Tab> {
|
||||||
|
return tabIds
|
||||||
|
?.mapNotNull { this.findSessionById(it) }
|
||||||
|
?.map { it.toTab(publicSuffixList) }
|
||||||
|
?: emptyList()
|
||||||
|
}
|
@ -0,0 +1,107 @@
|
|||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
@file:Suppress("TooManyFunctions")
|
||||||
|
|
||||||
|
package org.mozilla.fenix.collections
|
||||||
|
|
||||||
|
import org.mozilla.fenix.home.sessioncontrol.Tab
|
||||||
|
import org.mozilla.fenix.home.sessioncontrol.TabCollection
|
||||||
|
|
||||||
|
interface CollectionCreationInteractor {
|
||||||
|
|
||||||
|
fun onNewCollectionNameSaved(tabs: List<Tab>, name: String)
|
||||||
|
|
||||||
|
fun onCollectionRenamed(collection: TabCollection, name: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when either the physical back button, or the back arrow are clicked.
|
||||||
|
*
|
||||||
|
* Note that this is not called when the close button on the snackbar is clicked. See [close].
|
||||||
|
*/
|
||||||
|
fun onBackPressed(fromStep: SaveCollectionStep)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a user hits 'Select All' from the 'Select Tabs' step. This affects which tabs
|
||||||
|
* have been 'selected' to be saved into a collection.
|
||||||
|
*/
|
||||||
|
fun selectAllTapped()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a user hits 'Deselect All' from the 'Select Tabs' step. This affects which tabs
|
||||||
|
* have been 'selected' to be saved into a collection.
|
||||||
|
*/
|
||||||
|
fun deselectAllTapped()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a user hits the close button on the snackbar.
|
||||||
|
*
|
||||||
|
* Note that this is not called when the back arrow is clicked. See [onBackPressed].
|
||||||
|
*/
|
||||||
|
fun close()
|
||||||
|
|
||||||
|
fun selectCollection(collection: TabCollection, tabs: List<Tab>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the user decides to save tabs to the currently selected session.
|
||||||
|
*/
|
||||||
|
fun saveTabsToCollection(tabs: List<Tab>)
|
||||||
|
|
||||||
|
fun addNewCollection()
|
||||||
|
|
||||||
|
fun addTabToSelection(tab: Tab)
|
||||||
|
|
||||||
|
fun removeTabFromSelection(tab: Tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forwards all method calls to their equivalents in [CollectionCreationController].
|
||||||
|
*/
|
||||||
|
class DefaultCollectionCreationInteractor(
|
||||||
|
private val controller: CollectionCreationController
|
||||||
|
) : CollectionCreationInteractor {
|
||||||
|
override fun onNewCollectionNameSaved(tabs: List<Tab>, name: String) {
|
||||||
|
controller.saveCollectionName(tabs, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCollectionRenamed(collection: TabCollection, name: String) {
|
||||||
|
controller.renameCollection(collection, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed(fromStep: SaveCollectionStep) {
|
||||||
|
controller.backPressed(fromStep)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun selectAllTapped() {
|
||||||
|
controller.selectAllTabs()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deselectAllTapped() {
|
||||||
|
controller.deselectAllTabs()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
controller.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun selectCollection(collection: TabCollection, tabs: List<Tab>) {
|
||||||
|
controller.selectCollection(collection, tabs)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveTabsToCollection(tabs: List<Tab>) {
|
||||||
|
controller.saveTabsToCollection(tabs)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addNewCollection() {
|
||||||
|
controller.addNewCollection()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addTabToSelection(tab: Tab) {
|
||||||
|
controller.addTabToSelection(tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeTabFromSelection(tab: Tab) {
|
||||||
|
controller.removeTabFromSelection(tab)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
/* 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.collections
|
||||||
|
|
||||||
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
|
import mozilla.components.lib.state.Action
|
||||||
|
import mozilla.components.lib.state.State
|
||||||
|
import mozilla.components.lib.state.Store
|
||||||
|
import org.mozilla.fenix.collections.CollectionCreationAction.StepChanged
|
||||||
|
import org.mozilla.fenix.home.sessioncontrol.Tab
|
||||||
|
|
||||||
|
class CollectionCreationStore(
|
||||||
|
initialState: CollectionCreationState
|
||||||
|
) : Store<CollectionCreationState, CollectionCreationAction>(
|
||||||
|
initialState,
|
||||||
|
::collectionCreationReducer
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the current purpose of the screen. This determines what options are shown to the
|
||||||
|
* user.
|
||||||
|
*
|
||||||
|
* TODO refactor [CollectionCreationState] into a sealed class with four implementations, each
|
||||||
|
* replacing a [SaveCollectionStep] value. These will not need null / emptyCollection default
|
||||||
|
* values. Handle changes bebtween these state changes internally, here and in the controller,
|
||||||
|
* instead of exposing [StepChanged], which currently acts as a setter.
|
||||||
|
*/
|
||||||
|
enum class SaveCollectionStep {
|
||||||
|
SelectTabs,
|
||||||
|
SelectCollection,
|
||||||
|
NameCollection,
|
||||||
|
RenameCollection
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CollectionCreationState(
|
||||||
|
val previousFragmentId: Int,
|
||||||
|
val tabs: List<Tab> = emptyList(),
|
||||||
|
val selectedTabs: Set<Tab> = emptySet(),
|
||||||
|
val saveCollectionStep: SaveCollectionStep = SaveCollectionStep.SelectTabs,
|
||||||
|
val tabCollections: List<TabCollection> = emptyList(),
|
||||||
|
val selectedTabCollection: TabCollection? = null
|
||||||
|
) : State
|
||||||
|
|
||||||
|
sealed class CollectionCreationAction : Action {
|
||||||
|
object AddAllTabs : CollectionCreationAction()
|
||||||
|
object RemoveAllTabs : CollectionCreationAction()
|
||||||
|
data class TabAdded(val tab: Tab) : CollectionCreationAction()
|
||||||
|
data class TabRemoved(val tab: Tab) : CollectionCreationAction()
|
||||||
|
/**
|
||||||
|
* Used as a setter for [SaveCollectionStep].
|
||||||
|
*
|
||||||
|
* This should be refactored, see kdoc on [SaveCollectionStep].
|
||||||
|
*/
|
||||||
|
data class StepChanged(val saveCollectionStep: SaveCollectionStep) : CollectionCreationAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun collectionCreationReducer(
|
||||||
|
prevState: CollectionCreationState,
|
||||||
|
action: CollectionCreationAction
|
||||||
|
): CollectionCreationState = when (action) {
|
||||||
|
is CollectionCreationAction.AddAllTabs -> prevState.copy(selectedTabs = prevState.tabs.toSet())
|
||||||
|
is CollectionCreationAction.RemoveAllTabs -> prevState.copy(selectedTabs = emptySet())
|
||||||
|
is CollectionCreationAction.TabAdded -> prevState.copy(selectedTabs = prevState.selectedTabs + action.tab)
|
||||||
|
is CollectionCreationAction.TabRemoved -> prevState.copy(selectedTabs = prevState.selectedTabs - action.tab)
|
||||||
|
is CollectionCreationAction.StepChanged -> prevState.copy(saveCollectionStep = action.saveCollectionStep)
|
||||||
|
}
|
@ -1,332 +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.collections
|
|
||||||
|
|
||||||
import android.os.Handler
|
|
||||||
import android.text.InputFilter
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.inputmethod.EditorInfo
|
|
||||||
import androidx.constraintlayout.widget.ConstraintSet
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.transition.AutoTransition
|
|
||||||
import androidx.transition.Transition
|
|
||||||
import androidx.transition.TransitionManager
|
|
||||||
import io.reactivex.Observable
|
|
||||||
import io.reactivex.Observer
|
|
||||||
import io.reactivex.functions.Consumer
|
|
||||||
import kotlinx.android.synthetic.main.component_collection_creation.*
|
|
||||||
import kotlinx.android.synthetic.main.component_collection_creation.view.*
|
|
||||||
import mozilla.components.support.ktx.android.view.hideKeyboard
|
|
||||||
import mozilla.components.support.ktx.android.view.showKeyboard
|
|
||||||
import org.mozilla.fenix.R
|
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
|
||||||
import org.mozilla.fenix.ext.components
|
|
||||||
import org.mozilla.fenix.ext.increaseTapArea
|
|
||||||
import org.mozilla.fenix.ext.urlToTrimmedHost
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.Tab
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.TabCollection
|
|
||||||
import org.mozilla.fenix.mvi.UIView
|
|
||||||
|
|
||||||
@SuppressWarnings("LargeClass")
|
|
||||||
class CollectionCreationUIView(
|
|
||||||
container: ViewGroup,
|
|
||||||
actionEmitter: Observer<CollectionCreationAction>,
|
|
||||||
changesObservable: Observable<CollectionCreationChange>
|
|
||||||
) : UIView<CollectionCreationState, CollectionCreationAction, CollectionCreationChange>(
|
|
||||||
container,
|
|
||||||
actionEmitter,
|
|
||||||
changesObservable
|
|
||||||
) {
|
|
||||||
override val view = LayoutInflater.from(container.context)
|
|
||||||
.inflate(R.layout.component_collection_creation, container, true)
|
|
||||||
|
|
||||||
var step: SaveCollectionStep = SaveCollectionStep.SelectTabs
|
|
||||||
private set
|
|
||||||
|
|
||||||
private val collectionCreationTabListAdapter = CollectionCreationTabListAdapter(actionEmitter)
|
|
||||||
private val collectionSaveListAdapter = SaveCollectionListAdapter(actionEmitter)
|
|
||||||
private var selectedCollection: TabCollection? = null
|
|
||||||
private var selectedTabs: Set<Tab> = setOf()
|
|
||||||
private val selectTabsConstraints = ConstraintSet()
|
|
||||||
private val selectCollectionConstraints = ConstraintSet()
|
|
||||||
private val nameCollectionConstraints = ConstraintSet()
|
|
||||||
private val transition = AutoTransition()
|
|
||||||
|
|
||||||
init {
|
|
||||||
transition.duration = TRANSITION_DURATION
|
|
||||||
|
|
||||||
selectTabsConstraints.clone(collection_constraint_layout)
|
|
||||||
selectCollectionConstraints.clone(
|
|
||||||
view.context,
|
|
||||||
R.layout.component_collection_creation_select_collection
|
|
||||||
)
|
|
||||||
nameCollectionConstraints.clone(
|
|
||||||
view.context,
|
|
||||||
R.layout.component_collection_creation_name_collection
|
|
||||||
)
|
|
||||||
|
|
||||||
view.bottom_bar_icon_button.apply {
|
|
||||||
increaseTapArea(increaseButtonByDps)
|
|
||||||
}
|
|
||||||
|
|
||||||
view.name_collection_edittext.filters += InputFilter.LengthFilter(COLLECTION_NAME_MAX_LENGTH)
|
|
||||||
view.name_collection_edittext.setOnEditorActionListener { view, actionId, _ ->
|
|
||||||
val text = view.text.toString()
|
|
||||||
if (actionId == EditorInfo.IME_ACTION_DONE && text.isNotBlank()) {
|
|
||||||
when (step) {
|
|
||||||
SaveCollectionStep.NameCollection ->
|
|
||||||
CollectionCreationAction.SaveCollectionName(selectedTabs.toList(), text)
|
|
||||||
SaveCollectionStep.RenameCollection ->
|
|
||||||
selectedCollection?.let { CollectionCreationAction.RenameCollection(it, text) }
|
|
||||||
else -> null
|
|
||||||
}?.let { action ->
|
|
||||||
actionEmitter.onNext(action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
view.tab_list.run {
|
|
||||||
adapter = collectionCreationTabListAdapter
|
|
||||||
itemAnimator = null
|
|
||||||
layoutManager = LinearLayoutManager(container.context, RecyclerView.VERTICAL, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
view.collections_list.run {
|
|
||||||
adapter = collectionSaveListAdapter
|
|
||||||
layoutManager = LinearLayoutManager(container.context, RecyclerView.VERTICAL, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("ComplexMethod", "LongMethod")
|
|
||||||
override fun updateView() = Consumer<CollectionCreationState> {
|
|
||||||
step = it.saveCollectionStep
|
|
||||||
selectedTabs = it.selectedTabs
|
|
||||||
selectedCollection = it.selectedTabCollection
|
|
||||||
|
|
||||||
when (it.saveCollectionStep) {
|
|
||||||
SaveCollectionStep.SelectTabs -> {
|
|
||||||
view.context.components.analytics.metrics.track(Event.CollectionTabSelectOpened)
|
|
||||||
|
|
||||||
view.tab_list.isClickable = true
|
|
||||||
|
|
||||||
back_button.setOnClickListener {
|
|
||||||
actionEmitter.onNext(CollectionCreationAction.BackPressed(SaveCollectionStep.SelectTabs))
|
|
||||||
}
|
|
||||||
val allSelected = it.selectedTabs.size == it.tabs.size
|
|
||||||
select_all_button.text =
|
|
||||||
if (allSelected)
|
|
||||||
view.context.getString(R.string.create_collection_deselect_all) else
|
|
||||||
view.context.getString(R.string.create_collection_select_all)
|
|
||||||
|
|
||||||
view.select_all_button.setOnClickListener {
|
|
||||||
if (allSelected) {
|
|
||||||
actionEmitter.onNext(CollectionCreationAction.DeselectAllTapped)
|
|
||||||
} else {
|
|
||||||
actionEmitter.onNext(CollectionCreationAction.SelectAllTapped)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
view.bottom_button_bar_layout.setOnClickListener(null)
|
|
||||||
view.bottom_button_bar_layout.isClickable = false
|
|
||||||
|
|
||||||
val drawable = view.context.getDrawable(R.drawable.ic_close)
|
|
||||||
drawable?.setTint(ContextCompat.getColor(view.context, R.color.photonWhite))
|
|
||||||
view.bottom_bar_icon_button.setImageDrawable(drawable)
|
|
||||||
view.bottom_bar_icon_button.contentDescription =
|
|
||||||
view.context.getString(R.string.create_collection_close)
|
|
||||||
view.bottom_bar_icon_button.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
|
|
||||||
view.bottom_bar_icon_button.setOnClickListener {
|
|
||||||
actionEmitter.onNext(CollectionCreationAction.Close)
|
|
||||||
}
|
|
||||||
|
|
||||||
TransitionManager.beginDelayedTransition(
|
|
||||||
view.collection_constraint_layout,
|
|
||||||
transition
|
|
||||||
)
|
|
||||||
val constraint = selectTabsConstraints
|
|
||||||
constraint.applyTo(view.collection_constraint_layout)
|
|
||||||
|
|
||||||
collectionCreationTabListAdapter.updateData(it.tabs, it.selectedTabs)
|
|
||||||
|
|
||||||
back_button.text = view.context.getString(R.string.create_collection_select_tabs)
|
|
||||||
|
|
||||||
val selectTabsText = if (it.selectedTabs.isEmpty()) {
|
|
||||||
view.context.getString(R.string.create_collection_save_to_collection_empty)
|
|
||||||
} else {
|
|
||||||
view.context.getString(
|
|
||||||
if (it.selectedTabs.size == 1)
|
|
||||||
R.string.create_collection_save_to_collection_tab_selected else
|
|
||||||
R.string.create_collection_save_to_collection_tabs_selected,
|
|
||||||
it.selectedTabs.size
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
view.bottom_bar_text.text = selectTabsText
|
|
||||||
|
|
||||||
save_button.setOnClickListener { _ ->
|
|
||||||
if (selectedCollection != null) {
|
|
||||||
actionEmitter.onNext(
|
|
||||||
CollectionCreationAction.SelectCollection(
|
|
||||||
selectedCollection!!,
|
|
||||||
it.selectedTabs.toList()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
actionEmitter.onNext(CollectionCreationAction.SaveTabsToCollection(selectedTabs.toList()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
save_button.visibility = if (it.selectedTabs.isEmpty()) {
|
|
||||||
View.GONE
|
|
||||||
} else {
|
|
||||||
View.VISIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SaveCollectionStep.SelectCollection -> {
|
|
||||||
view.tab_list.isClickable = false
|
|
||||||
|
|
||||||
save_button.visibility = View.GONE
|
|
||||||
|
|
||||||
view.bottom_bar_text.text =
|
|
||||||
view.context.getString(R.string.create_collection_add_new_collection)
|
|
||||||
|
|
||||||
val drawable = view.context.getDrawable(R.drawable.ic_new)
|
|
||||||
drawable?.setTint(ContextCompat.getColor(view.context, R.color.photonWhite))
|
|
||||||
view.bottom_bar_icon_button.setImageDrawable(drawable)
|
|
||||||
view.bottom_bar_icon_button.contentDescription = null
|
|
||||||
view.bottom_bar_icon_button.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
|
||||||
view.bottom_button_bar_layout.isClickable = true
|
|
||||||
view.bottom_button_bar_layout.setOnClickListener {
|
|
||||||
actionEmitter.onNext(CollectionCreationAction.AddNewCollection)
|
|
||||||
}
|
|
||||||
|
|
||||||
back_button.setOnClickListener {
|
|
||||||
actionEmitter.onNext(CollectionCreationAction.BackPressed(SaveCollectionStep.SelectCollection))
|
|
||||||
}
|
|
||||||
TransitionManager.beginDelayedTransition(
|
|
||||||
view.collection_constraint_layout,
|
|
||||||
transition
|
|
||||||
)
|
|
||||||
val constraint = selectCollectionConstraints
|
|
||||||
constraint.applyTo(view.collection_constraint_layout)
|
|
||||||
back_button.text =
|
|
||||||
view.context.getString(R.string.create_collection_select_collection)
|
|
||||||
}
|
|
||||||
SaveCollectionStep.NameCollection -> {
|
|
||||||
view.tab_list.isClickable = false
|
|
||||||
|
|
||||||
collectionCreationTabListAdapter.updateData(it.selectedTabs.toList(), it.selectedTabs, true)
|
|
||||||
back_button.setOnClickListener {
|
|
||||||
name_collection_edittext.hideKeyboard()
|
|
||||||
val handler = Handler()
|
|
||||||
handler.postDelayed({
|
|
||||||
actionEmitter.onNext(CollectionCreationAction.BackPressed(SaveCollectionStep.NameCollection))
|
|
||||||
}, TRANSITION_DURATION)
|
|
||||||
}
|
|
||||||
transition.addListener(object : Transition.TransitionListener {
|
|
||||||
override fun onTransitionStart(transition: Transition) { /* noop */ }
|
|
||||||
|
|
||||||
override fun onTransitionEnd(transition: Transition) {
|
|
||||||
view.name_collection_edittext.showKeyboard()
|
|
||||||
transition.removeListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTransitionCancel(transition: Transition) { /* noop */ }
|
|
||||||
override fun onTransitionPause(transition: Transition) { /* noop */ }
|
|
||||||
override fun onTransitionResume(transition: Transition) { /* noop */ }
|
|
||||||
})
|
|
||||||
TransitionManager.beginDelayedTransition(
|
|
||||||
view.collection_constraint_layout,
|
|
||||||
transition
|
|
||||||
)
|
|
||||||
val constraint = nameCollectionConstraints
|
|
||||||
constraint.applyTo(view.collection_constraint_layout)
|
|
||||||
name_collection_edittext.setText(
|
|
||||||
view.context.getString(
|
|
||||||
R.string.create_collection_default_name,
|
|
||||||
it.tabCollections.size + 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
name_collection_edittext.setSelection(0, name_collection_edittext.text.length)
|
|
||||||
back_button.text =
|
|
||||||
view.context.getString(R.string.create_collection_name_collection)
|
|
||||||
}
|
|
||||||
SaveCollectionStep.RenameCollection -> {
|
|
||||||
view.tab_list.isClickable = false
|
|
||||||
|
|
||||||
it.selectedTabCollection?.let { tabCollection ->
|
|
||||||
tabCollection.tabs.map { tab ->
|
|
||||||
Tab(
|
|
||||||
tab.id.toString(),
|
|
||||||
tab.url,
|
|
||||||
tab.url.urlToTrimmedHost(view.context),
|
|
||||||
tab.title
|
|
||||||
)
|
|
||||||
}.let { tabs ->
|
|
||||||
collectionCreationTabListAdapter.updateData(tabs, tabs.toSet(), true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val constraint = nameCollectionConstraints
|
|
||||||
constraint.applyTo(view.collection_constraint_layout)
|
|
||||||
name_collection_edittext.setText(it.selectedTabCollection?.title)
|
|
||||||
name_collection_edittext.setSelection(0, name_collection_edittext.text.length)
|
|
||||||
|
|
||||||
back_button.text =
|
|
||||||
view.context.getString(R.string.collection_rename)
|
|
||||||
back_button.setOnClickListener {
|
|
||||||
name_collection_edittext.hideKeyboard()
|
|
||||||
val handler = Handler()
|
|
||||||
handler.postDelayed({
|
|
||||||
actionEmitter.onNext(CollectionCreationAction.BackPressed(SaveCollectionStep.RenameCollection))
|
|
||||||
}, TRANSITION_DURATION)
|
|
||||||
}
|
|
||||||
transition.addListener(object : Transition.TransitionListener {
|
|
||||||
override fun onTransitionStart(transition: Transition) { /* noop */ }
|
|
||||||
|
|
||||||
override fun onTransitionEnd(transition: Transition) {
|
|
||||||
view.name_collection_edittext.showKeyboard()
|
|
||||||
transition.removeListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTransitionCancel(transition: Transition) { /* noop */ }
|
|
||||||
override fun onTransitionPause(transition: Transition) { /* noop */ }
|
|
||||||
override fun onTransitionResume(transition: Transition) { /* noop */ }
|
|
||||||
})
|
|
||||||
TransitionManager.beginDelayedTransition(
|
|
||||||
view.collection_constraint_layout,
|
|
||||||
transition
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
collectionSaveListAdapter.updateData(it.tabCollections, it.selectedTabs)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onResumed() {
|
|
||||||
if (step == SaveCollectionStep.NameCollection || step == SaveCollectionStep.RenameCollection) {
|
|
||||||
view.name_collection_edittext.showKeyboard()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onKey(keyCode: Int, event: KeyEvent?): Boolean {
|
|
||||||
return if (event?.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
|
||||||
actionEmitter.onNext(CollectionCreationAction.BackPressed(step))
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TRANSITION_DURATION = 200L
|
|
||||||
private const val increaseButtonByDps = 16
|
|
||||||
private const val COLLECTION_NAME_MAX_LENGTH = 128
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,330 @@
|
|||||||
|
/* 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.collections
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import android.text.InputFilter
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import androidx.constraintlayout.widget.ConstraintSet
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.transition.AutoTransition
|
||||||
|
import androidx.transition.Transition
|
||||||
|
import androidx.transition.TransitionManager
|
||||||
|
import kotlinx.android.extensions.LayoutContainer
|
||||||
|
import kotlinx.android.synthetic.main.component_collection_creation.*
|
||||||
|
import kotlinx.android.synthetic.main.component_collection_creation.view.*
|
||||||
|
import mozilla.components.support.ktx.android.view.hideKeyboard
|
||||||
|
import mozilla.components.support.ktx.android.view.showKeyboard
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.ext.increaseTapArea
|
||||||
|
import org.mozilla.fenix.ext.urlToTrimmedHost
|
||||||
|
import org.mozilla.fenix.home.sessioncontrol.Tab
|
||||||
|
import org.mozilla.fenix.home.sessioncontrol.TabCollection
|
||||||
|
|
||||||
|
@SuppressWarnings("LargeClass")
|
||||||
|
class CollectionCreationView(
|
||||||
|
override val containerView: ViewGroup,
|
||||||
|
private val interactor: CollectionCreationInteractor
|
||||||
|
) : LayoutContainer {
|
||||||
|
val view: View = LayoutInflater.from(containerView.context)
|
||||||
|
.inflate(R.layout.component_collection_creation, containerView, true)
|
||||||
|
|
||||||
|
private val collectionCreationTabListAdapter = CollectionCreationTabListAdapter(interactor)
|
||||||
|
private val collectionSaveListAdapter = SaveCollectionListAdapter(interactor)
|
||||||
|
private val selectTabsConstraints = ConstraintSet()
|
||||||
|
private val selectCollectionConstraints = ConstraintSet()
|
||||||
|
private val nameCollectionConstraints = ConstraintSet()
|
||||||
|
private val transition = AutoTransition()
|
||||||
|
|
||||||
|
private var selectedCollection: TabCollection? = null
|
||||||
|
private var selectedTabs: Set<Tab> = setOf()
|
||||||
|
var step: SaveCollectionStep = SaveCollectionStep.SelectTabs
|
||||||
|
private set
|
||||||
|
|
||||||
|
init {
|
||||||
|
transition.duration = TRANSITION_DURATION
|
||||||
|
|
||||||
|
selectTabsConstraints.clone(collection_constraint_layout)
|
||||||
|
selectCollectionConstraints.clone(
|
||||||
|
view.context,
|
||||||
|
R.layout.component_collection_creation_select_collection
|
||||||
|
)
|
||||||
|
nameCollectionConstraints.clone(
|
||||||
|
view.context,
|
||||||
|
R.layout.component_collection_creation_name_collection
|
||||||
|
)
|
||||||
|
|
||||||
|
view.bottom_bar_icon_button.apply {
|
||||||
|
increaseTapArea(increaseButtonByDps)
|
||||||
|
}
|
||||||
|
|
||||||
|
view.name_collection_edittext.filters += InputFilter.LengthFilter(COLLECTION_NAME_MAX_LENGTH)
|
||||||
|
view.name_collection_edittext.setOnEditorActionListener { view, actionId, _ ->
|
||||||
|
val text = view.text.toString()
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_DONE && text.isNotBlank()) {
|
||||||
|
when (step) {
|
||||||
|
SaveCollectionStep.NameCollection ->
|
||||||
|
interactor.onNewCollectionNameSaved(selectedTabs.toList(), text)
|
||||||
|
SaveCollectionStep.RenameCollection ->
|
||||||
|
selectedCollection?.let { interactor.onCollectionRenamed(it, text) }
|
||||||
|
else -> { /* noop */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
view.tab_list.run {
|
||||||
|
adapter = collectionCreationTabListAdapter
|
||||||
|
itemAnimator = null
|
||||||
|
layoutManager = LinearLayoutManager(containerView.context, RecyclerView.VERTICAL, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
view.collections_list.run {
|
||||||
|
adapter = collectionSaveListAdapter
|
||||||
|
layoutManager = LinearLayoutManager(containerView.context, RecyclerView.VERTICAL, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(state: CollectionCreationState) {
|
||||||
|
|
||||||
|
cacheState(state)
|
||||||
|
|
||||||
|
when (step) {
|
||||||
|
SaveCollectionStep.SelectTabs -> updateForSelectTabs(state)
|
||||||
|
SaveCollectionStep.SelectCollection -> updateForSelectCollection()
|
||||||
|
SaveCollectionStep.NameCollection -> updateForNameCollection(state)
|
||||||
|
SaveCollectionStep.RenameCollection -> updateForRenameCollection(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
collectionSaveListAdapter.updateData(state.tabCollections, state.selectedTabs)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cacheState(state: CollectionCreationState) {
|
||||||
|
step = state.saveCollectionStep
|
||||||
|
selectedTabs = state.selectedTabs
|
||||||
|
selectedCollection = state.selectedTabCollection
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("ComplexMethod")
|
||||||
|
private fun updateForSelectTabs(state: CollectionCreationState) {
|
||||||
|
view.context.components.analytics.metrics.track(Event.CollectionTabSelectOpened)
|
||||||
|
|
||||||
|
view.tab_list.isClickable = true
|
||||||
|
|
||||||
|
back_button.setOnClickListener {
|
||||||
|
interactor.onBackPressed(SaveCollectionStep.SelectTabs)
|
||||||
|
}
|
||||||
|
val allSelected = state.selectedTabs.size == state.tabs.size
|
||||||
|
select_all_button.text =
|
||||||
|
if (allSelected) view.context.getString(R.string.create_collection_deselect_all)
|
||||||
|
else view.context.getString(R.string.create_collection_select_all)
|
||||||
|
|
||||||
|
view.select_all_button.setOnClickListener {
|
||||||
|
if (allSelected) interactor.deselectAllTapped()
|
||||||
|
else interactor.selectAllTapped()
|
||||||
|
}
|
||||||
|
|
||||||
|
view.bottom_button_bar_layout.setOnClickListener(null)
|
||||||
|
view.bottom_button_bar_layout.isClickable = false
|
||||||
|
|
||||||
|
val drawable = view.context.getDrawable(R.drawable.ic_close)
|
||||||
|
drawable?.setTint(ContextCompat.getColor(view.context, R.color.photonWhite))
|
||||||
|
view.bottom_bar_icon_button.setImageDrawable(drawable)
|
||||||
|
view.bottom_bar_icon_button.contentDescription =
|
||||||
|
view.context.getString(R.string.create_collection_close)
|
||||||
|
view.bottom_bar_icon_button.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
|
||||||
|
view.bottom_bar_icon_button.setOnClickListener {
|
||||||
|
interactor.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
TransitionManager.beginDelayedTransition(
|
||||||
|
view.collection_constraint_layout,
|
||||||
|
transition
|
||||||
|
)
|
||||||
|
val constraint = selectTabsConstraints
|
||||||
|
constraint.applyTo(view.collection_constraint_layout)
|
||||||
|
|
||||||
|
collectionCreationTabListAdapter.updateData(state.tabs, state.selectedTabs)
|
||||||
|
|
||||||
|
back_button.text = view.context.getString(R.string.create_collection_select_tabs)
|
||||||
|
|
||||||
|
val selectTabsText = if (state.selectedTabs.isEmpty()) {
|
||||||
|
view.context.getString(R.string.create_collection_save_to_collection_empty)
|
||||||
|
} else {
|
||||||
|
view.context.getString(
|
||||||
|
if (state.selectedTabs.size == 1)
|
||||||
|
R.string.create_collection_save_to_collection_tab_selected else
|
||||||
|
R.string.create_collection_save_to_collection_tabs_selected,
|
||||||
|
state.selectedTabs.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
view.bottom_bar_text.text = selectTabsText
|
||||||
|
|
||||||
|
save_button.setOnClickListener { _ ->
|
||||||
|
if (selectedCollection != null) {
|
||||||
|
interactor.selectCollection(
|
||||||
|
collection = selectedCollection!!,
|
||||||
|
tabs = state.selectedTabs.toList()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
interactor.saveTabsToCollection(tabs = selectedTabs.toList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
save_button.visibility = if (state.selectedTabs.isEmpty()) {
|
||||||
|
View.GONE
|
||||||
|
} else {
|
||||||
|
View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateForSelectCollection() {
|
||||||
|
view.tab_list.isClickable = false
|
||||||
|
|
||||||
|
save_button.visibility = View.GONE
|
||||||
|
|
||||||
|
view.bottom_bar_text.text =
|
||||||
|
view.context.getString(R.string.create_collection_add_new_collection)
|
||||||
|
|
||||||
|
val drawable = view.context.getDrawable(R.drawable.ic_new)
|
||||||
|
drawable?.setTint(ContextCompat.getColor(view.context, R.color.photonWhite))
|
||||||
|
view.bottom_bar_icon_button.setImageDrawable(drawable)
|
||||||
|
view.bottom_bar_icon_button.contentDescription = null
|
||||||
|
view.bottom_bar_icon_button.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||||
|
view.bottom_button_bar_layout.isClickable = true
|
||||||
|
view.bottom_button_bar_layout.setOnClickListener {
|
||||||
|
interactor.addNewCollection()
|
||||||
|
}
|
||||||
|
|
||||||
|
back_button.setOnClickListener {
|
||||||
|
interactor.onBackPressed(SaveCollectionStep.SelectCollection)
|
||||||
|
}
|
||||||
|
TransitionManager.beginDelayedTransition(
|
||||||
|
view.collection_constraint_layout,
|
||||||
|
transition
|
||||||
|
)
|
||||||
|
val constraint = selectCollectionConstraints
|
||||||
|
constraint.applyTo(view.collection_constraint_layout)
|
||||||
|
back_button.text =
|
||||||
|
view.context.getString(R.string.create_collection_select_collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateForNameCollection(state: CollectionCreationState) {
|
||||||
|
view.tab_list.isClickable = false
|
||||||
|
|
||||||
|
collectionCreationTabListAdapter.updateData(state.selectedTabs.toList(), state.selectedTabs, true)
|
||||||
|
back_button.setOnClickListener {
|
||||||
|
name_collection_edittext.hideKeyboard()
|
||||||
|
val handler = Handler()
|
||||||
|
handler.postDelayed({
|
||||||
|
interactor.onBackPressed(SaveCollectionStep.NameCollection)
|
||||||
|
}, TRANSITION_DURATION)
|
||||||
|
}
|
||||||
|
transition.addListener(object : Transition.TransitionListener {
|
||||||
|
override fun onTransitionStart(transition: Transition) { /* noop */ }
|
||||||
|
|
||||||
|
override fun onTransitionEnd(transition: Transition) {
|
||||||
|
view.name_collection_edittext.showKeyboard()
|
||||||
|
transition.removeListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTransitionCancel(transition: Transition) { /* noop */ }
|
||||||
|
override fun onTransitionPause(transition: Transition) { /* noop */ }
|
||||||
|
override fun onTransitionResume(transition: Transition) { /* noop */ }
|
||||||
|
})
|
||||||
|
TransitionManager.beginDelayedTransition(
|
||||||
|
view.collection_constraint_layout,
|
||||||
|
transition
|
||||||
|
)
|
||||||
|
val constraint = nameCollectionConstraints
|
||||||
|
constraint.applyTo(view.collection_constraint_layout)
|
||||||
|
name_collection_edittext.setText(
|
||||||
|
view.context.getString(
|
||||||
|
R.string.create_collection_default_name,
|
||||||
|
state.tabCollections.size + 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
name_collection_edittext.setSelection(0, name_collection_edittext.text.length)
|
||||||
|
back_button.text =
|
||||||
|
view.context.getString(R.string.create_collection_name_collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateForRenameCollection(state: CollectionCreationState) {
|
||||||
|
view.tab_list.isClickable = false
|
||||||
|
|
||||||
|
state.selectedTabCollection?.let { tabCollection ->
|
||||||
|
tabCollection.tabs.map { tab ->
|
||||||
|
Tab(
|
||||||
|
tab.id.toString(),
|
||||||
|
tab.url,
|
||||||
|
tab.url.urlToTrimmedHost(view.context),
|
||||||
|
tab.title
|
||||||
|
)
|
||||||
|
}.let { tabs ->
|
||||||
|
collectionCreationTabListAdapter.updateData(tabs, tabs.toSet(), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val constraint = nameCollectionConstraints
|
||||||
|
constraint.applyTo(view.collection_constraint_layout)
|
||||||
|
name_collection_edittext.setText(state.selectedTabCollection?.title)
|
||||||
|
name_collection_edittext.setSelection(0, name_collection_edittext.text.length)
|
||||||
|
|
||||||
|
back_button.text =
|
||||||
|
view.context.getString(R.string.collection_rename)
|
||||||
|
back_button.setOnClickListener {
|
||||||
|
name_collection_edittext.hideKeyboard()
|
||||||
|
val handler = Handler()
|
||||||
|
handler.postDelayed({
|
||||||
|
interactor.onBackPressed(SaveCollectionStep.RenameCollection)
|
||||||
|
}, TRANSITION_DURATION)
|
||||||
|
}
|
||||||
|
transition.addListener(object : Transition.TransitionListener {
|
||||||
|
override fun onTransitionStart(transition: Transition) { /* noop */ }
|
||||||
|
|
||||||
|
override fun onTransitionEnd(transition: Transition) {
|
||||||
|
view.name_collection_edittext.showKeyboard()
|
||||||
|
transition.removeListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTransitionCancel(transition: Transition) { /* noop */ }
|
||||||
|
override fun onTransitionPause(transition: Transition) { /* noop */ }
|
||||||
|
override fun onTransitionResume(transition: Transition) { /* noop */ }
|
||||||
|
})
|
||||||
|
TransitionManager.beginDelayedTransition(
|
||||||
|
view.collection_constraint_layout,
|
||||||
|
transition
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onResumed() {
|
||||||
|
if (step == SaveCollectionStep.NameCollection || step == SaveCollectionStep.RenameCollection) {
|
||||||
|
view.name_collection_edittext.showKeyboard()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onKey(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
|
return if (event?.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
|
interactor.onBackPressed(step)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TRANSITION_DURATION = 200L
|
||||||
|
private const val increaseButtonByDps = 16
|
||||||
|
private const val COLLECTION_NAME_MAX_LENGTH = 128
|
||||||
|
}
|
||||||
|
}
|
@ -1,196 +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.collections
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import kotlinx.android.synthetic.main.fragment_create_collection.view.*
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.mozilla.fenix.FenixViewModelProvider
|
|
||||||
import org.mozilla.fenix.R
|
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
|
||||||
import org.mozilla.fenix.ext.components
|
|
||||||
import org.mozilla.fenix.ext.requireComponents
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.Tab
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.toSessionBundle
|
|
||||||
import org.mozilla.fenix.mvi.ActionBusFactory
|
|
||||||
import org.mozilla.fenix.mvi.getAutoDisposeObservable
|
|
||||||
import org.mozilla.fenix.mvi.getManagedEmitter
|
|
||||||
|
|
||||||
class CreateCollectionFragment : DialogFragment() {
|
|
||||||
private lateinit var collectionCreationComponent: CollectionCreationComponent
|
|
||||||
private val viewModel: CreateCollectionViewModel by activityViewModels {
|
|
||||||
ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
isCancelable = false
|
|
||||||
setStyle(STYLE_NO_TITLE, R.style.CreateCollectionDialogStyle)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
val view = inflater.inflate(R.layout.fragment_create_collection, container, false)
|
|
||||||
|
|
||||||
collectionCreationComponent = CollectionCreationComponent(
|
|
||||||
view.createCollectionWrapper,
|
|
||||||
ActionBusFactory.get(this),
|
|
||||||
FenixViewModelProvider.create(
|
|
||||||
this,
|
|
||||||
CollectionCreationViewModel::class.java
|
|
||||||
) {
|
|
||||||
CollectionCreationViewModel(viewModel.state)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
val dialog = super.onCreateDialog(savedInstanceState)
|
|
||||||
dialog.setOnKeyListener { _, keyCode, event ->
|
|
||||||
(collectionCreationComponent.uiView as CollectionCreationUIView).onKey(keyCode, event)
|
|
||||||
}
|
|
||||||
return dialog
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
(collectionCreationComponent.uiView as CollectionCreationUIView).onResumed()
|
|
||||||
subscribeToActions()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("ComplexMethod")
|
|
||||||
private fun subscribeToActions() {
|
|
||||||
getAutoDisposeObservable<CollectionCreationAction>().subscribe {
|
|
||||||
when (it) {
|
|
||||||
is CollectionCreationAction.Close -> dismiss()
|
|
||||||
is CollectionCreationAction.SaveTabsToCollection -> {
|
|
||||||
getManagedEmitter<CollectionCreationChange>()
|
|
||||||
.onNext(
|
|
||||||
CollectionCreationChange.StepChanged(
|
|
||||||
if (viewModel.state.tabCollections.isEmpty()) {
|
|
||||||
SaveCollectionStep.NameCollection
|
|
||||||
} else {
|
|
||||||
SaveCollectionStep.SelectCollection
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is CollectionCreationAction.AddTabToSelection -> {
|
|
||||||
getManagedEmitter<CollectionCreationChange>()
|
|
||||||
.onNext(CollectionCreationChange.TabAdded(it.tab))
|
|
||||||
}
|
|
||||||
is CollectionCreationAction.RemoveTabFromSelection -> {
|
|
||||||
getManagedEmitter<CollectionCreationChange>()
|
|
||||||
.onNext(CollectionCreationChange.TabRemoved(it.tab))
|
|
||||||
}
|
|
||||||
is CollectionCreationAction.SelectAllTapped -> {
|
|
||||||
getManagedEmitter<CollectionCreationChange>()
|
|
||||||
.onNext(CollectionCreationChange.AddAllTabs)
|
|
||||||
}
|
|
||||||
is CollectionCreationAction.DeselectAllTapped -> {
|
|
||||||
getManagedEmitter<CollectionCreationChange>()
|
|
||||||
.onNext(CollectionCreationChange.RemoveAllTabs)
|
|
||||||
}
|
|
||||||
is CollectionCreationAction.AddNewCollection -> getManagedEmitter<CollectionCreationChange>().onNext(
|
|
||||||
CollectionCreationChange.StepChanged(SaveCollectionStep.NameCollection)
|
|
||||||
)
|
|
||||||
is CollectionCreationAction.BackPressed -> handleBackPress(backPressFrom = it.backPressFrom)
|
|
||||||
is CollectionCreationAction.SaveCollectionName -> {
|
|
||||||
dismiss()
|
|
||||||
|
|
||||||
context?.let { context ->
|
|
||||||
val sessionBundle = it.tabs.toList().toSessionBundle(context)
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
context.components.core.tabCollectionStorage.createCollection(it.name, sessionBundle)
|
|
||||||
}
|
|
||||||
|
|
||||||
context.components.analytics.metrics.track(
|
|
||||||
Event.CollectionSaved(normalSessionSize(), sessionBundle.size)
|
|
||||||
)
|
|
||||||
|
|
||||||
closeTabsIfNecessary(it.tabs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is CollectionCreationAction.SelectCollection -> {
|
|
||||||
dismiss()
|
|
||||||
context?.let { context ->
|
|
||||||
val sessionBundle = it.tabs.toList().toSessionBundle(context)
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
context.components.core.tabCollectionStorage
|
|
||||||
.addTabsToCollection(it.collection, sessionBundle)
|
|
||||||
}
|
|
||||||
|
|
||||||
context.components.analytics.metrics.track(
|
|
||||||
Event.CollectionTabsAdded(normalSessionSize(), sessionBundle.size)
|
|
||||||
)
|
|
||||||
|
|
||||||
closeTabsIfNecessary(it.tabs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is CollectionCreationAction.RenameCollection -> {
|
|
||||||
dismiss()
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
context?.components?.core?.tabCollectionStorage?.renameCollection(it.collection, it.name)
|
|
||||||
context?.components?.analytics?.metrics?.track(Event.CollectionRenamed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun normalSessionSize(): Int {
|
|
||||||
return requireComponents.core.sessionManager.sessions.filter { session ->
|
|
||||||
(!session.isCustomTabSession() && !session.private)
|
|
||||||
}.size
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleBackPress(backPressFrom: SaveCollectionStep) {
|
|
||||||
val newStep = stepBack(backPressFrom)
|
|
||||||
if (newStep != null) {
|
|
||||||
getManagedEmitter<CollectionCreationChange>().onNext(CollectionCreationChange.StepChanged(newStep))
|
|
||||||
} else {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stepBack(backFromStep: SaveCollectionStep): SaveCollectionStep? {
|
|
||||||
val state = viewModel.state
|
|
||||||
return when (backFromStep) {
|
|
||||||
SaveCollectionStep.SelectTabs, SaveCollectionStep.RenameCollection -> null
|
|
||||||
SaveCollectionStep.SelectCollection -> if (state.tabs.size <= 1) {
|
|
||||||
stepBack(SaveCollectionStep.SelectTabs)
|
|
||||||
} else {
|
|
||||||
SaveCollectionStep.SelectTabs
|
|
||||||
}
|
|
||||||
SaveCollectionStep.NameCollection -> if (state.tabCollections.isEmpty()) {
|
|
||||||
stepBack(SaveCollectionStep.SelectCollection)
|
|
||||||
} else {
|
|
||||||
SaveCollectionStep.SelectCollection
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun closeTabsIfNecessary(tabs: List<Tab>) {
|
|
||||||
// Only close the tabs if the user is not on the BrowserFragment
|
|
||||||
if (viewModel.previousFragmentId == R.id.browserFragment) { return }
|
|
||||||
val components = requireComponents
|
|
||||||
tabs.asSequence()
|
|
||||||
.mapNotNull { tab -> components.core.sessionManager.findSessionById(tab.sessionId) }
|
|
||||||
.forEach { session -> components.useCases.tabsUseCases.removeTab(session) }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,50 +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.collections
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.Tab
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.TabCollection
|
|
||||||
|
|
||||||
class CreateCollectionViewModel : ViewModel() {
|
|
||||||
var state = CollectionCreationState()
|
|
||||||
private set
|
|
||||||
|
|
||||||
var previousFragmentId: Int? = null
|
|
||||||
|
|
||||||
fun updateCollection(
|
|
||||||
tabs: List<Tab>,
|
|
||||||
saveCollectionStep: SaveCollectionStep,
|
|
||||||
selectedTabCollection: TabCollection,
|
|
||||||
cachedTabCollections: List<TabCollection>
|
|
||||||
) {
|
|
||||||
state = CollectionCreationState(
|
|
||||||
tabs = tabs,
|
|
||||||
selectedTabs = if (tabs.size == 1) setOf(tabs.first()) else emptySet(),
|
|
||||||
tabCollections = cachedTabCollections.reversed(),
|
|
||||||
selectedTabCollection = selectedTabCollection,
|
|
||||||
saveCollectionStep = saveCollectionStep
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveTabToCollection(
|
|
||||||
tabs: List<Tab>,
|
|
||||||
selectedTab: Tab?,
|
|
||||||
cachedTabCollections: List<TabCollection>
|
|
||||||
) {
|
|
||||||
val tabCollections = cachedTabCollections.reversed()
|
|
||||||
state = CollectionCreationState(
|
|
||||||
tabs = tabs,
|
|
||||||
selectedTabs = selectedTab?.let { setOf(it) } ?: emptySet(),
|
|
||||||
tabCollections = tabCollections,
|
|
||||||
selectedTabCollection = null,
|
|
||||||
saveCollectionStep = when {
|
|
||||||
tabs.size > 1 -> SaveCollectionStep.SelectTabs
|
|
||||||
tabCollections.isNotEmpty() -> SaveCollectionStep.SelectCollection
|
|
||||||
else -> SaveCollectionStep.NameCollection
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,117 @@
|
|||||||
|
/* 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.collections
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import assertk.assertThat
|
||||||
|
import assertk.assertions.isNotNull
|
||||||
|
import assertk.assertions.isNull
|
||||||
|
import assertk.assertions.isTrue
|
||||||
|
import mozilla.components.support.test.robolectric.createAddedTestFragment
|
||||||
|
import io.mockk.MockKAnnotations
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.impl.annotations.MockK
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.ObsoleteCoroutinesApi
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import mozilla.components.browser.session.Session
|
||||||
|
import mozilla.components.browser.session.SessionManager
|
||||||
|
import mozilla.components.feature.tab.collections.Tab
|
||||||
|
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mozilla.fenix.TestApplication
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
|
private const val URL_MOZILLA = "www.mozilla.org"
|
||||||
|
private const val SESSION_ID_MOZILLA = "0"
|
||||||
|
private const val URL_BCC = "www.bcc.co.uk"
|
||||||
|
private const val SESSION_ID_BCC = "1"
|
||||||
|
|
||||||
|
private const val SESSION_ID_BAD_1 = "not a real session id"
|
||||||
|
private const val SESSION_ID_BAD_2 = "definitely not a real session id"
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
@ObsoleteCoroutinesApi
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@Config(application = TestApplication::class)
|
||||||
|
class CollectionCreationFragmentTest {
|
||||||
|
|
||||||
|
@MockK private lateinit var sessionManager: SessionManager
|
||||||
|
@MockK private lateinit var publicSuffixList: PublicSuffixList
|
||||||
|
|
||||||
|
private val sessionMozilla = Session(initialUrl = URL_MOZILLA, id = SESSION_ID_MOZILLA)
|
||||||
|
private val sessionBcc = Session(initialUrl = URL_BCC, id = SESSION_ID_BCC)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun before() {
|
||||||
|
MockKAnnotations.init(this)
|
||||||
|
every { sessionManager.findSessionById(SESSION_ID_MOZILLA) } answers { sessionMozilla }
|
||||||
|
every { sessionManager.findSessionById(SESSION_ID_BCC) } answers { sessionBcc }
|
||||||
|
every { sessionManager.findSessionById(SESSION_ID_BAD_1) } answers { null }
|
||||||
|
every { sessionManager.findSessionById(SESSION_ID_BAD_2) } answers { null }
|
||||||
|
every { publicSuffixList.stripPublicSuffix(URL_MOZILLA) } answers { GlobalScope.async { URL_MOZILLA } }
|
||||||
|
every { publicSuffixList.stripPublicSuffix(URL_BCC) } answers { GlobalScope.async { URL_BCC } }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `creation dialog shows and can be dismissed`() {
|
||||||
|
val fragment = createAddedTestFragment {
|
||||||
|
CollectionCreationFragment().apply {
|
||||||
|
arguments = CollectionCreationFragmentArgs(
|
||||||
|
// Fragment crashes if navArgs is null
|
||||||
|
previousFragmentId = 0,
|
||||||
|
saveCollectionStep = SaveCollectionStep.SelectTabs
|
||||||
|
).toBundle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(fragment.dialog).isNotNull()
|
||||||
|
assertThat(fragment.requireDialog().isShowing).isTrue()
|
||||||
|
fragment.dismiss()
|
||||||
|
assertThat(fragment.dialog).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN tabs are present in session manager WHEN getTabs is called THEN tabs will be returned`() {
|
||||||
|
val tabs = sessionManager
|
||||||
|
.getTabs(arrayOf(SESSION_ID_MOZILLA, SESSION_ID_BCC), publicSuffixList)
|
||||||
|
|
||||||
|
val hosts = tabs.map { it.hostname }
|
||||||
|
|
||||||
|
assertEquals(URL_MOZILLA, hosts[0])
|
||||||
|
assertEquals(URL_BCC, hosts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN some tabs are present in session manager WHEN getTabs is called THEN only valid tabs will be returned`() {
|
||||||
|
val tabs = sessionManager
|
||||||
|
.getTabs(arrayOf(SESSION_ID_MOZILLA, SESSION_ID_BAD_1), publicSuffixList)
|
||||||
|
|
||||||
|
val hosts = tabs.map { it.hostname }
|
||||||
|
|
||||||
|
assertEquals(URL_MOZILLA, hosts[0])
|
||||||
|
assertEquals(1, hosts.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN tabs are not present in session manager WHEN getTabs is called THEN an empty list will be returned`() {
|
||||||
|
val tabs = sessionManager
|
||||||
|
.getTabs(arrayOf(SESSION_ID_BAD_1, SESSION_ID_BAD_2), publicSuffixList)
|
||||||
|
|
||||||
|
assertEquals(emptyList<Tab>(), tabs)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `WHEN getTabs is called will null tabIds THEN an empty list will be returned`() {
|
||||||
|
val tabs = sessionManager
|
||||||
|
.getTabs(null, publicSuffixList)
|
||||||
|
|
||||||
|
assertEquals(emptyList<Tab>(), tabs)
|
||||||
|
}
|
||||||
|
}
|
@ -1,30 +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.collections
|
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import assertk.assertThat
|
|
||||||
import assertk.assertions.isNotNull
|
|
||||||
import assertk.assertions.isNull
|
|
||||||
import assertk.assertions.isTrue
|
|
||||||
import mozilla.components.support.test.robolectric.createAddedTestFragment
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.mozilla.fenix.TestApplication
|
|
||||||
import org.robolectric.annotation.Config
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
@Config(application = TestApplication::class)
|
|
||||||
class CreateCollectionFragmentTest {
|
|
||||||
@Test
|
|
||||||
fun `creation dialog shows and can be dismissed`() {
|
|
||||||
val fragment = createAddedTestFragment { CreateCollectionFragment() }
|
|
||||||
|
|
||||||
assertThat(fragment.dialog).isNotNull()
|
|
||||||
assertThat(fragment.requireDialog().isShowing).isTrue()
|
|
||||||
fragment.dismiss()
|
|
||||||
assertThat(fragment.dialog).isNull()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,150 +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.collections
|
|
||||||
|
|
||||||
import io.mockk.MockKAnnotations
|
|
||||||
import io.mockk.mockk
|
|
||||||
import mozilla.components.feature.tab.collections.TabCollection
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Assert.assertNull
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.Tab
|
|
||||||
|
|
||||||
class CreateCollectionViewModelTest {
|
|
||||||
|
|
||||||
private lateinit var viewModel: CreateCollectionViewModel
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
MockKAnnotations.init(this)
|
|
||||||
|
|
||||||
viewModel = CreateCollectionViewModel()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `initial state defaults`() {
|
|
||||||
assertEquals(
|
|
||||||
CollectionCreationState(
|
|
||||||
tabs = emptyList(),
|
|
||||||
selectedTabs = emptySet(),
|
|
||||||
saveCollectionStep = SaveCollectionStep.SelectTabs,
|
|
||||||
tabCollections = emptyList(),
|
|
||||||
selectedTabCollection = null
|
|
||||||
),
|
|
||||||
viewModel.state
|
|
||||||
)
|
|
||||||
assertNull(viewModel.previousFragmentId)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `updateCollection copies tabs to state`() {
|
|
||||||
val tabs = listOf<Tab>(mockk(), mockk())
|
|
||||||
val tabCollections = listOf<TabCollection>(mockk(), mockk())
|
|
||||||
val selectedCollection: TabCollection = mockk()
|
|
||||||
viewModel.updateCollection(
|
|
||||||
tabs = tabs,
|
|
||||||
saveCollectionStep = SaveCollectionStep.SelectCollection,
|
|
||||||
selectedTabCollection = selectedCollection,
|
|
||||||
cachedTabCollections = tabCollections
|
|
||||||
)
|
|
||||||
assertEquals(tabs, viewModel.state.tabs)
|
|
||||||
assertEquals(SaveCollectionStep.SelectCollection, viewModel.state.saveCollectionStep)
|
|
||||||
assertEquals(selectedCollection, viewModel.state.selectedTabCollection)
|
|
||||||
assertEquals(tabCollections.reversed(), viewModel.state.tabCollections)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `updateCollection selects the only tab`() {
|
|
||||||
val tab: Tab = mockk()
|
|
||||||
viewModel.updateCollection(
|
|
||||||
tabs = listOf(tab),
|
|
||||||
saveCollectionStep = mockk(),
|
|
||||||
selectedTabCollection = mockk(),
|
|
||||||
cachedTabCollections = emptyList()
|
|
||||||
)
|
|
||||||
assertEquals(setOf(tab), viewModel.state.selectedTabs)
|
|
||||||
|
|
||||||
viewModel.updateCollection(
|
|
||||||
tabs = listOf(tab, mockk()),
|
|
||||||
saveCollectionStep = mockk(),
|
|
||||||
selectedTabCollection = mockk(),
|
|
||||||
cachedTabCollections = emptyList()
|
|
||||||
)
|
|
||||||
assertEquals(emptySet<Tab>(), viewModel.state.selectedTabs)
|
|
||||||
|
|
||||||
viewModel.updateCollection(
|
|
||||||
tabs = emptyList(),
|
|
||||||
saveCollectionStep = mockk(),
|
|
||||||
selectedTabCollection = mockk(),
|
|
||||||
cachedTabCollections = emptyList()
|
|
||||||
)
|
|
||||||
assertEquals(emptySet<Tab>(), viewModel.state.selectedTabs)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `saveTabToCollection copies tabs to state`() {
|
|
||||||
val tabs = listOf<Tab>(mockk(), mockk())
|
|
||||||
val tabCollections = listOf<TabCollection>(mockk(), mockk())
|
|
||||||
viewModel.saveTabToCollection(
|
|
||||||
tabs = tabs,
|
|
||||||
selectedTab = null,
|
|
||||||
cachedTabCollections = tabCollections
|
|
||||||
)
|
|
||||||
assertEquals(tabs, viewModel.state.tabs)
|
|
||||||
assertEquals(SaveCollectionStep.SelectTabs, viewModel.state.saveCollectionStep)
|
|
||||||
assertNull(viewModel.state.selectedTabCollection)
|
|
||||||
assertEquals(tabCollections.reversed(), viewModel.state.tabCollections)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `saveTabToCollection selects selectedTab`() {
|
|
||||||
val tab: Tab = mockk()
|
|
||||||
viewModel.saveTabToCollection(
|
|
||||||
tabs = listOf(mockk()),
|
|
||||||
selectedTab = tab,
|
|
||||||
cachedTabCollections = emptyList()
|
|
||||||
)
|
|
||||||
assertEquals(setOf(tab), viewModel.state.selectedTabs)
|
|
||||||
|
|
||||||
viewModel.saveTabToCollection(
|
|
||||||
tabs = listOf(mockk()),
|
|
||||||
selectedTab = null,
|
|
||||||
cachedTabCollections = emptyList()
|
|
||||||
)
|
|
||||||
assertEquals(emptySet<Tab>(), viewModel.state.selectedTabs)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `saveTabToCollection sets saveCollectionStep`() {
|
|
||||||
viewModel.saveTabToCollection(
|
|
||||||
tabs = listOf(mockk(), mockk()),
|
|
||||||
selectedTab = null,
|
|
||||||
cachedTabCollections = listOf(mockk())
|
|
||||||
)
|
|
||||||
assertEquals(SaveCollectionStep.SelectTabs, viewModel.state.saveCollectionStep)
|
|
||||||
|
|
||||||
viewModel.saveTabToCollection(
|
|
||||||
tabs = listOf(mockk()),
|
|
||||||
selectedTab = null,
|
|
||||||
cachedTabCollections = listOf(mockk())
|
|
||||||
)
|
|
||||||
assertEquals(SaveCollectionStep.SelectCollection, viewModel.state.saveCollectionStep)
|
|
||||||
|
|
||||||
viewModel.saveTabToCollection(
|
|
||||||
tabs = emptyList(),
|
|
||||||
selectedTab = null,
|
|
||||||
cachedTabCollections = listOf(mockk())
|
|
||||||
)
|
|
||||||
assertEquals(SaveCollectionStep.SelectCollection, viewModel.state.saveCollectionStep)
|
|
||||||
|
|
||||||
viewModel.saveTabToCollection(
|
|
||||||
tabs = emptyList(),
|
|
||||||
selectedTab = null,
|
|
||||||
cachedTabCollections = emptyList()
|
|
||||||
)
|
|
||||||
assertEquals(SaveCollectionStep.NameCollection, viewModel.state.saveCollectionStep)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,120 @@
|
|||||||
|
package org.mozilla.fenix.collections
|
||||||
|
|
||||||
|
import io.mockk.MockKAnnotations
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.impl.annotations.MockK
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.TestCoroutineScope
|
||||||
|
import mozilla.components.browser.session.Session
|
||||||
|
import mozilla.components.browser.session.SessionManager
|
||||||
|
import mozilla.components.feature.tabs.TabsUseCases
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mozilla.fenix.components.Analytics
|
||||||
|
import org.mozilla.fenix.components.TabCollectionStorage
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
class DefaultCollectionCreationControllerTest {
|
||||||
|
|
||||||
|
private val testCoroutineScope = TestCoroutineScope()
|
||||||
|
|
||||||
|
private lateinit var controller: DefaultCollectionCreationController
|
||||||
|
|
||||||
|
@MockK private lateinit var store: CollectionCreationStore
|
||||||
|
@MockK(relaxed = true) private lateinit var dismiss: () -> Unit
|
||||||
|
@MockK(relaxed = true) private lateinit var analytics: Analytics
|
||||||
|
@MockK private lateinit var tabCollectionStorage: TabCollectionStorage
|
||||||
|
@MockK private lateinit var tabsUseCases: TabsUseCases
|
||||||
|
@MockK private lateinit var sessionManager: SessionManager
|
||||||
|
@MockK private lateinit var state: CollectionCreationState
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun before() {
|
||||||
|
MockKAnnotations.init(this)
|
||||||
|
|
||||||
|
every { state.previousFragmentId } returns 0
|
||||||
|
every { store.state } returns state
|
||||||
|
every { state.tabCollections } returns emptyList()
|
||||||
|
every { state.tabs } returns emptyList()
|
||||||
|
|
||||||
|
controller = DefaultCollectionCreationController(store, dismiss, analytics,
|
||||||
|
tabCollectionStorage, tabsUseCases, sessionManager, testCoroutineScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN previous step was SelectTabs or RenameCollection WHEN stepBack is called THEN null should be returned`() {
|
||||||
|
assertNull(controller.stepBack(SaveCollectionStep.SelectTabs))
|
||||||
|
assertNull(controller.stepBack(SaveCollectionStep.RenameCollection))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN previous step was SelectCollection AND more than one tab is open WHEN stepBack is called THEN SelectTabs should be returned`() {
|
||||||
|
every { state.tabs } returns listOf(mockk(), mockk())
|
||||||
|
|
||||||
|
assertEquals(SaveCollectionStep.SelectTabs, controller.stepBack(SaveCollectionStep.SelectCollection))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN previous step was SelectCollection AND one or fewer tabs are open WHEN stepbback is called THEN null should be returned`() {
|
||||||
|
every { state.tabs } returns listOf(mockk())
|
||||||
|
assertNull(controller.stepBack(SaveCollectionStep.SelectCollection))
|
||||||
|
|
||||||
|
every { state.tabs } returns emptyList()
|
||||||
|
assertNull(controller.stepBack(SaveCollectionStep.SelectCollection))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN previous step was NameCollection AND tabCollections is empty AND more than one tab is open WHEN stepBack is called THEN SelectTabs should be returned`() {
|
||||||
|
every { state.tabCollections } returns emptyList()
|
||||||
|
every { state.tabs } returns listOf(mockk(), mockk())
|
||||||
|
|
||||||
|
assertEquals(SaveCollectionStep.SelectTabs, controller.stepBack(SaveCollectionStep.NameCollection))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN previous step was NameCollection AND tabCollections is empty AND one or fewer tabs are open WHEN stepBack is called THEN null should be returned`() {
|
||||||
|
every { state.tabCollections } returns emptyList()
|
||||||
|
every { state.tabs } returns listOf(mockk())
|
||||||
|
assertNull(controller.stepBack(SaveCollectionStep.NameCollection))
|
||||||
|
|
||||||
|
every { state.tabCollections } returns emptyList()
|
||||||
|
every { state.tabs } returns emptyList()
|
||||||
|
assertNull(controller.stepBack(SaveCollectionStep.NameCollection))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN previous step was NameCollection AND tabCollections is not empty WHEN stepBack is called THEN SelectCollection should be returned`() {
|
||||||
|
every { state.tabCollections } returns listOf(mockk())
|
||||||
|
|
||||||
|
assertEquals(SaveCollectionStep.SelectCollection, controller.stepBack(SaveCollectionStep.NameCollection))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `normalSessionSize only counts non-private non-custom sessions`() {
|
||||||
|
fun session(isPrivate: Boolean, isCustom: Boolean) = mockk<Session>().apply {
|
||||||
|
every { private } returns isPrivate
|
||||||
|
every { isCustomTabSession() } returns isCustom
|
||||||
|
}
|
||||||
|
|
||||||
|
val normal1 = session(isPrivate = false, isCustom = false)
|
||||||
|
val normal2 = session(isPrivate = false, isCustom = false)
|
||||||
|
val normal3 = session(isPrivate = false, isCustom = false)
|
||||||
|
|
||||||
|
val private1 = session(isPrivate = true, isCustom = false)
|
||||||
|
val private2 = session(isPrivate = true, isCustom = false)
|
||||||
|
|
||||||
|
val custom1 = session(isPrivate = false, isCustom = true)
|
||||||
|
val custom2 = session(isPrivate = false, isCustom = true)
|
||||||
|
val custom3 = session(isPrivate = false, isCustom = true)
|
||||||
|
|
||||||
|
val privateCustom = session(isPrivate = true, isCustom = true)
|
||||||
|
|
||||||
|
every { sessionManager.sessions } returns listOf(normal1, private1, private2, custom1,
|
||||||
|
normal2, normal3, custom2, custom3, privateCustom)
|
||||||
|
|
||||||
|
assertEquals(3, controller.normalSessionSize(sessionManager))
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue