diff --git a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt index 4a0263743..daefc841b 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -17,8 +17,6 @@ import androidx.appcompat.app.AppCompatActivity import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.net.toUri import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.navigation.NavDirections import androidx.navigation.fragment.findNavController @@ -60,7 +58,6 @@ import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.IntentReceiverActivity import org.mozilla.fenix.R -import org.mozilla.fenix.collections.CreateCollectionViewModel import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FindInPageIntegration import org.mozilla.fenix.components.StoreProvider @@ -115,10 +112,6 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs private var browserInitialized: Boolean = false private var initUIJob: Job? = null - val viewModel: CreateCollectionViewModel by activityViewModels { - ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652 - } - @CallSuper override fun onCreateView( inflater: LayoutInflater, @@ -185,7 +178,6 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs swipeRefresh = swipeRefresh, adjustBackgroundAndNavigate = ::adjustBackgroundAndNavigate, customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, - viewModel = viewModel, getSupportUrl = { SupportUtils.getSumoURLForTopic( context, diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationComponent.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationComponent.kt deleted file mode 100644 index 32bb5f40d..000000000 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationComponent.kt +++ /dev/null @@ -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 = emptyList(), - val selectedTabs: Set = emptySet(), - val saveCollectionStep: SaveCollectionStep = SaveCollectionStep.SelectTabs, - val tabCollections: List = emptyList(), - val selectedTabCollection: TabCollection? = null -) : ViewState - -sealed class CollectionCreationChange : Change { - data class TabListChange(val tabs: List) : 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) : CollectionCreationAction() - data class BackPressed(val backPressFrom: SaveCollectionStep) : CollectionCreationAction() - data class SaveCollectionName(val tabs: List, val name: String) : - CollectionCreationAction() - data class RenameCollection(val collection: TabCollection, val name: String) : - CollectionCreationAction() - data class SelectCollection(val collection: TabCollection, val tabs: List) : - CollectionCreationAction() -} - -class CollectionCreationComponent( - private val container: ViewGroup, - bus: ActionBusFactory, - viewModelProvider: UIComponentViewModelProvider -) : UIComponent( - 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( - initialState, - reducer - ) { - - companion object { - val reducer: Reducer = { 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) - } - } - } -} diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationController.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationController.kt new file mode 100644 index 000000000..fcd3e269e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationController.kt @@ -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, 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) + + /** + * See [CollectionCreationInteractor.saveTabsToCollection] + */ + fun saveTabsToCollection(tabs: List) + + 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, 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) { + 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) { + 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, 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) } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationFragment.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationFragment.kt new file mode 100644 index 000000000..06abd9006 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationFragment.kt @@ -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?, publicSuffixList: PublicSuffixList): List { + return tabIds + ?.mapNotNull { this.findSessionById(it) } + ?.map { it.toTab(publicSuffixList) } + ?: emptyList() +} diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationInteractor.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationInteractor.kt new file mode 100644 index 000000000..1dba731c7 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationInteractor.kt @@ -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, 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) + + /** + * Called when the user decides to save tabs to the currently selected session. + */ + fun saveTabsToCollection(tabs: List) + + 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, 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) { + controller.selectCollection(collection, tabs) + } + + override fun saveTabsToCollection(tabs: List) { + controller.saveTabsToCollection(tabs) + } + + override fun addNewCollection() { + controller.addNewCollection() + } + + override fun addTabToSelection(tab: Tab) { + controller.addTabToSelection(tab) + } + + override fun removeTabFromSelection(tab: Tab) { + controller.removeTabFromSelection(tab) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationStore.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationStore.kt new file mode 100644 index 000000000..f23f28ea7 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationStore.kt @@ -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( + 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 = emptyList(), + val selectedTabs: Set = emptySet(), + val saveCollectionStep: SaveCollectionStep = SaveCollectionStep.SelectTabs, + val tabCollections: List = 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) +} diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationTabListAdapter.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationTabListAdapter.kt index 475868f2c..c52e1f994 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationTabListAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationTabListAdapter.kt @@ -11,7 +11,6 @@ import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import io.reactivex.Observer import kotlinx.android.synthetic.main.collection_tab_list_row.view.* import org.mozilla.fenix.R import org.mozilla.fenix.ext.components @@ -19,7 +18,7 @@ import org.mozilla.fenix.ext.loadIntoView import org.mozilla.fenix.home.sessioncontrol.Tab class CollectionCreationTabListAdapter( - val actionEmitter: Observer + private val interactor: CollectionCreationInteractor ) : RecyclerView.Adapter() { private var tabs: List = listOf() private var selectedTabs: MutableSet = mutableSetOf() @@ -54,14 +53,13 @@ class CollectionCreationTabListAdapter( val tab = tabs[position] val isSelected = selectedTabs.contains(tab) holder.itemView.tab_selected_checkbox.setOnCheckedChangeListener { _, isChecked -> - val action = if (isChecked) { + if (isChecked) { selectedTabs.add(tab) - CollectionCreationAction.AddTabToSelection(tab) + interactor.addTabToSelection(tab) } else { selectedTabs.remove(tab) - CollectionCreationAction.RemoveTabFromSelection(tab) + interactor.removeTabFromSelection(tab) } - actionEmitter.onNext(action) } holder.bind(tab, isSelected, hideCheckboxes) } diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationUIView.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationUIView.kt deleted file mode 100644 index 4a4b6a73a..000000000 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationUIView.kt +++ /dev/null @@ -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, - changesObservable: Observable -) : UIView( - 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 = 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 { - 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 - } -} diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationView.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationView.kt new file mode 100644 index 000000000..990f75008 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationView.kt @@ -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 = 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 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/collections/CreateCollectionFragment.kt b/app/src/main/java/org/mozilla/fenix/collections/CreateCollectionFragment.kt deleted file mode 100644 index 83bea3ed1..000000000 --- a/app/src/main/java/org/mozilla/fenix/collections/CreateCollectionFragment.kt +++ /dev/null @@ -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().subscribe { - when (it) { - is CollectionCreationAction.Close -> dismiss() - is CollectionCreationAction.SaveTabsToCollection -> { - getManagedEmitter() - .onNext( - CollectionCreationChange.StepChanged( - if (viewModel.state.tabCollections.isEmpty()) { - SaveCollectionStep.NameCollection - } else { - SaveCollectionStep.SelectCollection - } - ) - ) - } - is CollectionCreationAction.AddTabToSelection -> { - getManagedEmitter() - .onNext(CollectionCreationChange.TabAdded(it.tab)) - } - is CollectionCreationAction.RemoveTabFromSelection -> { - getManagedEmitter() - .onNext(CollectionCreationChange.TabRemoved(it.tab)) - } - is CollectionCreationAction.SelectAllTapped -> { - getManagedEmitter() - .onNext(CollectionCreationChange.AddAllTabs) - } - is CollectionCreationAction.DeselectAllTapped -> { - getManagedEmitter() - .onNext(CollectionCreationChange.RemoveAllTabs) - } - is CollectionCreationAction.AddNewCollection -> getManagedEmitter().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().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) { - // 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) } - } -} diff --git a/app/src/main/java/org/mozilla/fenix/collections/CreateCollectionViewModel.kt b/app/src/main/java/org/mozilla/fenix/collections/CreateCollectionViewModel.kt deleted file mode 100644 index b10b472af..000000000 --- a/app/src/main/java/org/mozilla/fenix/collections/CreateCollectionViewModel.kt +++ /dev/null @@ -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, - saveCollectionStep: SaveCollectionStep, - selectedTabCollection: TabCollection, - cachedTabCollections: List - ) { - 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, - selectedTab: Tab?, - cachedTabCollections: List - ) { - 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 - } - ) - } -} diff --git a/app/src/main/java/org/mozilla/fenix/collections/SaveCollectionListAdapter.kt b/app/src/main/java/org/mozilla/fenix/collections/SaveCollectionListAdapter.kt index abc24fe70..484ad5c21 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/SaveCollectionListAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/SaveCollectionListAdapter.kt @@ -9,7 +9,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import io.reactivex.Observer import kotlinx.android.synthetic.main.collections_list_item.view.* import org.mozilla.fenix.R import org.mozilla.fenix.components.description @@ -18,7 +17,7 @@ import org.mozilla.fenix.home.sessioncontrol.Tab import org.mozilla.fenix.home.sessioncontrol.TabCollection class SaveCollectionListAdapter( - val actionEmitter: Observer + private val interactor: CollectionCreationInteractor ) : RecyclerView.Adapter() { private var tabCollections = listOf() @@ -35,8 +34,7 @@ class SaveCollectionListAdapter( val collection = tabCollections[position] holder.bind(collection) holder.itemView.setOnClickListener { - val action = CollectionCreationAction.SelectCollection(collection, selectedTabs.toList()) - actionEmitter.onNext(action) + interactor.selectCollection(collection, selectedTabs.toList()) } } diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt index 2d358d88f..2eebccb26 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt @@ -31,12 +31,11 @@ import org.mozilla.fenix.browser.BrowserFragment import org.mozilla.fenix.browser.BrowserFragmentDirections import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager -import org.mozilla.fenix.collections.CreateCollectionViewModel import org.mozilla.fenix.components.FenixSnackbar +import org.mozilla.fenix.collections.SaveCollectionStep import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.nav -import org.mozilla.fenix.ext.toTab import org.mozilla.fenix.lib.Do import org.mozilla.fenix.quickactionsheet.QuickActionSheetBehavior import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit @@ -64,7 +63,6 @@ class DefaultBrowserToolbarController( private val adjustBackgroundAndNavigate: (NavDirections) -> Unit, private val swipeRefresh: SwipeRefreshLayout, private val customTabSession: Session?, - private val viewModel: CreateCollectionViewModel, private val getSupportUrl: () -> String, private val openInFenixIntent: Intent, private val bottomSheetBehavior: QuickActionSheetBehavior, @@ -181,16 +179,13 @@ class DefaultBrowserToolbarController( activity.components.analytics.metrics .track(Event.CollectionSaveButtonPressed(TELEMETRY_BROWSER_IDENTIFIER)) - currentSession?.toTab(activity)?.let { currentSessionAsTab -> - viewModel.saveTabToCollection( - tabs = listOf(currentSessionAsTab), - selectedTab = currentSessionAsTab, - cachedTabCollections = activity.components.core.tabCollectionStorage.cachedTabCollections + currentSession?.let { currentSession -> + val directions = BrowserFragmentDirections.actionBrowserFragmentToCreateCollectionFragment( + previousFragmentId = R.id.browserFragment, + tabIds = arrayOf(currentSession.id), + selectedTabIds = arrayOf(currentSession.id), + saveCollectionStep = SaveCollectionStep.SelectCollection ) - viewModel.previousFragmentId = R.id.browserFragment - - val directions = - BrowserFragmentDirections.actionBrowserFragmentToCreateCollectionFragment() navController.nav(R.id.browserFragment, directions) } } diff --git a/app/src/main/java/org/mozilla/fenix/ext/Session.kt b/app/src/main/java/org/mozilla/fenix/ext/Session.kt index 9b086637a..ff6ebcc3c 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/Session.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/Session.kt @@ -7,13 +7,17 @@ package org.mozilla.fenix.ext import android.content.Context import mozilla.components.browser.session.Session import mozilla.components.feature.media.state.MediaState +import mozilla.components.lib.publicsuffixlist.PublicSuffixList import org.mozilla.fenix.home.sessioncontrol.Tab -fun Session.toTab(context: Context, selected: Boolean? = null, mediaState: MediaState? = null): Tab { +fun Session.toTab(context: Context, selected: Boolean? = null, mediaState: MediaState? = null): Tab = + this.toTab(context.components.publicSuffixList, selected, mediaState) + +fun Session.toTab(publicSuffixList: PublicSuffixList, selected: Boolean? = null, mediaState: MediaState? = null): Tab { return Tab( this.id, this.url, - this.url.urlToTrimmedHost(context), + this.url.urlToTrimmedHost(publicSuffixList), this.title, selected, mediaState, diff --git a/app/src/main/java/org/mozilla/fenix/ext/String.kt b/app/src/main/java/org/mozilla/fenix/ext/String.kt index eed77ff39..ce22e79d7 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/String.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/String.kt @@ -7,6 +7,7 @@ package org.mozilla.fenix.ext import android.content.Context import androidx.core.net.toUri import kotlinx.coroutines.runBlocking +import mozilla.components.lib.publicsuffixlist.PublicSuffixList import mozilla.components.support.ktx.android.net.hostWithoutCommonPrefixes import java.net.MalformedURLException import java.net.URL @@ -33,11 +34,17 @@ fun String.tryGetHostFromUrl(): String = try { /** * Trim a host's prefix and suffix */ -fun String.urlToTrimmedHost(context: Context): String { +fun String.urlToTrimmedHost(context: Context): String = + this.urlToTrimmedHost(context.components.publicSuffixList) + +/** + * Trim a host's prefix and suffix + */ +fun String.urlToTrimmedHost(publicSuffixList: PublicSuffixList): String { return try { val host = toUri().hostWithoutCommonPrefixes ?: return this runBlocking { - context.components.publicSuffixList.stripPublicSuffix(host).await() + publicSuffixList.stripPublicSuffix(host).await() } } catch (e: MalformedURLException) { this diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index 5d83e974f..857bcb2f7 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -65,7 +65,6 @@ import org.mozilla.fenix.FenixViewModelProvider import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode -import org.mozilla.fenix.collections.CreateCollectionViewModel import org.mozilla.fenix.collections.SaveCollectionStep import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.PrivateShortcutCreateManager @@ -515,10 +514,16 @@ class HomeFragment : Fragment() { } is CollectionAction.AddTab -> { requireComponents.analytics.metrics.track(Event.CollectionAddTabPressed) - updateCollection(action.collection, SaveCollectionStep.SelectTabs) + showCollectionCreationFragment( + step = SaveCollectionStep.SelectTabs, + selectedTabCollectionId = action.collection.id + ) } is CollectionAction.Rename -> { - updateCollection(action.collection, SaveCollectionStep.RenameCollection) + showCollectionCreationFragment( + step = SaveCollectionStep.RenameCollection, + selectedTabCollectionId = action.collection.id + ) requireComponents.analytics.metrics.track(Event.CollectionRenamePressed) } is CollectionAction.OpenTab -> { @@ -805,48 +810,40 @@ class HomeFragment : Fragment() { } private fun showCollectionCreationFragment( - setupViewModel: (CreateCollectionViewModel, tabs: List, cachedTabCollections: List) -> Unit + step: SaveCollectionStep, + selectedTabIds: Array? = null, + selectedTabCollectionId: Long? = null ) { - if (findNavController().currentDestination?.id == R.id.createCollectionFragment) return + if (findNavController().currentDestination?.id == R.id.collectionCreationFragment) return - val viewModel: CreateCollectionViewModel by activityViewModels { - ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652 - } - - val tabs = getListOfSessions().toTabs() val storage = requireComponents.core.tabCollectionStorage - setupViewModel(viewModel, tabs, storage.cachedTabCollections) - - viewModel.previousFragmentId = R.id.homeFragment - // Only register the observer right before moving to collection creation storage.register(collectionStorageObserver, this) + val tabIds = getListOfSessions().toTabs().map { it.sessionId }.toTypedArray() view?.let { - val directions = HomeFragmentDirections.actionHomeFragmentToCreateCollectionFragment() + val directions = HomeFragmentDirections.actionHomeFragmentToCreateCollectionFragment( + tabIds = tabIds, + previousFragmentId = R.id.homeFragment, + saveCollectionStep = step, + selectedTabIds = selectedTabIds, + selectedTabCollectionId = selectedTabCollectionId ?: -1 + ) nav(R.id.homeFragment, directions) } } private fun saveTabToCollection(selectedTabId: String?) { - showCollectionCreationFragment { viewModel, tabs, cachedTabCollections -> - viewModel.saveTabToCollection( - tabs = tabs, - selectedTab = tabs.find { it.sessionId == selectedTabId } ?: if (tabs.size == 1) tabs[0] else null, - cachedTabCollections = cachedTabCollections - ) - } - } + val tabs = getListOfSessions().toTabs() + val storage = requireComponents.core.tabCollectionStorage - private fun updateCollection(selectedTabCollection: TabCollection, step: SaveCollectionStep) { - showCollectionCreationFragment { viewModel, tabs, cachedTabCollections -> - viewModel.updateCollection( - tabs = tabs, - saveCollectionStep = step, - selectedTabCollection = selectedTabCollection, - cachedTabCollections = cachedTabCollections - ) + val step = when { + tabs.size > 1 -> SaveCollectionStep.SelectTabs + storage.cachedTabCollections.isNotEmpty() -> SaveCollectionStep.SelectCollection + else -> SaveCollectionStep.NameCollection } + + showCollectionCreationFragment(step, selectedTabId?.let { arrayOf(it) }) } private fun share(url: String? = null, tabs: List? = null) { diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlComponent.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlComponent.kt index e5e59dd8b..cdb53b3c0 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlComponent.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlComponent.kt @@ -11,6 +11,7 @@ import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import io.reactivex.Observer import mozilla.components.browser.session.Session +import mozilla.components.browser.session.SessionManager import mozilla.components.feature.media.state.MediaState import org.mozilla.fenix.ext.components import org.mozilla.fenix.home.Mode @@ -55,10 +56,13 @@ data class Tab( val icon: Bitmap? = null ) -fun List.toSessionBundle(context: Context): MutableList { +fun List.toSessionBundle(context: Context): MutableList = + this.toSessionBundle(context.components.core.sessionManager) + +fun List.toSessionBundle(sessionManager: SessionManager): MutableList { val sessionBundle = mutableListOf() this.forEach { - context.components.core.sessionManager.findSessionById(it.sessionId)?.let { session -> + sessionManager.findSessionById(it.sessionId)?.let { session -> sessionBundle.add(session) } } diff --git a/app/src/main/res/layout/fragment_create_collection.xml b/app/src/main/res/layout/fragment_create_collection.xml index 389d9d19a..900db2934 100644 --- a/app/src/main/res/layout/fragment_create_collection.xml +++ b/app/src/main/res/layout/fragment_create_collection.xml @@ -10,6 +10,6 @@ android:layout_height="match_parent" android:background="@drawable/scrim_background" android:fitsSystemWindows="true" - tools:context="org.mozilla.fenix.collections.CreateCollectionFragment"> + tools:context="org.mozilla.fenix.collections.CollectionCreationFragment"> diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 9b988b619..e8a6824fc 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -78,7 +78,7 @@ app:destination="@id/settingsFragment" /> + app:destination="@id/collectionCreationFragment" /> @@ -179,7 +179,7 @@ app:destination="@id/bookmarkEditFragment" /> + app:destination="@id/collectionCreationFragment" /> @@ -465,10 +465,34 @@ app:popUpToInclusive="true" /> + tools:layout="@layout/fragment_create_collection" > + + + + + + + (), 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(), tabs) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/collections/CreateCollectionFragmentTest.kt b/app/src/test/java/org/mozilla/fenix/collections/CreateCollectionFragmentTest.kt deleted file mode 100644 index 613f36656..000000000 --- a/app/src/test/java/org/mozilla/fenix/collections/CreateCollectionFragmentTest.kt +++ /dev/null @@ -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() - } -} diff --git a/app/src/test/java/org/mozilla/fenix/collections/CreateCollectionViewModelTest.kt b/app/src/test/java/org/mozilla/fenix/collections/CreateCollectionViewModelTest.kt deleted file mode 100644 index d3d066ea5..000000000 --- a/app/src/test/java/org/mozilla/fenix/collections/CreateCollectionViewModelTest.kt +++ /dev/null @@ -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(mockk(), mockk()) - val tabCollections = listOf(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(), viewModel.state.selectedTabs) - - viewModel.updateCollection( - tabs = emptyList(), - saveCollectionStep = mockk(), - selectedTabCollection = mockk(), - cachedTabCollections = emptyList() - ) - assertEquals(emptySet(), viewModel.state.selectedTabs) - } - - @Test - fun `saveTabToCollection copies tabs to state`() { - val tabs = listOf(mockk(), mockk()) - val tabCollections = listOf(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(), 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) - } -} diff --git a/app/src/test/java/org/mozilla/fenix/collections/DefaultCollectionCreationControllerTest.kt b/app/src/test/java/org/mozilla/fenix/collections/DefaultCollectionCreationControllerTest.kt new file mode 100644 index 000000000..38355527c --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/collections/DefaultCollectionCreationControllerTest.kt @@ -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().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)) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt b/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt index af078df41..8a008485f 100644 --- a/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt @@ -4,6 +4,7 @@ package org.mozilla.fenix.components.toolbar +import android.content.Context import android.content.Intent import android.view.ViewGroup import androidx.core.widget.NestedScrollView @@ -40,7 +41,7 @@ import org.mozilla.fenix.browser.BrowserFragment import org.mozilla.fenix.browser.BrowserFragmentDirections import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager -import org.mozilla.fenix.collections.CreateCollectionViewModel +import org.mozilla.fenix.collections.SaveCollectionStep import org.mozilla.fenix.components.Analytics import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.metrics.Event @@ -67,7 +68,6 @@ class DefaultBrowserToolbarControllerTest { private var findInPageLauncher: () -> Unit = mockk(relaxed = true) private val engineView: EngineView = mockk(relaxed = true) private val currentSession: Session = mockk(relaxed = true) - private val viewModel: CreateCollectionViewModel = mockk(relaxed = true) private val getSupportUrl: () -> String = { "https://supportUrl.org" } private val openInFenixIntent: Intent = mockk(relaxed = true) private val currentSessionAsTab: Tab = mockk(relaxed = true) @@ -95,7 +95,6 @@ class DefaultBrowserToolbarControllerTest { engineView = engineView, adjustBackgroundAndNavigate = adjustBackgroundAndNavigate, customTabSession = null, - viewModel = viewModel, getSupportUrl = getSupportUrl, openInFenixIntent = openInFenixIntent, bottomSheetBehavior = bottomSheetBehavior, @@ -107,7 +106,7 @@ class DefaultBrowserToolbarControllerTest { mockkStatic( "org.mozilla.fenix.ext.SessionKt" ) - every { any().toTab(any()) } returns currentSessionAsTab + every { any().toTab(any()) } returns currentSessionAsTab mockkStatic( "org.mozilla.fenix.settings.deletebrowsingdata.DeleteAndQuitKt" @@ -414,16 +413,13 @@ class DefaultBrowserToolbarControllerTest { verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.SAVE_TO_COLLECTION)) } verify { metrics.track(Event.CollectionSaveButtonPressed(DefaultBrowserToolbarController.TELEMETRY_BROWSER_IDENTIFIER)) } verify { - viewModel.saveTabToCollection( - listOf(currentSessionAsTab), - currentSessionAsTab, - cachedTabCollections - ) - } - verify { viewModel.previousFragmentId = R.id.browserFragment } - verify { - val directions = BrowserFragmentDirections - .actionBrowserFragmentToSearchFragment(sessionId = null) + val directions = + BrowserFragmentDirections.actionBrowserFragmentToCreateCollectionFragment( + previousFragmentId = R.id.browserFragment, + saveCollectionStep = SaveCollectionStep.SelectCollection, + tabIds = arrayOf(currentSession.id), + selectedTabIds = arrayOf(currentSession.id) + ) navController.nav(R.id.browserFragment, directions) } } @@ -439,7 +435,6 @@ class DefaultBrowserToolbarControllerTest { engineView = engineView, adjustBackgroundAndNavigate = adjustBackgroundAndNavigate, customTabSession = currentSession, - viewModel = viewModel, getSupportUrl = getSupportUrl, openInFenixIntent = openInFenixIntent, bottomSheetBehavior = bottomSheetBehavior,