* 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 reviewmaster
parent
40cda1d758
commit
aa8642f534
|
@ -17,8 +17,6 @@ import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
@ -60,7 +58,6 @@ import org.mozilla.fenix.FeatureFlags
|
||||||
import org.mozilla.fenix.HomeActivity
|
import org.mozilla.fenix.HomeActivity
|
||||||
import org.mozilla.fenix.IntentReceiverActivity
|
import org.mozilla.fenix.IntentReceiverActivity
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.collections.CreateCollectionViewModel
|
|
||||||
import org.mozilla.fenix.components.FenixSnackbar
|
import org.mozilla.fenix.components.FenixSnackbar
|
||||||
import org.mozilla.fenix.components.FindInPageIntegration
|
import org.mozilla.fenix.components.FindInPageIntegration
|
||||||
import org.mozilla.fenix.components.StoreProvider
|
import org.mozilla.fenix.components.StoreProvider
|
||||||
|
@ -115,10 +112,6 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs
|
||||||
private var browserInitialized: Boolean = false
|
private var browserInitialized: Boolean = false
|
||||||
private var initUIJob: Job? = null
|
private var initUIJob: Job? = null
|
||||||
|
|
||||||
val viewModel: CreateCollectionViewModel by activityViewModels {
|
|
||||||
ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
|
@ -185,7 +178,6 @@ abstract class BaseBrowserFragment : Fragment(), BackHandler, SessionManager.Obs
|
||||||
swipeRefresh = swipeRefresh,
|
swipeRefresh = swipeRefresh,
|
||||||
adjustBackgroundAndNavigate = ::adjustBackgroundAndNavigate,
|
adjustBackgroundAndNavigate = ::adjustBackgroundAndNavigate,
|
||||||
customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) },
|
customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) },
|
||||||
viewModel = viewModel,
|
|
||||||
getSupportUrl = {
|
getSupportUrl = {
|
||||||
SupportUtils.getSumoURLForTopic(
|
SupportUtils.getSumoURLForTopic(
|
||||||
context,
|
context,
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -11,7 +11,6 @@ import androidx.core.view.isGone
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.reactivex.Observer
|
|
||||||
import kotlinx.android.synthetic.main.collection_tab_list_row.view.*
|
import kotlinx.android.synthetic.main.collection_tab_list_row.view.*
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
|
@ -19,7 +18,7 @@ import org.mozilla.fenix.ext.loadIntoView
|
||||||
import org.mozilla.fenix.home.sessioncontrol.Tab
|
import org.mozilla.fenix.home.sessioncontrol.Tab
|
||||||
|
|
||||||
class CollectionCreationTabListAdapter(
|
class CollectionCreationTabListAdapter(
|
||||||
val actionEmitter: Observer<CollectionCreationAction>
|
private val interactor: CollectionCreationInteractor
|
||||||
) : RecyclerView.Adapter<TabViewHolder>() {
|
) : RecyclerView.Adapter<TabViewHolder>() {
|
||||||
private var tabs: List<Tab> = listOf()
|
private var tabs: List<Tab> = listOf()
|
||||||
private var selectedTabs: MutableSet<Tab> = mutableSetOf()
|
private var selectedTabs: MutableSet<Tab> = mutableSetOf()
|
||||||
|
@ -54,14 +53,13 @@ class CollectionCreationTabListAdapter(
|
||||||
val tab = tabs[position]
|
val tab = tabs[position]
|
||||||
val isSelected = selectedTabs.contains(tab)
|
val isSelected = selectedTabs.contains(tab)
|
||||||
holder.itemView.tab_selected_checkbox.setOnCheckedChangeListener { _, isChecked ->
|
holder.itemView.tab_selected_checkbox.setOnCheckedChangeListener { _, isChecked ->
|
||||||
val action = if (isChecked) {
|
if (isChecked) {
|
||||||
selectedTabs.add(tab)
|
selectedTabs.add(tab)
|
||||||
CollectionCreationAction.AddTabToSelection(tab)
|
interactor.addTabToSelection(tab)
|
||||||
} else {
|
} else {
|
||||||
selectedTabs.remove(tab)
|
selectedTabs.remove(tab)
|
||||||
CollectionCreationAction.RemoveTabFromSelection(tab)
|
interactor.removeTabFromSelection(tab)
|
||||||
}
|
}
|
||||||
actionEmitter.onNext(action)
|
|
||||||
}
|
}
|
||||||
holder.bind(tab, isSelected, hideCheckboxes)
|
holder.bind(tab, isSelected, hideCheckboxes)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -9,7 +9,6 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.reactivex.Observer
|
|
||||||
import kotlinx.android.synthetic.main.collections_list_item.view.*
|
import kotlinx.android.synthetic.main.collections_list_item.view.*
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.components.description
|
import org.mozilla.fenix.components.description
|
||||||
|
@ -18,7 +17,7 @@ import org.mozilla.fenix.home.sessioncontrol.Tab
|
||||||
import org.mozilla.fenix.home.sessioncontrol.TabCollection
|
import org.mozilla.fenix.home.sessioncontrol.TabCollection
|
||||||
|
|
||||||
class SaveCollectionListAdapter(
|
class SaveCollectionListAdapter(
|
||||||
val actionEmitter: Observer<CollectionCreationAction>
|
private val interactor: CollectionCreationInteractor
|
||||||
) : RecyclerView.Adapter<CollectionViewHolder>() {
|
) : RecyclerView.Adapter<CollectionViewHolder>() {
|
||||||
|
|
||||||
private var tabCollections = listOf<TabCollection>()
|
private var tabCollections = listOf<TabCollection>()
|
||||||
|
@ -35,8 +34,7 @@ class SaveCollectionListAdapter(
|
||||||
val collection = tabCollections[position]
|
val collection = tabCollections[position]
|
||||||
holder.bind(collection)
|
holder.bind(collection)
|
||||||
holder.itemView.setOnClickListener {
|
holder.itemView.setOnClickListener {
|
||||||
val action = CollectionCreationAction.SelectCollection(collection, selectedTabs.toList())
|
interactor.selectCollection(collection, selectedTabs.toList())
|
||||||
actionEmitter.onNext(action)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,12 +31,11 @@ import org.mozilla.fenix.browser.BrowserFragment
|
||||||
import org.mozilla.fenix.browser.BrowserFragmentDirections
|
import org.mozilla.fenix.browser.BrowserFragmentDirections
|
||||||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||||
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
|
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
|
||||||
import org.mozilla.fenix.collections.CreateCollectionViewModel
|
|
||||||
import org.mozilla.fenix.components.FenixSnackbar
|
import org.mozilla.fenix.components.FenixSnackbar
|
||||||
|
import org.mozilla.fenix.collections.SaveCollectionStep
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.nav
|
import org.mozilla.fenix.ext.nav
|
||||||
import org.mozilla.fenix.ext.toTab
|
|
||||||
import org.mozilla.fenix.lib.Do
|
import org.mozilla.fenix.lib.Do
|
||||||
import org.mozilla.fenix.quickactionsheet.QuickActionSheetBehavior
|
import org.mozilla.fenix.quickactionsheet.QuickActionSheetBehavior
|
||||||
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
|
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
|
||||||
|
@ -64,7 +63,6 @@ class DefaultBrowserToolbarController(
|
||||||
private val adjustBackgroundAndNavigate: (NavDirections) -> Unit,
|
private val adjustBackgroundAndNavigate: (NavDirections) -> Unit,
|
||||||
private val swipeRefresh: SwipeRefreshLayout,
|
private val swipeRefresh: SwipeRefreshLayout,
|
||||||
private val customTabSession: Session?,
|
private val customTabSession: Session?,
|
||||||
private val viewModel: CreateCollectionViewModel,
|
|
||||||
private val getSupportUrl: () -> String,
|
private val getSupportUrl: () -> String,
|
||||||
private val openInFenixIntent: Intent,
|
private val openInFenixIntent: Intent,
|
||||||
private val bottomSheetBehavior: QuickActionSheetBehavior<NestedScrollView>,
|
private val bottomSheetBehavior: QuickActionSheetBehavior<NestedScrollView>,
|
||||||
|
@ -181,16 +179,13 @@ class DefaultBrowserToolbarController(
|
||||||
activity.components.analytics.metrics
|
activity.components.analytics.metrics
|
||||||
.track(Event.CollectionSaveButtonPressed(TELEMETRY_BROWSER_IDENTIFIER))
|
.track(Event.CollectionSaveButtonPressed(TELEMETRY_BROWSER_IDENTIFIER))
|
||||||
|
|
||||||
currentSession?.toTab(activity)?.let { currentSessionAsTab ->
|
currentSession?.let { currentSession ->
|
||||||
viewModel.saveTabToCollection(
|
val directions = BrowserFragmentDirections.actionBrowserFragmentToCreateCollectionFragment(
|
||||||
tabs = listOf(currentSessionAsTab),
|
previousFragmentId = R.id.browserFragment,
|
||||||
selectedTab = currentSessionAsTab,
|
tabIds = arrayOf(currentSession.id),
|
||||||
cachedTabCollections = activity.components.core.tabCollectionStorage.cachedTabCollections
|
selectedTabIds = arrayOf(currentSession.id),
|
||||||
|
saveCollectionStep = SaveCollectionStep.SelectCollection
|
||||||
)
|
)
|
||||||
viewModel.previousFragmentId = R.id.browserFragment
|
|
||||||
|
|
||||||
val directions =
|
|
||||||
BrowserFragmentDirections.actionBrowserFragmentToCreateCollectionFragment()
|
|
||||||
navController.nav(R.id.browserFragment, directions)
|
navController.nav(R.id.browserFragment, directions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,13 +7,17 @@ package org.mozilla.fenix.ext
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import mozilla.components.browser.session.Session
|
import mozilla.components.browser.session.Session
|
||||||
import mozilla.components.feature.media.state.MediaState
|
import mozilla.components.feature.media.state.MediaState
|
||||||
|
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
||||||
import org.mozilla.fenix.home.sessioncontrol.Tab
|
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(
|
return Tab(
|
||||||
this.id,
|
this.id,
|
||||||
this.url,
|
this.url,
|
||||||
this.url.urlToTrimmedHost(context),
|
this.url.urlToTrimmedHost(publicSuffixList),
|
||||||
this.title,
|
this.title,
|
||||||
selected,
|
selected,
|
||||||
mediaState,
|
mediaState,
|
||||||
|
|
|
@ -7,6 +7,7 @@ package org.mozilla.fenix.ext
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
||||||
import mozilla.components.support.ktx.android.net.hostWithoutCommonPrefixes
|
import mozilla.components.support.ktx.android.net.hostWithoutCommonPrefixes
|
||||||
import java.net.MalformedURLException
|
import java.net.MalformedURLException
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
@ -33,11 +34,17 @@ fun String.tryGetHostFromUrl(): String = try {
|
||||||
/**
|
/**
|
||||||
* Trim a host's prefix and suffix
|
* 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 {
|
return try {
|
||||||
val host = toUri().hostWithoutCommonPrefixes ?: return this
|
val host = toUri().hostWithoutCommonPrefixes ?: return this
|
||||||
runBlocking {
|
runBlocking {
|
||||||
context.components.publicSuffixList.stripPublicSuffix(host).await()
|
publicSuffixList.stripPublicSuffix(host).await()
|
||||||
}
|
}
|
||||||
} catch (e: MalformedURLException) {
|
} catch (e: MalformedURLException) {
|
||||||
this
|
this
|
||||||
|
|
|
@ -65,7 +65,6 @@ import org.mozilla.fenix.FenixViewModelProvider
|
||||||
import org.mozilla.fenix.HomeActivity
|
import org.mozilla.fenix.HomeActivity
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||||
import org.mozilla.fenix.collections.CreateCollectionViewModel
|
|
||||||
import org.mozilla.fenix.collections.SaveCollectionStep
|
import org.mozilla.fenix.collections.SaveCollectionStep
|
||||||
import org.mozilla.fenix.components.FenixSnackbar
|
import org.mozilla.fenix.components.FenixSnackbar
|
||||||
import org.mozilla.fenix.components.PrivateShortcutCreateManager
|
import org.mozilla.fenix.components.PrivateShortcutCreateManager
|
||||||
|
@ -515,10 +514,16 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
is CollectionAction.AddTab -> {
|
is CollectionAction.AddTab -> {
|
||||||
requireComponents.analytics.metrics.track(Event.CollectionAddTabPressed)
|
requireComponents.analytics.metrics.track(Event.CollectionAddTabPressed)
|
||||||
updateCollection(action.collection, SaveCollectionStep.SelectTabs)
|
showCollectionCreationFragment(
|
||||||
|
step = SaveCollectionStep.SelectTabs,
|
||||||
|
selectedTabCollectionId = action.collection.id
|
||||||
|
)
|
||||||
}
|
}
|
||||||
is CollectionAction.Rename -> {
|
is CollectionAction.Rename -> {
|
||||||
updateCollection(action.collection, SaveCollectionStep.RenameCollection)
|
showCollectionCreationFragment(
|
||||||
|
step = SaveCollectionStep.RenameCollection,
|
||||||
|
selectedTabCollectionId = action.collection.id
|
||||||
|
)
|
||||||
requireComponents.analytics.metrics.track(Event.CollectionRenamePressed)
|
requireComponents.analytics.metrics.track(Event.CollectionRenamePressed)
|
||||||
}
|
}
|
||||||
is CollectionAction.OpenTab -> {
|
is CollectionAction.OpenTab -> {
|
||||||
|
@ -805,48 +810,40 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showCollectionCreationFragment(
|
private fun showCollectionCreationFragment(
|
||||||
setupViewModel: (CreateCollectionViewModel, tabs: List<Tab>, cachedTabCollections: List<TabCollection>) -> Unit
|
step: SaveCollectionStep,
|
||||||
|
selectedTabIds: Array<String>? = 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
|
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
|
// Only register the observer right before moving to collection creation
|
||||||
storage.register(collectionStorageObserver, this)
|
storage.register(collectionStorageObserver, this)
|
||||||
|
|
||||||
|
val tabIds = getListOfSessions().toTabs().map { it.sessionId }.toTypedArray()
|
||||||
view?.let {
|
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)
|
nav(R.id.homeFragment, directions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveTabToCollection(selectedTabId: String?) {
|
private fun saveTabToCollection(selectedTabId: String?) {
|
||||||
showCollectionCreationFragment { viewModel, tabs, cachedTabCollections ->
|
val tabs = getListOfSessions().toTabs()
|
||||||
viewModel.saveTabToCollection(
|
val storage = requireComponents.core.tabCollectionStorage
|
||||||
tabs = tabs,
|
|
||||||
selectedTab = tabs.find { it.sessionId == selectedTabId } ?: if (tabs.size == 1) tabs[0] else null,
|
|
||||||
cachedTabCollections = cachedTabCollections
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateCollection(selectedTabCollection: TabCollection, step: SaveCollectionStep) {
|
val step = when {
|
||||||
showCollectionCreationFragment { viewModel, tabs, cachedTabCollections ->
|
tabs.size > 1 -> SaveCollectionStep.SelectTabs
|
||||||
viewModel.updateCollection(
|
storage.cachedTabCollections.isNotEmpty() -> SaveCollectionStep.SelectCollection
|
||||||
tabs = tabs,
|
else -> SaveCollectionStep.NameCollection
|
||||||
saveCollectionStep = step,
|
|
||||||
selectedTabCollection = selectedTabCollection,
|
|
||||||
cachedTabCollections = cachedTabCollections
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showCollectionCreationFragment(step, selectedTabId?.let { arrayOf(it) })
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun share(url: String? = null, tabs: List<ShareTab>? = null) {
|
private fun share(url: String? = null, tabs: List<ShareTab>? = null) {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.reactivex.Observer
|
import io.reactivex.Observer
|
||||||
import mozilla.components.browser.session.Session
|
import mozilla.components.browser.session.Session
|
||||||
|
import mozilla.components.browser.session.SessionManager
|
||||||
import mozilla.components.feature.media.state.MediaState
|
import mozilla.components.feature.media.state.MediaState
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.home.Mode
|
import org.mozilla.fenix.home.Mode
|
||||||
|
@ -55,10 +56,13 @@ data class Tab(
|
||||||
val icon: Bitmap? = null
|
val icon: Bitmap? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
fun List<Tab>.toSessionBundle(context: Context): MutableList<Session> {
|
fun List<Tab>.toSessionBundle(context: Context): MutableList<Session> =
|
||||||
|
this.toSessionBundle(context.components.core.sessionManager)
|
||||||
|
|
||||||
|
fun List<Tab>.toSessionBundle(sessionManager: SessionManager): MutableList<Session> {
|
||||||
val sessionBundle = mutableListOf<Session>()
|
val sessionBundle = mutableListOf<Session>()
|
||||||
this.forEach {
|
this.forEach {
|
||||||
context.components.core.sessionManager.findSessionById(it.sessionId)?.let { session ->
|
sessionManager.findSessionById(it.sessionId)?.let { session ->
|
||||||
sessionBundle.add(session)
|
sessionBundle.add(session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,6 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@drawable/scrim_background"
|
android:background="@drawable/scrim_background"
|
||||||
android:fitsSystemWindows="true"
|
android:fitsSystemWindows="true"
|
||||||
tools:context="org.mozilla.fenix.collections.CreateCollectionFragment">
|
tools:context="org.mozilla.fenix.collections.CollectionCreationFragment">
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
|
@ -78,7 +78,7 @@
|
||||||
app:destination="@id/settingsFragment" />
|
app:destination="@id/settingsFragment" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_homeFragment_to_createCollectionFragment"
|
android:id="@+id/action_homeFragment_to_createCollectionFragment"
|
||||||
app:destination="@id/createCollectionFragment" />
|
app:destination="@id/collectionCreationFragment" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_homeFragment_to_shareFragment"
|
android:id="@+id/action_homeFragment_to_shareFragment"
|
||||||
app:destination="@id/shareFragment" />
|
app:destination="@id/shareFragment" />
|
||||||
|
@ -179,7 +179,7 @@
|
||||||
app:destination="@id/bookmarkEditFragment" />
|
app:destination="@id/bookmarkEditFragment" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_browserFragment_to_createCollectionFragment"
|
android:id="@+id/action_browserFragment_to_createCollectionFragment"
|
||||||
app:destination="@id/createCollectionFragment" />
|
app:destination="@id/collectionCreationFragment" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_browserFragment_to_createShortcutFragment"
|
android:id="@+id/action_browserFragment_to_createShortcutFragment"
|
||||||
app:destination="@id/createShortcutFragment" />
|
app:destination="@id/createShortcutFragment" />
|
||||||
|
@ -465,10 +465,34 @@
|
||||||
app:popUpToInclusive="true" />
|
app:popUpToInclusive="true" />
|
||||||
</fragment>
|
</fragment>
|
||||||
<dialog
|
<dialog
|
||||||
android:id="@+id/createCollectionFragment"
|
android:id="@+id/collectionCreationFragment"
|
||||||
android:name="org.mozilla.fenix.collections.CreateCollectionFragment"
|
android:name="org.mozilla.fenix.collections.CollectionCreationFragment"
|
||||||
android:label="fragment_create_collection"
|
android:label="fragment_create_collection"
|
||||||
tools:layout="@layout/fragment_create_collection" />
|
tools:layout="@layout/fragment_create_collection" >
|
||||||
|
<argument
|
||||||
|
android:name="tabIds"
|
||||||
|
android:defaultValue="@null"
|
||||||
|
app:argType="string[]"
|
||||||
|
app:nullable="true" />
|
||||||
|
<argument
|
||||||
|
android:name="selectedTabIds"
|
||||||
|
android:defaultValue="@null"
|
||||||
|
app:argType="string[]"
|
||||||
|
app:nullable="true" />
|
||||||
|
<!-- nav_graph does not allow nullable Longs, so this defaults to -1 -->
|
||||||
|
<argument
|
||||||
|
android:name="selectedTabCollectionId"
|
||||||
|
android:defaultValue="-1L"
|
||||||
|
app:argType="long" />
|
||||||
|
<argument
|
||||||
|
android:name="previousFragmentId"
|
||||||
|
app:argType="reference"
|
||||||
|
app:nullable="false" />
|
||||||
|
<argument
|
||||||
|
android:name="saveCollectionStep"
|
||||||
|
app:argType="org.mozilla.fenix.collections.SaveCollectionStep"
|
||||||
|
app:nullable="false" />
|
||||||
|
</dialog>
|
||||||
<dialog
|
<dialog
|
||||||
android:id="@+id/createShortcutFragment"
|
android:id="@+id/createShortcutFragment"
|
||||||
android:name="org.mozilla.fenix.shortcut.CreateShortcutFragment"
|
android:name="org.mozilla.fenix.shortcut.CreateShortcutFragment"
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
package org.mozilla.fenix.components.toolbar
|
package org.mozilla.fenix.components.toolbar
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.widget.NestedScrollView
|
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.BrowserFragmentDirections
|
||||||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||||
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
|
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.Analytics
|
||||||
import org.mozilla.fenix.components.FenixSnackbar
|
import org.mozilla.fenix.components.FenixSnackbar
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
|
@ -67,7 +68,6 @@ class DefaultBrowserToolbarControllerTest {
|
||||||
private var findInPageLauncher: () -> Unit = mockk(relaxed = true)
|
private var findInPageLauncher: () -> Unit = mockk(relaxed = true)
|
||||||
private val engineView: EngineView = mockk(relaxed = true)
|
private val engineView: EngineView = mockk(relaxed = true)
|
||||||
private val currentSession: Session = 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 getSupportUrl: () -> String = { "https://supportUrl.org" }
|
||||||
private val openInFenixIntent: Intent = mockk(relaxed = true)
|
private val openInFenixIntent: Intent = mockk(relaxed = true)
|
||||||
private val currentSessionAsTab: Tab = mockk(relaxed = true)
|
private val currentSessionAsTab: Tab = mockk(relaxed = true)
|
||||||
|
@ -95,7 +95,6 @@ class DefaultBrowserToolbarControllerTest {
|
||||||
engineView = engineView,
|
engineView = engineView,
|
||||||
adjustBackgroundAndNavigate = adjustBackgroundAndNavigate,
|
adjustBackgroundAndNavigate = adjustBackgroundAndNavigate,
|
||||||
customTabSession = null,
|
customTabSession = null,
|
||||||
viewModel = viewModel,
|
|
||||||
getSupportUrl = getSupportUrl,
|
getSupportUrl = getSupportUrl,
|
||||||
openInFenixIntent = openInFenixIntent,
|
openInFenixIntent = openInFenixIntent,
|
||||||
bottomSheetBehavior = bottomSheetBehavior,
|
bottomSheetBehavior = bottomSheetBehavior,
|
||||||
|
@ -107,7 +106,7 @@ class DefaultBrowserToolbarControllerTest {
|
||||||
mockkStatic(
|
mockkStatic(
|
||||||
"org.mozilla.fenix.ext.SessionKt"
|
"org.mozilla.fenix.ext.SessionKt"
|
||||||
)
|
)
|
||||||
every { any<Session>().toTab(any()) } returns currentSessionAsTab
|
every { any<Session>().toTab(any<Context>()) } returns currentSessionAsTab
|
||||||
|
|
||||||
mockkStatic(
|
mockkStatic(
|
||||||
"org.mozilla.fenix.settings.deletebrowsingdata.DeleteAndQuitKt"
|
"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.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.SAVE_TO_COLLECTION)) }
|
||||||
verify { metrics.track(Event.CollectionSaveButtonPressed(DefaultBrowserToolbarController.TELEMETRY_BROWSER_IDENTIFIER)) }
|
verify { metrics.track(Event.CollectionSaveButtonPressed(DefaultBrowserToolbarController.TELEMETRY_BROWSER_IDENTIFIER)) }
|
||||||
verify {
|
verify {
|
||||||
viewModel.saveTabToCollection(
|
val directions =
|
||||||
listOf(currentSessionAsTab),
|
BrowserFragmentDirections.actionBrowserFragmentToCreateCollectionFragment(
|
||||||
currentSessionAsTab,
|
previousFragmentId = R.id.browserFragment,
|
||||||
cachedTabCollections
|
saveCollectionStep = SaveCollectionStep.SelectCollection,
|
||||||
)
|
tabIds = arrayOf(currentSession.id),
|
||||||
}
|
selectedTabIds = arrayOf(currentSession.id)
|
||||||
verify { viewModel.previousFragmentId = R.id.browserFragment }
|
)
|
||||||
verify {
|
|
||||||
val directions = BrowserFragmentDirections
|
|
||||||
.actionBrowserFragmentToSearchFragment(sessionId = null)
|
|
||||||
navController.nav(R.id.browserFragment, directions)
|
navController.nav(R.id.browserFragment, directions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -439,7 +435,6 @@ class DefaultBrowserToolbarControllerTest {
|
||||||
engineView = engineView,
|
engineView = engineView,
|
||||||
adjustBackgroundAndNavigate = adjustBackgroundAndNavigate,
|
adjustBackgroundAndNavigate = adjustBackgroundAndNavigate,
|
||||||
customTabSession = currentSession,
|
customTabSession = currentSession,
|
||||||
viewModel = viewModel,
|
|
||||||
getSupportUrl = getSupportUrl,
|
getSupportUrl = getSupportUrl,
|
||||||
openInFenixIntent = openInFenixIntent,
|
openInFenixIntent = openInFenixIntent,
|
||||||
bottomSheetBehavior = bottomSheetBehavior,
|
bottomSheetBehavior = bottomSheetBehavior,
|
||||||
|
|
Loading…
Reference in New Issue