* 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