* For #5574 - Part 1: Port TabAction.SaveTabGroup to TabSessionInteractor and SessionControlController. (#6651) - Introduces the TabSessionInteractor, SessionControlInteractor and SessionControlController classes. - Removes the TabAction.SaveTabGroup. * For #5574 - Part 2: Port TabAction.PrivateBrowsingLearnMore to TabSessionInteractor and SessionControlController (#6651) * For #5574 - Part 3: Port TabAction.ShareTabs to TabSessionInteractor and SessionControlController (#6651) * For #5574 - Part 4: Remove unused TabAction.Share and TabItemMenu (#6651) In #2205, the tab overflow button was removed which would have shown the TabItemMenu when clicked. So, we can remove TabItemMenu since it is not used and as a result, we can also remove TabAction.Share since there are no consumers. * For #5574 - Part 5: Port TabAction.PlayMedia and TabAction.PauseMedia to TabSessionInteractor and SessionControlController (#6651) * For #5574 - Part 6: Port TabAction.Select to TabSessionInteractor and SessionControlController (#6651) * For #5574 - Part 7: Port Onboarding.Finish to OnboardingInteractor and SessionControlController (#6651) * For #5574 - Part 8: Port TabAction.Close and TabAction.CloseAll to TabSessionInteractor and SessionControlController (#6651) - Removes TabAction * For #5574 - Part 9: Port CollectionAction.Delete to CollectionInteractor and SessionControlController (#6651) * For #5574 - Part 10: Port CollectionAction.ShareTabs to CollectionInteractor and SessionControlController (#6651) * For #5574 - Part 11: Port CollectionAction.AddTab and CollectionAction.Rename to CollectionInteractor and SessionControlController (#6651) * For #5574 - Part 12: Port CollectionAction.RemoveTab to CollectionInteractor and SessionControlController (#6651) * For #5574 - Part 13: Port CollectionAction.OpenTab to CollectionInteractor and SessionControlController (#6651) * For #5574 - Part 14: Port CollectionAction.CloseTabs to CollectionInteractor and SessionControlController (#6651) * For #5574 - Part 15: Introduce a HomeFragmentStore (#6651) - We will hook up the HomeFragmentStore in later parts. - Removes List<Tab>.toSessionBundle(context: Context) since it is unused. * For #5574 - Part 16: Port CollectionAction.Collapse and CollectionAction.Expand to CollectionInteractor and SessionControlController (#6651) - We assume the store is hooked up to the SessionControlController in this part, but this work will be done in a later part. - Removes CollectionAction. * For #5574 - Part 20: Remove the architecture module. (#6651) * For #5574 - Part 17: Remove duplicate subscribeToTabCollections in BrowserFragment.kt (#6651) There is a duplicate call of subscribeToTabCollections() in both HomeFragment and BrowserFragment. In this patch, we remove the call in BrowserFragment to avoid passing the HomeFragmentStore to BrowserFragment in order to dispatch the CollectionsChange event. * For #5574 - Part 18: Delete SessionControlComponent and fix TabCollection and Tab imports (#6651) * For #5574 - Part 19: Use the new HomeFragmentStore in the HomeFragment (#6651) - Renames SessionControlUIView to SessionControlView * For #5574 - Part 21: Fix white screen on home fragment (#6651) * For #5574 - Part 22: Fix formatting in SessionControlInteractor and replace See with @see in SessionControlController (#6651) * For #5574 - Part 23: Move to metrics.track call to the beginning of handleCollectionRemoveTab (#6651) This ensures that the metrics.track will be called immediately before the tab is removed from the collection. * For #5574 - Part 24: Use the sessionManager getter in SessionControlController (#6651) * For #5574 - Part 25: Use mapNotNull in List<Tab>.toSessionBundle (#6651) * For #5574 - Part 26: Simplify closeTab and closeAllTabs functions by assigning a deletionJob constant (#6651) * For #5574 - Part 27: Replace listOf() with emptyList() in removeAllTabsWithUndo (#6651) * For #5574 - Part 28: Replace the Context parameter with the HomeActivity in SessionControlController (#6651) * For #5574 - Part 29: Add test for HomeFragmentStore, DefaultSessionControlController and SessionControlInteractor (#6651) * For #5574 - Removes running CI against the architecture debug build varientmaster
parent
a07a77c01a
commit
56b21426eb
|
@ -363,8 +363,6 @@ androidExtensions {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':architecture')
|
|
||||||
|
|
||||||
geckoNightlyImplementation Deps.mozilla_browser_engine_gecko_nightly
|
geckoNightlyImplementation Deps.mozilla_browser_engine_gecko_nightly
|
||||||
geckoBetaImplementation Deps.mozilla_browser_engine_gecko_beta
|
geckoBetaImplementation Deps.mozilla_browser_engine_gecko_beta
|
||||||
|
|
||||||
|
@ -437,6 +435,7 @@ dependencies {
|
||||||
implementation Deps.mozilla_service_glean
|
implementation Deps.mozilla_service_glean
|
||||||
implementation Deps.mozilla_service_experiments
|
implementation Deps.mozilla_service_experiments
|
||||||
|
|
||||||
|
implementation Deps.mozilla_support_base
|
||||||
implementation Deps.mozilla_support_ktx
|
implementation Deps.mozilla_support_ktx
|
||||||
implementation Deps.mozilla_support_rustlog
|
implementation Deps.mozilla_support_rustlog
|
||||||
implementation Deps.mozilla_support_utils
|
implementation Deps.mozilla_support_utils
|
||||||
|
|
|
@ -1,34 +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
|
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import org.mozilla.fenix.mvi.Change
|
|
||||||
import org.mozilla.fenix.mvi.UIComponentViewModelBase
|
|
||||||
import org.mozilla.fenix.mvi.UIComponentViewModelProvider
|
|
||||||
import org.mozilla.fenix.mvi.ViewState
|
|
||||||
|
|
||||||
object FenixViewModelProvider {
|
|
||||||
fun <S : ViewState, C : Change, T : UIComponentViewModelBase<S, C>> create(
|
|
||||||
fragment: Fragment,
|
|
||||||
modelClass: Class<T>,
|
|
||||||
viewModelCreator: () -> T
|
|
||||||
): UIComponentViewModelProvider<S, C> {
|
|
||||||
val factory = object : ViewModelProvider.Factory {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
|
||||||
return viewModelCreator() as T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return object : UIComponentViewModelProvider<S, C> {
|
|
||||||
override fun fetchViewModel(): T {
|
|
||||||
return ViewModelProvider(fragment, factory).get(modelClass)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -14,7 +14,6 @@ import android.widget.Button
|
||||||
import android.widget.RadioButton
|
import android.widget.RadioButton
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.transition.TransitionInflater
|
import androidx.transition.TransitionInflater
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.android.synthetic.main.fragment_browser.*
|
import kotlinx.android.synthetic.main.fragment_browser.*
|
||||||
|
@ -25,6 +24,7 @@ import mozilla.components.feature.contextmenu.ContextMenuCandidate
|
||||||
import mozilla.components.feature.readerview.ReaderViewFeature
|
import mozilla.components.feature.readerview.ReaderViewFeature
|
||||||
import mozilla.components.feature.session.TrackingProtectionUseCases
|
import mozilla.components.feature.session.TrackingProtectionUseCases
|
||||||
import mozilla.components.feature.sitepermissions.SitePermissions
|
import mozilla.components.feature.sitepermissions.SitePermissions
|
||||||
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
import mozilla.components.feature.tabs.WindowFeature
|
import mozilla.components.feature.tabs.WindowFeature
|
||||||
import mozilla.components.lib.state.ext.consumeFrom
|
import mozilla.components.lib.state.ext.consumeFrom
|
||||||
import mozilla.components.support.base.feature.UserInteractionHandler
|
import mozilla.components.support.base.feature.UserInteractionHandler
|
||||||
|
@ -39,9 +39,6 @@ import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.nav
|
import org.mozilla.fenix.ext.nav
|
||||||
import org.mozilla.fenix.ext.requireComponents
|
import org.mozilla.fenix.ext.requireComponents
|
||||||
import org.mozilla.fenix.ext.settings
|
import org.mozilla.fenix.ext.settings
|
||||||
import org.mozilla.fenix.home.sessioncontrol.SessionControlChange
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.TabCollection
|
|
||||||
import org.mozilla.fenix.mvi.getManagedEmitter
|
|
||||||
import org.mozilla.fenix.trackingprotection.TrackingProtectionOverlay
|
import org.mozilla.fenix.trackingprotection.TrackingProtectionOverlay
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -119,7 +116,6 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
subscribeToTabCollections()
|
|
||||||
val toolbarSessionObserver = TrackingProtectionOverlay(
|
val toolbarSessionObserver = TrackingProtectionOverlay(
|
||||||
context = requireContext(),
|
context = requireContext(),
|
||||||
settings = requireContext().settings()
|
settings = requireContext().settings()
|
||||||
|
@ -232,17 +228,6 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun subscribeToTabCollections() {
|
|
||||||
requireComponents.core.tabCollectionStorage.getCollections().observe(this, Observer {
|
|
||||||
requireComponents.core.tabCollectionStorage.cachedTabCollections = it
|
|
||||||
getManagedEmitter<SessionControlChange>().onNext(
|
|
||||||
SessionControlChange.CollectionsChange(
|
|
||||||
it
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private val collectionStorageObserver = object : TabCollectionStorage.Observer {
|
private val collectionStorageObserver = object : TabCollectionStorage.Observer {
|
||||||
override fun onCollectionCreated(title: String, sessions: List<Session>) {
|
override fun onCollectionCreated(title: String, sessions: List<Session>) {
|
||||||
showTabSavedToCollectionSnackbar()
|
showTabSavedToCollectionSnackbar()
|
||||||
|
|
|
@ -17,8 +17,8 @@ import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.components.Analytics
|
import org.mozilla.fenix.components.Analytics
|
||||||
import org.mozilla.fenix.components.TabCollectionStorage
|
import org.mozilla.fenix.components.TabCollectionStorage
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
import org.mozilla.fenix.home.sessioncontrol.Tab
|
import org.mozilla.fenix.home.Tab
|
||||||
import org.mozilla.fenix.home.sessioncontrol.toSessionBundle
|
import org.mozilla.fenix.home.toSessionBundle
|
||||||
|
|
||||||
interface CollectionCreationController {
|
interface CollectionCreationController {
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.components.StoreProvider
|
import org.mozilla.fenix.components.StoreProvider
|
||||||
import org.mozilla.fenix.ext.requireComponents
|
import org.mozilla.fenix.ext.requireComponents
|
||||||
import org.mozilla.fenix.ext.toTab
|
import org.mozilla.fenix.ext.toTab
|
||||||
import org.mozilla.fenix.home.sessioncontrol.Tab
|
import org.mozilla.fenix.home.Tab
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
class CollectionCreationFragment : DialogFragment() {
|
class CollectionCreationFragment : DialogFragment() {
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
|
|
||||||
package org.mozilla.fenix.collections
|
package org.mozilla.fenix.collections
|
||||||
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.Tab
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
import org.mozilla.fenix.home.sessioncontrol.TabCollection
|
import org.mozilla.fenix.home.Tab
|
||||||
|
|
||||||
interface CollectionCreationInteractor {
|
interface CollectionCreationInteractor {
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import mozilla.components.lib.state.Action
|
||||||
import mozilla.components.lib.state.State
|
import mozilla.components.lib.state.State
|
||||||
import mozilla.components.lib.state.Store
|
import mozilla.components.lib.state.Store
|
||||||
import org.mozilla.fenix.collections.CollectionCreationAction.StepChanged
|
import org.mozilla.fenix.collections.CollectionCreationAction.StepChanged
|
||||||
import org.mozilla.fenix.home.sessioncontrol.Tab
|
import org.mozilla.fenix.home.Tab
|
||||||
|
|
||||||
class CollectionCreationStore(
|
class CollectionCreationStore(
|
||||||
initialState: CollectionCreationState
|
initialState: CollectionCreationState
|
||||||
|
|
|
@ -15,7 +15,7 @@ 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
|
||||||
import org.mozilla.fenix.ext.loadIntoView
|
import org.mozilla.fenix.ext.loadIntoView
|
||||||
import org.mozilla.fenix.home.sessioncontrol.Tab
|
import org.mozilla.fenix.home.Tab
|
||||||
|
|
||||||
class CollectionCreationTabListAdapter(
|
class CollectionCreationTabListAdapter(
|
||||||
private val interactor: CollectionCreationInteractor
|
private val interactor: CollectionCreationInteractor
|
||||||
|
|
|
@ -21,6 +21,7 @@ import androidx.transition.TransitionManager
|
||||||
import kotlinx.android.extensions.LayoutContainer
|
import kotlinx.android.extensions.LayoutContainer
|
||||||
import kotlinx.android.synthetic.main.component_collection_creation.*
|
import kotlinx.android.synthetic.main.component_collection_creation.*
|
||||||
import kotlinx.android.synthetic.main.component_collection_creation.view.*
|
import kotlinx.android.synthetic.main.component_collection_creation.view.*
|
||||||
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
import mozilla.components.support.ktx.android.view.hideKeyboard
|
import mozilla.components.support.ktx.android.view.hideKeyboard
|
||||||
import mozilla.components.support.ktx.android.view.showKeyboard
|
import mozilla.components.support.ktx.android.view.showKeyboard
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
|
@ -28,8 +29,7 @@ import org.mozilla.fenix.components.metrics.Event
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.increaseTapArea
|
import org.mozilla.fenix.ext.increaseTapArea
|
||||||
import org.mozilla.fenix.ext.toShortUrl
|
import org.mozilla.fenix.ext.toShortUrl
|
||||||
import org.mozilla.fenix.home.sessioncontrol.Tab
|
import org.mozilla.fenix.home.Tab
|
||||||
import org.mozilla.fenix.home.sessioncontrol.TabCollection
|
|
||||||
|
|
||||||
@SuppressWarnings("LargeClass")
|
@SuppressWarnings("LargeClass")
|
||||||
class CollectionCreationView(
|
class CollectionCreationView(
|
||||||
|
|
|
@ -10,11 +10,11 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlinx.android.synthetic.main.collections_list_item.view.*
|
import kotlinx.android.synthetic.main.collections_list_item.view.*
|
||||||
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.components.description
|
import org.mozilla.fenix.components.description
|
||||||
import org.mozilla.fenix.ext.getIconColor
|
import org.mozilla.fenix.ext.getIconColor
|
||||||
import org.mozilla.fenix.home.sessioncontrol.Tab
|
import org.mozilla.fenix.home.Tab
|
||||||
import org.mozilla.fenix.home.sessioncontrol.TabCollection
|
|
||||||
|
|
||||||
class SaveCollectionListAdapter(
|
class SaveCollectionListAdapter(
|
||||||
private val interactor: CollectionCreationInteractor
|
private val interactor: CollectionCreationInteractor
|
||||||
|
|
|
@ -8,7 +8,7 @@ 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 mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
||||||
import org.mozilla.fenix.home.sessioncontrol.Tab
|
import org.mozilla.fenix.home.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)
|
this.toTab(context.components.publicSuffixList, selected, mediaState)
|
||||||
|
|
|
@ -7,8 +7,8 @@ package org.mozilla.fenix.ext
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.home.sessioncontrol.TabCollection
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -36,22 +36,20 @@ import androidx.transition.TransitionInflater
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.android.synthetic.main.fragment_home.*
|
import kotlinx.android.synthetic.main.fragment_home.*
|
||||||
import kotlinx.android.synthetic.main.fragment_home.view.*
|
import kotlinx.android.synthetic.main.fragment_home.view.*
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import mozilla.appservices.places.BookmarkRoot
|
import mozilla.appservices.places.BookmarkRoot
|
||||||
import mozilla.components.browser.menu.BrowserMenu
|
import mozilla.components.browser.menu.BrowserMenu
|
||||||
import mozilla.components.browser.session.Session
|
import mozilla.components.browser.session.Session
|
||||||
import mozilla.components.browser.session.SessionManager
|
import mozilla.components.browser.session.SessionManager
|
||||||
import mozilla.components.concept.engine.prompt.ShareData
|
|
||||||
import mozilla.components.concept.sync.AccountObserver
|
import mozilla.components.concept.sync.AccountObserver
|
||||||
import mozilla.components.concept.sync.AuthType
|
import mozilla.components.concept.sync.AuthType
|
||||||
import mozilla.components.concept.sync.OAuthAccount
|
import mozilla.components.concept.sync.OAuthAccount
|
||||||
import mozilla.components.feature.media.ext.getSession
|
import mozilla.components.feature.media.ext.getSession
|
||||||
import mozilla.components.feature.media.ext.pauseIfPlaying
|
|
||||||
import mozilla.components.feature.media.ext.playIfPaused
|
|
||||||
import mozilla.components.feature.media.state.MediaState
|
import mozilla.components.feature.media.state.MediaState
|
||||||
import mozilla.components.feature.media.state.MediaStateMachine
|
import mozilla.components.feature.media.state.MediaStateMachine
|
||||||
import mozilla.components.feature.tab.collections.TabCollection
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
|
@ -61,13 +59,12 @@ import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.START
|
||||||
import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.TOP
|
import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.TOP
|
||||||
import org.jetbrains.anko.constraint.layout.applyConstraintSet
|
import org.jetbrains.anko.constraint.layout.applyConstraintSet
|
||||||
import org.mozilla.fenix.BrowserDirection
|
import org.mozilla.fenix.BrowserDirection
|
||||||
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.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
|
||||||
|
import org.mozilla.fenix.components.StoreProvider
|
||||||
import org.mozilla.fenix.components.TabCollectionStorage
|
import org.mozilla.fenix.components.TabCollectionStorage
|
||||||
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
|
||||||
|
@ -78,20 +75,10 @@ import org.mozilla.fenix.ext.requireComponents
|
||||||
import org.mozilla.fenix.ext.sessionsOfType
|
import org.mozilla.fenix.ext.sessionsOfType
|
||||||
import org.mozilla.fenix.ext.settings
|
import org.mozilla.fenix.ext.settings
|
||||||
import org.mozilla.fenix.ext.toTab
|
import org.mozilla.fenix.ext.toTab
|
||||||
import org.mozilla.fenix.home.sessioncontrol.CollectionAction
|
import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
|
||||||
import org.mozilla.fenix.home.sessioncontrol.OnboardingAction
|
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
|
||||||
import org.mozilla.fenix.home.sessioncontrol.SessionControlAction
|
import org.mozilla.fenix.home.sessioncontrol.SessionControlView
|
||||||
import org.mozilla.fenix.home.sessioncontrol.SessionControlChange
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.SessionControlComponent
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.SessionControlState
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.SessionControlViewModel
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.Tab
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.TabAction
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
|
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
|
||||||
import org.mozilla.fenix.lib.Do
|
|
||||||
import org.mozilla.fenix.mvi.ActionBusFactory
|
|
||||||
import org.mozilla.fenix.mvi.getAutoDisposeObservable
|
|
||||||
import org.mozilla.fenix.mvi.getManagedEmitter
|
|
||||||
import org.mozilla.fenix.onboarding.FenixOnboarding
|
import org.mozilla.fenix.onboarding.FenixOnboarding
|
||||||
import org.mozilla.fenix.settings.SupportUtils
|
import org.mozilla.fenix.settings.SupportUtils
|
||||||
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
|
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
|
||||||
|
@ -100,11 +87,9 @@ import org.mozilla.fenix.utils.allowUndo
|
||||||
import org.mozilla.fenix.whatsnew.WhatsNew
|
import org.mozilla.fenix.whatsnew.WhatsNew
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||||
class HomeFragment : Fragment() {
|
class HomeFragment : Fragment() {
|
||||||
|
|
||||||
private val bus = ActionBusFactory.get(this)
|
|
||||||
|
|
||||||
private val browsingModeManager get() = (activity as HomeActivity).browsingModeManager
|
private val browsingModeManager get() = (activity as HomeActivity).browsingModeManager
|
||||||
|
|
||||||
private val singleSessionObserver = object : Session.Observer {
|
private val singleSessionObserver = object : Session.Observer {
|
||||||
|
@ -142,7 +127,9 @@ class HomeFragment : Fragment() {
|
||||||
data class PendingSessionDeletion(val deletionJob: (suspend () -> Unit), val sessionId: String)
|
data class PendingSessionDeletion(val deletionJob: (suspend () -> Unit), val sessionId: String)
|
||||||
|
|
||||||
private val onboarding by lazy { FenixOnboarding(requireContext()) }
|
private val onboarding by lazy { FenixOnboarding(requireContext()) }
|
||||||
private lateinit var sessionControlComponent: SessionControlComponent
|
private lateinit var homeFragmentStore: HomeFragmentStore
|
||||||
|
private lateinit var sessionControlInteractor: SessionControlInteractor
|
||||||
|
private lateinit var sessionControlView: SessionControlView
|
||||||
private lateinit var currentMode: CurrentMode
|
private lateinit var currentMode: CurrentMode
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
@ -169,34 +156,49 @@ class HomeFragment : Fragment() {
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
val view = inflater.inflate(R.layout.fragment_home, container, false)
|
val view = inflater.inflate(R.layout.fragment_home, container, false)
|
||||||
|
val activity = activity as HomeActivity
|
||||||
|
|
||||||
currentMode = CurrentMode(
|
currentMode = CurrentMode(
|
||||||
view.context,
|
view.context,
|
||||||
onboarding,
|
onboarding,
|
||||||
browsingModeManager,
|
browsingModeManager,
|
||||||
getManagedEmitter()
|
::dispatchModeChanges
|
||||||
)
|
)
|
||||||
|
|
||||||
sessionControlComponent = SessionControlComponent(
|
homeFragmentStore = StoreProvider.get(this) {
|
||||||
view.homeLayout,
|
HomeFragmentStore(
|
||||||
bus,
|
HomeFragmentState(
|
||||||
FenixViewModelProvider.create(
|
collections = requireComponents.core.tabCollectionStorage.cachedTabCollections,
|
||||||
this,
|
expandedCollections = emptySet(),
|
||||||
SessionControlViewModel::class.java
|
mode = currentMode.getCurrentMode(),
|
||||||
) {
|
tabs = emptyList()
|
||||||
SessionControlViewModel(
|
|
||||||
SessionControlState(
|
|
||||||
emptyList(),
|
|
||||||
emptySet(),
|
|
||||||
requireComponents.core.tabCollectionStorage.cachedTabCollections,
|
|
||||||
currentMode.getCurrentMode()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionControlInteractor = SessionControlInteractor(
|
||||||
|
DefaultSessionControlController(
|
||||||
|
activity = activity,
|
||||||
|
store = homeFragmentStore,
|
||||||
|
navController = findNavController(),
|
||||||
|
homeLayout = view.homeLayout,
|
||||||
|
browsingModeManager = browsingModeManager,
|
||||||
|
lifecycleScope = viewLifecycleOwner.lifecycleScope,
|
||||||
|
closeTab = ::closeTab,
|
||||||
|
closeAllTabs = ::closeAllTabs,
|
||||||
|
getListOfTabs = ::getListOfTabs,
|
||||||
|
hideOnboarding = ::hideOnboarding,
|
||||||
|
invokePendingDeleteJobs = ::invokePendingDeleteJobs,
|
||||||
|
registerCollectionStorageObserver = ::registerCollectionStorageObserver,
|
||||||
|
scrollToTheTop = ::scrollToTheTop,
|
||||||
|
showDeleteCollectionPrompt = ::showDeleteCollectionPrompt
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sessionControlView = SessionControlView(homeFragmentStore, view.homeLayout, sessionControlInteractor)
|
||||||
|
|
||||||
view.homeLayout.applyConstraintSet {
|
view.homeLayout.applyConstraintSet {
|
||||||
sessionControlComponent.view {
|
sessionControlView.view {
|
||||||
connect(
|
connect(
|
||||||
TOP to BOTTOM of view.wordmark_spacer,
|
TOP to BOTTOM of view.wordmark_spacer,
|
||||||
START to START of PARENT_ID,
|
START to START of PARENT_ID,
|
||||||
|
@ -206,13 +208,12 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ActionBusFactory.get(this).logMergedObservables()
|
|
||||||
val activity = activity as HomeActivity
|
|
||||||
activity.themeManager.applyStatusBarTheme(activity)
|
activity.themeManager.applyStatusBarTheme(activity)
|
||||||
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
@SuppressWarnings("LongMethod")
|
@SuppressWarnings("LongMethod")
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
@ -222,7 +223,7 @@ class HomeFragment : Fragment() {
|
||||||
ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652
|
ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652
|
||||||
}
|
}
|
||||||
homeViewModel.layoutManagerState?.also { parcelable ->
|
homeViewModel.layoutManagerState?.also { parcelable ->
|
||||||
sessionControlComponent.view.layoutManager?.onRestoreInstanceState(parcelable)
|
sessionControlView.view.layoutManager?.onRestoreInstanceState(parcelable)
|
||||||
}
|
}
|
||||||
homeLayout?.progress = homeViewModel.motionLayoutProgress
|
homeLayout?.progress = homeViewModel.motionLayoutProgress
|
||||||
homeViewModel.layoutManagerState = null
|
homeViewModel.layoutManagerState = null
|
||||||
|
@ -292,9 +293,8 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onboarding.userHasBeenOnboarded()) {
|
if (onboarding.userHasBeenOnboarded()) {
|
||||||
getManagedEmitter<SessionControlChange>().onNext(
|
homeFragmentStore.dispatch(
|
||||||
SessionControlChange.ModeChange(Mode.fromBrowsingMode(newMode))
|
HomeFragmentAction.ModeChange(Mode.fromBrowsingMode(newMode)))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,25 +311,14 @@ class HomeFragment : Fragment() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
subscribeToTabCollections()
|
subscribeToTabCollections()
|
||||||
|
|
||||||
getAutoDisposeObservable<SessionControlAction>()
|
|
||||||
.subscribe {
|
|
||||||
when (it) {
|
|
||||||
is SessionControlAction.Tab -> handleTabAction(it.action)
|
|
||||||
is SessionControlAction.Collection -> handleCollectionAction(it.action)
|
|
||||||
is SessionControlAction.Onboarding -> handleOnboardingAction(it.action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val context = requireContext()
|
val context = requireContext()
|
||||||
val components = context.components
|
val components = context.components
|
||||||
|
|
||||||
getManagedEmitter<SessionControlChange>().onNext(
|
homeFragmentStore.dispatch(HomeFragmentAction.Change(
|
||||||
SessionControlChange.Change(
|
collections = components.core.tabCollectionStorage.cachedTabCollections,
|
||||||
tabs = getListOfSessions().toTabs(),
|
mode = currentMode.getCurrentMode(),
|
||||||
mode = currentMode.getCurrentMode(),
|
tabs = getListOfSessions().toTabs()
|
||||||
collections = components.core.tabCollectionStorage.cachedTabCollections
|
))
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
hideToolbar()
|
hideToolbar()
|
||||||
|
|
||||||
|
@ -357,103 +346,46 @@ class HomeFragment : Fragment() {
|
||||||
requireComponents.core.tabCollectionStorage.unregister(collectionStorageObserver)
|
requireComponents.core.tabCollectionStorage.unregister(collectionStorageObserver)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleOnboardingAction(action: OnboardingAction) {
|
private fun closeTab(sessionId: String) {
|
||||||
Do exhaustive when (action) {
|
val deletionJob = pendingSessionDeletion?.deletionJob
|
||||||
is OnboardingAction.Finish -> {
|
|
||||||
homeLayout?.progress = 0F
|
if (deletionJob == null) {
|
||||||
hideOnboarding()
|
removeTabWithUndo(sessionId, browsingModeManager.mode.isPrivate)
|
||||||
|
} else {
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
deletionJob.invoke()
|
||||||
|
}.invokeOnCompletion {
|
||||||
|
pendingSessionDeletion = null
|
||||||
|
removeTabWithUndo(sessionId, browsingModeManager.mode.isPrivate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("ComplexMethod", "LongMethod")
|
private fun closeAllTabs(isPrivateMode: Boolean) {
|
||||||
private fun handleTabAction(action: TabAction) {
|
val deletionJob = pendingSessionDeletion?.deletionJob
|
||||||
Do exhaustive when (action) {
|
|
||||||
is TabAction.SaveTabGroup -> {
|
if (deletionJob == null) {
|
||||||
if (browsingModeManager.mode.isPrivate) return
|
removeAllTabsWithUndo(
|
||||||
invokePendingDeleteJobs()
|
sessionManager.sessionsOfType(private = isPrivateMode),
|
||||||
saveTabToCollection(action.selectedTabSessionId)
|
isPrivateMode
|
||||||
}
|
)
|
||||||
is TabAction.Select -> {
|
} else {
|
||||||
invokePendingDeleteJobs()
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
val session = sessionManager.findSessionById(action.sessionId)
|
deletionJob.invoke()
|
||||||
sessionManager.select(session!!)
|
}.invokeOnCompletion {
|
||||||
val directions = HomeFragmentDirections.actionHomeFragmentToBrowserFragment(null)
|
pendingSessionDeletion = null
|
||||||
val extras =
|
removeAllTabsWithUndo(
|
||||||
FragmentNavigator.Extras.Builder()
|
sessionManager.sessionsOfType(private = isPrivateMode),
|
||||||
.addSharedElement(
|
isPrivateMode
|
||||||
action.tabView,
|
|
||||||
"$TAB_ITEM_TRANSITION_NAME${action.sessionId}"
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
nav(R.id.homeFragment, directions, extras)
|
|
||||||
}
|
|
||||||
is TabAction.Close -> {
|
|
||||||
if (pendingSessionDeletion?.deletionJob == null) {
|
|
||||||
removeTabWithUndo(action.sessionId, browsingModeManager.mode.isPrivate)
|
|
||||||
} else {
|
|
||||||
pendingSessionDeletion?.deletionJob?.let {
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
it.invoke()
|
|
||||||
}.invokeOnCompletion {
|
|
||||||
pendingSessionDeletion = null
|
|
||||||
removeTabWithUndo(action.sessionId, browsingModeManager.mode.isPrivate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is TabAction.Share -> {
|
|
||||||
invokePendingDeleteJobs()
|
|
||||||
sessionManager.findSessionById(action.sessionId)?.let { session ->
|
|
||||||
share(listOf(ShareData(url = session.url)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is TabAction.PauseMedia -> {
|
|
||||||
MediaStateMachine.state.pauseIfPlaying()
|
|
||||||
}
|
|
||||||
is TabAction.PlayMedia -> {
|
|
||||||
MediaStateMachine.state.playIfPaused()
|
|
||||||
}
|
|
||||||
is TabAction.CloseAll -> {
|
|
||||||
if (pendingSessionDeletion?.deletionJob == null) {
|
|
||||||
removeAllTabsWithUndo(
|
|
||||||
sessionManager.sessionsOfType(private = action.private),
|
|
||||||
action.private
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
pendingSessionDeletion?.deletionJob?.let {
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
it.invoke()
|
|
||||||
}.invokeOnCompletion {
|
|
||||||
pendingSessionDeletion = null
|
|
||||||
removeAllTabsWithUndo(
|
|
||||||
sessionManager.sessionsOfType(private = action.private),
|
|
||||||
action.private
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is TabAction.PrivateBrowsingLearnMore -> {
|
|
||||||
(activity as HomeActivity).openToBrowserAndLoad(
|
|
||||||
searchTermOrURL = SupportUtils.getGenericSumoURLForTopic
|
|
||||||
(SupportUtils.SumoTopic.PRIVATE_BROWSING_MYTHS),
|
|
||||||
newTab = true,
|
|
||||||
from = BrowserDirection.FromHome
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is TabAction.ShareTabs -> {
|
|
||||||
invokePendingDeleteJobs()
|
|
||||||
val shareData = sessionManager
|
|
||||||
.sessionsOfType(private = browsingModeManager.mode.isPrivate)
|
|
||||||
.map { ShareData(url = it.url, title = it.title) }
|
|
||||||
.toList()
|
|
||||||
share(shareData)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun dispatchModeChanges(mode: Mode) {
|
||||||
|
homeFragmentStore.dispatch(HomeFragmentAction.ModeChange(mode))
|
||||||
|
}
|
||||||
|
|
||||||
private fun invokePendingDeleteJobs() {
|
private fun invokePendingDeleteJobs() {
|
||||||
pendingSessionDeletion?.deletionJob?.let {
|
pendingSessionDeletion?.deletionJob?.let {
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
@ -472,7 +404,7 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createDeleteCollectionPrompt(tabCollection: TabCollection) {
|
private fun showDeleteCollectionPrompt(tabCollection: TabCollection) {
|
||||||
val context = context ?: return
|
val context = context ?: return
|
||||||
AlertDialog.Builder(context).apply {
|
AlertDialog.Builder(context).apply {
|
||||||
val message =
|
val message =
|
||||||
|
@ -493,107 +425,6 @@ class HomeFragment : Fragment() {
|
||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("LongMethod")
|
|
||||||
private fun handleCollectionAction(action: CollectionAction) {
|
|
||||||
when (action) {
|
|
||||||
is CollectionAction.Expand -> {
|
|
||||||
getManagedEmitter<SessionControlChange>()
|
|
||||||
.onNext(SessionControlChange.ExpansionChange(action.collection, true))
|
|
||||||
}
|
|
||||||
is CollectionAction.Collapse -> {
|
|
||||||
getManagedEmitter<SessionControlChange>()
|
|
||||||
.onNext(SessionControlChange.ExpansionChange(action.collection, false))
|
|
||||||
}
|
|
||||||
is CollectionAction.Delete -> {
|
|
||||||
createDeleteCollectionPrompt(action.collection)
|
|
||||||
}
|
|
||||||
is CollectionAction.AddTab -> {
|
|
||||||
requireComponents.analytics.metrics.track(Event.CollectionAddTabPressed)
|
|
||||||
showCollectionCreationFragment(
|
|
||||||
step = SaveCollectionStep.SelectTabs,
|
|
||||||
selectedTabCollectionId = action.collection.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is CollectionAction.Rename -> {
|
|
||||||
showCollectionCreationFragment(
|
|
||||||
step = SaveCollectionStep.RenameCollection,
|
|
||||||
selectedTabCollectionId = action.collection.id
|
|
||||||
)
|
|
||||||
requireComponents.analytics.metrics.track(Event.CollectionRenamePressed)
|
|
||||||
}
|
|
||||||
is CollectionAction.OpenTab -> {
|
|
||||||
invokePendingDeleteJobs()
|
|
||||||
|
|
||||||
val context = requireContext()
|
|
||||||
val components = context.components
|
|
||||||
|
|
||||||
val session = action.tab.restore(
|
|
||||||
context = context,
|
|
||||||
engine = components.core.engine,
|
|
||||||
tab = action.tab,
|
|
||||||
restoreSessionId = false
|
|
||||||
)
|
|
||||||
if (session == null) {
|
|
||||||
// We were unable to create a snapshot, so just load the tab instead
|
|
||||||
(activity as HomeActivity).openToBrowserAndLoad(
|
|
||||||
searchTermOrURL = action.tab.url,
|
|
||||||
newTab = true,
|
|
||||||
from = BrowserDirection.FromHome
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
components.core.sessionManager.add(
|
|
||||||
session,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
(activity as HomeActivity).openToBrowser(BrowserDirection.FromHome)
|
|
||||||
}
|
|
||||||
components.analytics.metrics.track(Event.CollectionTabRestored)
|
|
||||||
}
|
|
||||||
is CollectionAction.OpenTabs -> {
|
|
||||||
invokePendingDeleteJobs()
|
|
||||||
|
|
||||||
val context = requireContext()
|
|
||||||
val components = context.components
|
|
||||||
|
|
||||||
action.collection.tabs.reversed().forEach {
|
|
||||||
val session = it.restore(
|
|
||||||
context = context,
|
|
||||||
engine = components.core.engine,
|
|
||||||
tab = it,
|
|
||||||
restoreSessionId = false
|
|
||||||
)
|
|
||||||
if (session == null) {
|
|
||||||
// We were unable to create a snapshot, so just load the tab instead
|
|
||||||
components.useCases.tabsUseCases.addTab.invoke(it.url)
|
|
||||||
} else {
|
|
||||||
components.core.sessionManager.add(
|
|
||||||
session,
|
|
||||||
context.components.core.sessionManager.selectedSession == null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch(Main) {
|
|
||||||
delay(ANIM_SCROLL_DELAY)
|
|
||||||
sessionControlComponent.view.smoothScrollToPosition(0)
|
|
||||||
}
|
|
||||||
components.analytics.metrics.track(Event.CollectionAllTabsRestored)
|
|
||||||
}
|
|
||||||
is CollectionAction.ShareTabs -> {
|
|
||||||
share(action.collection.tabs.map { ShareData(url = it.url, title = it.title) })
|
|
||||||
requireComponents.analytics.metrics.track(Event.CollectionShared)
|
|
||||||
}
|
|
||||||
is CollectionAction.RemoveTab -> {
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch(IO) {
|
|
||||||
requireComponents.core.tabCollectionStorage.removeTabFromCollection(
|
|
||||||
action.collection,
|
|
||||||
action.tab
|
|
||||||
)
|
|
||||||
}
|
|
||||||
requireComponents.analytics.metrics.track(Event.CollectionTabRemoved)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
invokePendingDeleteJobs()
|
invokePendingDeleteJobs()
|
||||||
super.onStop()
|
super.onStop()
|
||||||
|
@ -601,7 +432,7 @@ class HomeFragment : Fragment() {
|
||||||
ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652
|
ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652
|
||||||
}
|
}
|
||||||
homeViewModel.layoutManagerState =
|
homeViewModel.layoutManagerState =
|
||||||
sessionControlComponent.view.layoutManager?.onSaveInstanceState()
|
sessionControlView.view.layoutManager?.onSaveInstanceState()
|
||||||
homeViewModel.motionLayoutProgress = homeLayout?.progress ?: 0F
|
homeViewModel.motionLayoutProgress = homeLayout?.progress ?: 0F
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -725,20 +556,14 @@ class HomeFragment : Fragment() {
|
||||||
private fun subscribeToTabCollections(): Observer<List<TabCollection>> {
|
private fun subscribeToTabCollections(): Observer<List<TabCollection>> {
|
||||||
return Observer<List<TabCollection>> {
|
return Observer<List<TabCollection>> {
|
||||||
requireComponents.core.tabCollectionStorage.cachedTabCollections = it
|
requireComponents.core.tabCollectionStorage.cachedTabCollections = it
|
||||||
getManagedEmitter<SessionControlChange>().onNext(
|
homeFragmentStore.dispatch(HomeFragmentAction.CollectionsChange(it))
|
||||||
SessionControlChange.CollectionsChange(
|
|
||||||
it
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}.also { observer ->
|
}.also { observer ->
|
||||||
requireComponents.core.tabCollectionStorage.getCollections().observe(this, observer)
|
requireComponents.core.tabCollectionStorage.getCollections().observe(this, observer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeAllTabsWithUndo(listOfSessionsToDelete: Sequence<Session>, private: Boolean) {
|
private fun removeAllTabsWithUndo(listOfSessionsToDelete: Sequence<Session>, private: Boolean) {
|
||||||
val sessionManager = requireComponents.core.sessionManager
|
homeFragmentStore.dispatch(HomeFragmentAction.TabsChange(emptyList()))
|
||||||
|
|
||||||
getManagedEmitter<SessionControlChange>().onNext(SessionControlChange.TabsChange(listOf()))
|
|
||||||
|
|
||||||
val deleteOperation: (suspend () -> Unit) = {
|
val deleteOperation: (suspend () -> Unit) = {
|
||||||
listOfSessionsToDelete.forEach {
|
listOfSessionsToDelete.forEach {
|
||||||
|
@ -802,11 +627,7 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun emitSessionChanges() {
|
private fun emitSessionChanges() {
|
||||||
getManagedEmitter<SessionControlChange>().onNext(
|
homeFragmentStore.dispatch(HomeFragmentAction.TabsChange(getListOfTabs()))
|
||||||
SessionControlChange.TabsChange(
|
|
||||||
getListOfSessions().toTabs()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getListOfSessions(): List<Session> {
|
private fun getListOfSessions(): List<Session> {
|
||||||
|
@ -815,48 +636,19 @@ class HomeFragment : Fragment() {
|
||||||
.toList()
|
.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showCollectionCreationFragment(
|
private fun getListOfTabs(): List<Tab> {
|
||||||
step: SaveCollectionStep,
|
return getListOfSessions().toTabs()
|
||||||
selectedTabIds: Array<String>? = null,
|
|
||||||
selectedTabCollectionId: Long? = null
|
|
||||||
) {
|
|
||||||
if (findNavController().currentDestination?.id == R.id.collectionCreationFragment) return
|
|
||||||
|
|
||||||
val storage = requireComponents.core.tabCollectionStorage
|
|
||||||
// Only register the observer right before moving to collection creation
|
|
||||||
storage.register(collectionStorageObserver, this)
|
|
||||||
|
|
||||||
val tabIds = getListOfSessions().toTabs().map { it.sessionId }.toTypedArray()
|
|
||||||
view?.let {
|
|
||||||
val directions = HomeFragmentDirections.actionHomeFragmentToCreateCollectionFragment(
|
|
||||||
tabIds = tabIds,
|
|
||||||
previousFragmentId = R.id.homeFragment,
|
|
||||||
saveCollectionStep = step,
|
|
||||||
selectedTabIds = selectedTabIds,
|
|
||||||
selectedTabCollectionId = selectedTabCollectionId ?: -1
|
|
||||||
)
|
|
||||||
nav(R.id.homeFragment, directions)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveTabToCollection(selectedTabId: String?) {
|
private fun registerCollectionStorageObserver() {
|
||||||
val tabs = getListOfSessions().toTabs()
|
requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this)
|
||||||
val storage = requireComponents.core.tabCollectionStorage
|
|
||||||
|
|
||||||
val step = when {
|
|
||||||
tabs.size > 1 -> SaveCollectionStep.SelectTabs
|
|
||||||
storage.cachedTabCollections.isNotEmpty() -> SaveCollectionStep.SelectCollection
|
|
||||||
else -> SaveCollectionStep.NameCollection
|
|
||||||
}
|
|
||||||
|
|
||||||
showCollectionCreationFragment(step, selectedTabId?.let { arrayOf(it) })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun share(data: List<ShareData>) {
|
private fun scrollToTheTop() {
|
||||||
val directions = HomeFragmentDirections.actionHomeFragmentToShareFragment(
|
lifecycleScope.launch(Main) {
|
||||||
data = data.toTypedArray()
|
delay(ANIM_SCROLL_DELAY)
|
||||||
)
|
sessionControlView.view.smoothScrollToPosition(0)
|
||||||
nav(R.id.homeFragment, directions)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scrollAndAnimateCollection(
|
private fun scrollAndAnimateCollection(
|
||||||
|
@ -865,7 +657,7 @@ class HomeFragment : Fragment() {
|
||||||
) {
|
) {
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
val recyclerView = sessionControlComponent.view
|
val recyclerView = sessionControlView.view
|
||||||
delay(ANIM_SCROLL_DELAY)
|
delay(ANIM_SCROLL_DELAY)
|
||||||
val tabsSize = getListOfSessions().size
|
val tabsSize = getListOfSessions().size
|
||||||
|
|
||||||
|
@ -908,7 +700,7 @@ class HomeFragment : Fragment() {
|
||||||
private fun animateCollection(addedTabsSize: Int, indexOfCollection: Int) {
|
private fun animateCollection(addedTabsSize: Int, indexOfCollection: Int) {
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
val viewHolder =
|
val viewHolder =
|
||||||
sessionControlComponent.view.findViewHolderForAdapterPosition(indexOfCollection)
|
sessionControlView.view.findViewHolderForAdapterPosition(indexOfCollection)
|
||||||
val border =
|
val border =
|
||||||
(viewHolder as? CollectionViewHolder)?.view?.findViewById<View>(R.id.selected_border)
|
(viewHolder as? CollectionViewHolder)?.view?.findViewById<View>(R.id.selected_border)
|
||||||
val listener = object : Animator.AnimatorListener {
|
val listener = object : Animator.AnimatorListener {
|
||||||
|
@ -982,7 +774,6 @@ class HomeFragment : Fragment() {
|
||||||
private const val FADE_ANIM_DURATION = 150L
|
private const val FADE_ANIM_DURATION = 150L
|
||||||
private const val ANIM_SNACKBAR_DELAY = 100L
|
private const val ANIM_SNACKBAR_DELAY = 100L
|
||||||
private const val SHARED_TRANSITION_MS = 200L
|
private const val SHARED_TRANSITION_MS = 200L
|
||||||
private const val TAB_ITEM_TRANSITION_NAME = "tab_item"
|
|
||||||
private const val CFR_WIDTH_DIVIDER = 1.7
|
private const val CFR_WIDTH_DIVIDER = 1.7
|
||||||
private const val CFR_Y_OFFSET = -20
|
private const val CFR_Y_OFFSET = -20
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
/* 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.home
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import mozilla.components.browser.session.Session
|
||||||
|
import mozilla.components.browser.session.SessionManager
|
||||||
|
import mozilla.components.feature.media.state.MediaState
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [Store] for holding the [HomeFragmentState] and applying [HomeFragmentAction]s.
|
||||||
|
*/
|
||||||
|
class HomeFragmentStore(
|
||||||
|
initialState: HomeFragmentState
|
||||||
|
) : Store<HomeFragmentState, HomeFragmentAction>(
|
||||||
|
initialState, ::homeFragmentStateReducer
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Tab(
|
||||||
|
val sessionId: String,
|
||||||
|
val url: String,
|
||||||
|
val hostname: String,
|
||||||
|
val title: String,
|
||||||
|
val selected: Boolean? = null,
|
||||||
|
var mediaState: MediaState? = null,
|
||||||
|
val icon: Bitmap? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
fun List<Tab>.toSessionBundle(sessionManager: SessionManager): List<Session> {
|
||||||
|
return this.mapNotNull { sessionManager.findSessionById(it.sessionId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state for the [HomeFragment].
|
||||||
|
*
|
||||||
|
* @property collections The list of [TabCollection] to display in the [HomeFragment].
|
||||||
|
* @property expandedCollections A set containing the ids of the [TabCollection] that are expanded
|
||||||
|
* in the [HomeFragment].
|
||||||
|
* @property mode The state of the [HomeFragment] UI.
|
||||||
|
* @property tabs The list of opened [Tab] in the [HomeFragment].
|
||||||
|
*/
|
||||||
|
data class HomeFragmentState(
|
||||||
|
val collections: List<TabCollection>,
|
||||||
|
val expandedCollections: Set<Long>,
|
||||||
|
val mode: Mode,
|
||||||
|
val tabs: List<Tab>
|
||||||
|
) : State
|
||||||
|
|
||||||
|
sealed class HomeFragmentAction : Action {
|
||||||
|
data class Change(val tabs: List<Tab>, val mode: Mode, val collections: List<TabCollection>) :
|
||||||
|
HomeFragmentAction()
|
||||||
|
data class CollectionExpanded(val collection: TabCollection, val expand: Boolean) : HomeFragmentAction()
|
||||||
|
data class CollectionsChange(val collections: List<TabCollection>) : HomeFragmentAction()
|
||||||
|
data class ModeChange(val mode: Mode) : HomeFragmentAction()
|
||||||
|
data class TabsChange(val tabs: List<Tab>) : HomeFragmentAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun homeFragmentStateReducer(
|
||||||
|
state: HomeFragmentState,
|
||||||
|
action: HomeFragmentAction
|
||||||
|
): HomeFragmentState {
|
||||||
|
return when (action) {
|
||||||
|
is HomeFragmentAction.Change -> state.copy(
|
||||||
|
collections = action.collections,
|
||||||
|
mode = action.mode,
|
||||||
|
tabs = action.tabs
|
||||||
|
)
|
||||||
|
is HomeFragmentAction.CollectionExpanded -> {
|
||||||
|
val newExpandedCollection = state.expandedCollections.toMutableSet()
|
||||||
|
|
||||||
|
if (action.expand) {
|
||||||
|
newExpandedCollection.add(action.collection.id)
|
||||||
|
} else {
|
||||||
|
newExpandedCollection.remove(action.collection.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.copy(expandedCollections = newExpandedCollection)
|
||||||
|
}
|
||||||
|
is HomeFragmentAction.CollectionsChange -> state.copy(collections = action.collections)
|
||||||
|
is HomeFragmentAction.ModeChange -> state.copy(mode = action.mode, tabs = emptyList())
|
||||||
|
is HomeFragmentAction.TabsChange -> state.copy(tabs = action.tabs)
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,6 @@
|
||||||
package org.mozilla.fenix.home
|
package org.mozilla.fenix.home
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import io.reactivex.Observer
|
|
||||||
import mozilla.components.concept.sync.AccountObserver
|
import mozilla.components.concept.sync.AccountObserver
|
||||||
import mozilla.components.concept.sync.AuthType
|
import mozilla.components.concept.sync.AuthType
|
||||||
import mozilla.components.concept.sync.OAuthAccount
|
import mozilla.components.concept.sync.OAuthAccount
|
||||||
|
@ -14,7 +13,6 @@ import mozilla.components.service.fxa.sharing.ShareableAccount
|
||||||
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.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.home.sessioncontrol.SessionControlChange
|
|
||||||
import org.mozilla.fenix.onboarding.FenixOnboarding
|
import org.mozilla.fenix.onboarding.FenixOnboarding
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -49,7 +47,7 @@ class CurrentMode(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val onboarding: FenixOnboarding,
|
private val onboarding: FenixOnboarding,
|
||||||
private val browsingModeManager: BrowsingModeManager,
|
private val browsingModeManager: BrowsingModeManager,
|
||||||
private val emitter: Observer<SessionControlChange>
|
private val dispatchModeChanges: (mode: Mode) -> Unit
|
||||||
) : AccountObserver {
|
) : AccountObserver {
|
||||||
|
|
||||||
private val accountManager = context.components.backgroundServices.accountManager
|
private val accountManager = context.components.backgroundServices.accountManager
|
||||||
|
@ -71,7 +69,7 @@ class CurrentMode(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun emitModeChanges() {
|
fun emitModeChanges() {
|
||||||
emitter.onNext(SessionControlChange.ModeChange(getCurrentMode()))
|
dispatchModeChanges(getCurrentMode())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) = emitModeChanges()
|
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) = emitModeChanges()
|
||||||
|
|
|
@ -14,10 +14,11 @@ import androidx.annotation.StringRes
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.reactivex.Observer
|
|
||||||
import kotlinx.android.synthetic.main.tab_list_row.*
|
import kotlinx.android.synthetic.main.tab_list_row.*
|
||||||
import mozilla.components.feature.media.state.MediaState
|
import mozilla.components.feature.media.state.MediaState
|
||||||
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
import org.mozilla.fenix.home.OnboardingState
|
import org.mozilla.fenix.home.OnboardingState
|
||||||
|
import org.mozilla.fenix.home.Tab
|
||||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHolder
|
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHolder
|
||||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
|
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
|
||||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.NoContentMessageViewHolder
|
import org.mozilla.fenix.home.sessioncontrol.viewholders.NoContentMessageViewHolder
|
||||||
|
@ -139,7 +140,7 @@ class AdapterItemDiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
class SessionControlAdapter(
|
class SessionControlAdapter(
|
||||||
private val actionEmitter: Observer<SessionControlAction>
|
private val interactor: SessionControlInteractor
|
||||||
) : ListAdapter<AdapterItem, RecyclerView.ViewHolder>(AdapterItemDiffCallback()) {
|
) : ListAdapter<AdapterItem, RecyclerView.ViewHolder>(AdapterItemDiffCallback()) {
|
||||||
|
|
||||||
// This method triggers the ComplexMethod lint error when in fact it's quite simple.
|
// This method triggers the ComplexMethod lint error when in fact it's quite simple.
|
||||||
|
@ -147,14 +148,14 @@ class SessionControlAdapter(
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
||||||
return when (viewType) {
|
return when (viewType) {
|
||||||
TabHeaderViewHolder.LAYOUT_ID -> TabHeaderViewHolder(view, actionEmitter)
|
TabHeaderViewHolder.LAYOUT_ID -> TabHeaderViewHolder(view, interactor)
|
||||||
TabViewHolder.LAYOUT_ID -> TabViewHolder(view, actionEmitter)
|
TabViewHolder.LAYOUT_ID -> TabViewHolder(view, interactor)
|
||||||
SaveTabGroupViewHolder.LAYOUT_ID -> SaveTabGroupViewHolder(view, actionEmitter)
|
SaveTabGroupViewHolder.LAYOUT_ID -> SaveTabGroupViewHolder(view, interactor)
|
||||||
PrivateBrowsingDescriptionViewHolder.LAYOUT_ID -> PrivateBrowsingDescriptionViewHolder(view, actionEmitter)
|
PrivateBrowsingDescriptionViewHolder.LAYOUT_ID -> PrivateBrowsingDescriptionViewHolder(view, interactor)
|
||||||
NoContentMessageViewHolder.LAYOUT_ID -> NoContentMessageViewHolder(view)
|
NoContentMessageViewHolder.LAYOUT_ID -> NoContentMessageViewHolder(view)
|
||||||
CollectionHeaderViewHolder.LAYOUT_ID -> CollectionHeaderViewHolder(view)
|
CollectionHeaderViewHolder.LAYOUT_ID -> CollectionHeaderViewHolder(view)
|
||||||
CollectionViewHolder.LAYOUT_ID -> CollectionViewHolder(view, actionEmitter)
|
CollectionViewHolder.LAYOUT_ID -> CollectionViewHolder(view, interactor)
|
||||||
TabInCollectionViewHolder.LAYOUT_ID -> TabInCollectionViewHolder(view, actionEmitter)
|
TabInCollectionViewHolder.LAYOUT_ID -> TabInCollectionViewHolder(view, interactor)
|
||||||
OnboardingHeaderViewHolder.LAYOUT_ID -> OnboardingHeaderViewHolder(view)
|
OnboardingHeaderViewHolder.LAYOUT_ID -> OnboardingHeaderViewHolder(view)
|
||||||
OnboardingSectionHeaderViewHolder.LAYOUT_ID -> OnboardingSectionHeaderViewHolder(view)
|
OnboardingSectionHeaderViewHolder.LAYOUT_ID -> OnboardingSectionHeaderViewHolder(view)
|
||||||
OnboardingAutomaticSignInViewHolder.LAYOUT_ID -> OnboardingAutomaticSignInViewHolder(view)
|
OnboardingAutomaticSignInViewHolder.LAYOUT_ID -> OnboardingAutomaticSignInViewHolder(view)
|
||||||
|
@ -163,7 +164,7 @@ class SessionControlAdapter(
|
||||||
OnboardingTrackingProtectionViewHolder.LAYOUT_ID -> OnboardingTrackingProtectionViewHolder(view)
|
OnboardingTrackingProtectionViewHolder.LAYOUT_ID -> OnboardingTrackingProtectionViewHolder(view)
|
||||||
OnboardingPrivateBrowsingViewHolder.LAYOUT_ID -> OnboardingPrivateBrowsingViewHolder(view)
|
OnboardingPrivateBrowsingViewHolder.LAYOUT_ID -> OnboardingPrivateBrowsingViewHolder(view)
|
||||||
OnboardingPrivacyNoticeViewHolder.LAYOUT_ID -> OnboardingPrivacyNoticeViewHolder(view)
|
OnboardingPrivacyNoticeViewHolder.LAYOUT_ID -> OnboardingPrivacyNoticeViewHolder(view)
|
||||||
OnboardingFinishViewHolder.LAYOUT_ID -> OnboardingFinishViewHolder(view, actionEmitter)
|
OnboardingFinishViewHolder.LAYOUT_ID -> OnboardingFinishViewHolder(view, interactor)
|
||||||
else -> throw IllegalStateException()
|
else -> throw IllegalStateException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,164 +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.home.sessioncontrol
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import io.reactivex.Observer
|
|
||||||
import mozilla.components.browser.session.Session
|
|
||||||
import mozilla.components.browser.session.SessionManager
|
|
||||||
import mozilla.components.feature.media.state.MediaState
|
|
||||||
import org.mozilla.fenix.ext.components
|
|
||||||
import org.mozilla.fenix.home.Mode
|
|
||||||
import org.mozilla.fenix.mvi.Action
|
|
||||||
import org.mozilla.fenix.mvi.ActionBusFactory
|
|
||||||
import org.mozilla.fenix.mvi.Change
|
|
||||||
import org.mozilla.fenix.mvi.UIComponent
|
|
||||||
import org.mozilla.fenix.mvi.UIComponentViewModelBase
|
|
||||||
import org.mozilla.fenix.mvi.UIComponentViewModelProvider
|
|
||||||
import org.mozilla.fenix.mvi.ViewState
|
|
||||||
import mozilla.components.feature.tab.collections.Tab as ComponentTab
|
|
||||||
import mozilla.components.feature.tab.collections.TabCollection as ACTabCollection
|
|
||||||
|
|
||||||
class SessionControlComponent(
|
|
||||||
private val container: ViewGroup,
|
|
||||||
bus: ActionBusFactory,
|
|
||||||
viewModelProvider: UIComponentViewModelProvider<SessionControlState, SessionControlChange>
|
|
||||||
) :
|
|
||||||
UIComponent<SessionControlState, SessionControlAction, SessionControlChange>(
|
|
||||||
bus.getManagedEmitter(SessionControlAction::class.java),
|
|
||||||
bus.getSafeManagedObservable(SessionControlChange::class.java),
|
|
||||||
viewModelProvider
|
|
||||||
) {
|
|
||||||
|
|
||||||
override fun initView() = SessionControlUIView(container, actionEmitter, changesObservable)
|
|
||||||
|
|
||||||
val view: RecyclerView
|
|
||||||
get() = uiView.view as RecyclerView
|
|
||||||
|
|
||||||
init {
|
|
||||||
bind()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Tab(
|
|
||||||
val sessionId: String,
|
|
||||||
val url: String,
|
|
||||||
val hostname: String,
|
|
||||||
val title: String,
|
|
||||||
val selected: Boolean? = null,
|
|
||||||
var mediaState: MediaState? = null,
|
|
||||||
val icon: Bitmap? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
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>()
|
|
||||||
this.forEach {
|
|
||||||
sessionManager.findSessionById(it.sessionId)?.let { session ->
|
|
||||||
sessionBundle.add(session)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sessionBundle
|
|
||||||
}
|
|
||||||
|
|
||||||
data class SessionControlState(
|
|
||||||
val tabs: List<Tab>,
|
|
||||||
val expandedCollections: Set<Long>,
|
|
||||||
val collections: List<TabCollection>,
|
|
||||||
val mode: Mode
|
|
||||||
) : ViewState
|
|
||||||
|
|
||||||
typealias TabCollection = ACTabCollection
|
|
||||||
|
|
||||||
sealed class TabAction : Action {
|
|
||||||
data class SaveTabGroup(val selectedTabSessionId: String?) : TabAction()
|
|
||||||
object ShareTabs : TabAction()
|
|
||||||
data class CloseAll(val private: Boolean) : TabAction()
|
|
||||||
data class Select(val tabView: View, val sessionId: String) : TabAction()
|
|
||||||
data class Close(val sessionId: String) : TabAction()
|
|
||||||
data class Share(val sessionId: String) : TabAction()
|
|
||||||
data class PauseMedia(val sessionId: String) : TabAction()
|
|
||||||
data class PlayMedia(val sessionId: String) : TabAction()
|
|
||||||
object PrivateBrowsingLearnMore : TabAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class CollectionAction : Action {
|
|
||||||
data class Expand(val collection: TabCollection) : CollectionAction()
|
|
||||||
data class Collapse(val collection: TabCollection) : CollectionAction()
|
|
||||||
data class Delete(val collection: TabCollection) : CollectionAction()
|
|
||||||
data class AddTab(val collection: TabCollection) : CollectionAction()
|
|
||||||
data class Rename(val collection: TabCollection) : CollectionAction()
|
|
||||||
data class OpenTab(val tab: ComponentTab) : CollectionAction()
|
|
||||||
data class OpenTabs(val collection: TabCollection) : CollectionAction()
|
|
||||||
data class ShareTabs(val collection: TabCollection) : CollectionAction()
|
|
||||||
data class RemoveTab(val collection: TabCollection, val tab: ComponentTab) : CollectionAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class OnboardingAction : Action {
|
|
||||||
object Finish : OnboardingAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class SessionControlAction : Action {
|
|
||||||
data class Tab(val action: TabAction) : SessionControlAction()
|
|
||||||
data class Collection(val action: CollectionAction) : SessionControlAction()
|
|
||||||
data class Onboarding(val action: OnboardingAction) : SessionControlAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Observer<SessionControlAction>.onNext(tabAction: TabAction) {
|
|
||||||
onNext(SessionControlAction.Tab(tabAction))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Observer<SessionControlAction>.onNext(collectionAction: CollectionAction) {
|
|
||||||
onNext(SessionControlAction.Collection(collectionAction))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Observer<SessionControlAction>.onNext(onboardingAction: OnboardingAction) {
|
|
||||||
onNext(SessionControlAction.Onboarding(onboardingAction))
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class SessionControlChange : Change {
|
|
||||||
data class Change(val tabs: List<Tab>, val mode: Mode, val collections: List<TabCollection>) :
|
|
||||||
SessionControlChange()
|
|
||||||
data class TabsChange(val tabs: List<Tab>) : SessionControlChange()
|
|
||||||
data class ModeChange(val mode: Mode) : SessionControlChange()
|
|
||||||
data class CollectionsChange(val collections: List<TabCollection>) : SessionControlChange()
|
|
||||||
data class ExpansionChange(val collection: TabCollection, val expand: Boolean) : SessionControlChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
class SessionControlViewModel(
|
|
||||||
initialState: SessionControlState
|
|
||||||
) : UIComponentViewModelBase<SessionControlState, SessionControlChange>(initialState, reducer) {
|
|
||||||
companion object {
|
|
||||||
val reducer: (SessionControlState, SessionControlChange) -> SessionControlState = { state, change ->
|
|
||||||
when (change) {
|
|
||||||
is SessionControlChange.CollectionsChange -> state.copy(collections = change.collections)
|
|
||||||
is SessionControlChange.TabsChange -> state.copy(tabs = change.tabs)
|
|
||||||
is SessionControlChange.ModeChange -> state.copy(mode = change.mode, tabs = emptyList())
|
|
||||||
is SessionControlChange.ExpansionChange -> {
|
|
||||||
val newExpandedCollection = state.expandedCollections.toMutableSet()
|
|
||||||
|
|
||||||
if (change.expand) {
|
|
||||||
newExpandedCollection.add(change.collection.id)
|
|
||||||
} else {
|
|
||||||
newExpandedCollection.remove(change.collection.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
state.copy(expandedCollections = newExpandedCollection)
|
|
||||||
}
|
|
||||||
is SessionControlChange.Change -> state.copy(
|
|
||||||
tabs = change.tabs,
|
|
||||||
mode = change.mode,
|
|
||||||
collections = change.collections
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,350 @@
|
||||||
|
/* 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.home.sessioncontrol
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.fragment.FragmentNavigator
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import mozilla.components.browser.session.SessionManager
|
||||||
|
import mozilla.components.concept.engine.prompt.ShareData
|
||||||
|
import mozilla.components.feature.media.ext.pauseIfPlaying
|
||||||
|
import mozilla.components.feature.media.ext.playIfPaused
|
||||||
|
import mozilla.components.feature.media.state.MediaStateMachine
|
||||||
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
|
import mozilla.components.feature.tab.collections.Tab as ComponentTab
|
||||||
|
import org.mozilla.fenix.BrowserDirection
|
||||||
|
import org.mozilla.fenix.HomeActivity
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
|
||||||
|
import org.mozilla.fenix.collections.SaveCollectionStep
|
||||||
|
import org.mozilla.fenix.components.TabCollectionStorage
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
|
import org.mozilla.fenix.components.metrics.MetricController
|
||||||
|
import org.mozilla.fenix.ext.nav
|
||||||
|
import org.mozilla.fenix.ext.sessionsOfType
|
||||||
|
import org.mozilla.fenix.home.HomeFragment
|
||||||
|
import org.mozilla.fenix.home.HomeFragmentAction
|
||||||
|
import org.mozilla.fenix.home.HomeFragmentDirections
|
||||||
|
import org.mozilla.fenix.home.HomeFragmentStore
|
||||||
|
import org.mozilla.fenix.home.Tab
|
||||||
|
import org.mozilla.fenix.settings.SupportUtils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [HomeFragment] controller. An interface that handles the view manipulation of the Tabs triggered
|
||||||
|
* by the Interactor.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("TooManyFunctions")
|
||||||
|
interface SessionControlController {
|
||||||
|
/**
|
||||||
|
* @see [TabSessionInteractor.onCloseTab]
|
||||||
|
*/
|
||||||
|
fun handleCloseTab(sessionId: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [TabSessionInteractor.onCloseAllTabs]
|
||||||
|
*/
|
||||||
|
fun handleCloseAllTabs(isPrivateMode: Boolean)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [CollectionInteractor.onCollectionAddTabTapped]
|
||||||
|
*/
|
||||||
|
fun handleCollectionAddTabTapped(collection: TabCollection)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [CollectionInteractor.onCollectionOpenTabClicked]
|
||||||
|
*/
|
||||||
|
fun handleCollectionOpenTabClicked(tab: ComponentTab)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [CollectionInteractor.onCollectionOpenTabsTapped]
|
||||||
|
*/
|
||||||
|
fun handleCollectionOpenTabsTapped(collection: TabCollection)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [CollectionInteractor.onCollectionRemoveTab]
|
||||||
|
*/
|
||||||
|
fun handleCollectionRemoveTab(collection: TabCollection, tab: ComponentTab)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [CollectionInteractor.onCollectionShareTabsClicked]
|
||||||
|
*/
|
||||||
|
fun handleCollectionShareTabsClicked(collection: TabCollection)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [CollectionInteractor.onDeleteCollectionTapped]
|
||||||
|
*/
|
||||||
|
fun handleDeleteCollectionTapped(collection: TabCollection)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [TabSessionInteractor.onPauseMediaClicked]
|
||||||
|
*/
|
||||||
|
fun handlePauseMediaClicked()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [TabSessionInteractor.onPlayMediaClicked]
|
||||||
|
*/
|
||||||
|
fun handlePlayMediaClicked()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [TabSessionInteractor.onPrivateBrowsingLearnMoreClicked]
|
||||||
|
*/
|
||||||
|
fun handlePrivateBrowsingLearnMoreClicked()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [CollectionInteractor.onRenameCollectionTapped]
|
||||||
|
*/
|
||||||
|
fun handleRenameCollectionTapped(collection: TabCollection)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [TabSessionInteractor.onSaveToCollection]
|
||||||
|
*/
|
||||||
|
fun handleSaveTabToCollection(selectedTabId: String?)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [TabSessionInteractor.onSelectTab]
|
||||||
|
*/
|
||||||
|
fun handleSelectTab(tabView: View, sessionId: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [TabSessionInteractor.onShareTabs]
|
||||||
|
*/
|
||||||
|
fun handleShareTabs()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [OnboardingInteractor.onStartBrowsingClicked]
|
||||||
|
*/
|
||||||
|
fun handleStartBrowsingClicked()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [CollectionInteractor.onToggleCollectionExpanded]
|
||||||
|
*/
|
||||||
|
fun handleToggleCollectionExpanded(collection: TabCollection, expand: Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("TooManyFunctions")
|
||||||
|
class DefaultSessionControlController(
|
||||||
|
private val activity: HomeActivity,
|
||||||
|
private val store: HomeFragmentStore,
|
||||||
|
private val navController: NavController,
|
||||||
|
private val homeLayout: MotionLayout,
|
||||||
|
private val browsingModeManager: BrowsingModeManager,
|
||||||
|
private val lifecycleScope: CoroutineScope,
|
||||||
|
private val closeTab: (sessionId: String) -> Unit,
|
||||||
|
private val closeAllTabs: (isPrivateMode: Boolean) -> Unit,
|
||||||
|
private val getListOfTabs: () -> List<Tab>,
|
||||||
|
private val hideOnboarding: () -> Unit,
|
||||||
|
private val invokePendingDeleteJobs: () -> Unit,
|
||||||
|
private val registerCollectionStorageObserver: () -> Unit,
|
||||||
|
private val scrollToTheTop: () -> Unit,
|
||||||
|
private val showDeleteCollectionPrompt: (tabCollection: TabCollection) -> Unit
|
||||||
|
) : SessionControlController {
|
||||||
|
private val metrics: MetricController
|
||||||
|
get() = activity.components.analytics.metrics
|
||||||
|
private val sessionManager: SessionManager
|
||||||
|
get() = activity.components.core.sessionManager
|
||||||
|
private val tabCollectionStorage: TabCollectionStorage
|
||||||
|
get() = activity.components.core.tabCollectionStorage
|
||||||
|
|
||||||
|
override fun handleCloseTab(sessionId: String) {
|
||||||
|
closeTab.invoke(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleCloseAllTabs(isPrivateMode: Boolean) {
|
||||||
|
closeAllTabs.invoke(isPrivateMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleCollectionAddTabTapped(collection: TabCollection) {
|
||||||
|
metrics.track(Event.CollectionAddTabPressed)
|
||||||
|
showCollectionCreationFragment(
|
||||||
|
step = SaveCollectionStep.SelectTabs,
|
||||||
|
selectedTabCollectionId = collection.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleCollectionOpenTabClicked(tab: ComponentTab) {
|
||||||
|
invokePendingDeleteJobs()
|
||||||
|
|
||||||
|
val session = tab.restore(
|
||||||
|
context = activity,
|
||||||
|
engine = activity.components.core.engine,
|
||||||
|
tab = tab,
|
||||||
|
restoreSessionId = false
|
||||||
|
)
|
||||||
|
|
||||||
|
if (session == null) {
|
||||||
|
// We were unable to create a snapshot, so just load the tab instead
|
||||||
|
activity.openToBrowserAndLoad(
|
||||||
|
searchTermOrURL = tab.url,
|
||||||
|
newTab = true,
|
||||||
|
from = BrowserDirection.FromHome
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
sessionManager.add(
|
||||||
|
session,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
activity.openToBrowser(BrowserDirection.FromHome)
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics.track(Event.CollectionTabRestored)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleCollectionOpenTabsTapped(collection: TabCollection) {
|
||||||
|
invokePendingDeleteJobs()
|
||||||
|
|
||||||
|
collection.tabs.reversed().forEach {
|
||||||
|
val session = it.restore(
|
||||||
|
context = activity,
|
||||||
|
engine = activity.components.core.engine,
|
||||||
|
tab = it,
|
||||||
|
restoreSessionId = false
|
||||||
|
)
|
||||||
|
|
||||||
|
if (session == null) {
|
||||||
|
// We were unable to create a snapshot, so just load the tab instead
|
||||||
|
activity.components.useCases.tabsUseCases.addTab.invoke(it.url)
|
||||||
|
} else {
|
||||||
|
sessionManager.add(
|
||||||
|
session,
|
||||||
|
activity.components.core.sessionManager.selectedSession == null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToTheTop()
|
||||||
|
metrics.track(Event.CollectionAllTabsRestored)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleCollectionRemoveTab(collection: TabCollection, tab: ComponentTab) {
|
||||||
|
metrics.track(Event.CollectionTabRemoved)
|
||||||
|
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
tabCollectionStorage.removeTabFromCollection(collection, tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleCollectionShareTabsClicked(collection: TabCollection) {
|
||||||
|
showShareFragment(collection.tabs.map { ShareData(url = it.url, title = it.title) })
|
||||||
|
metrics.track(Event.CollectionShared)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleDeleteCollectionTapped(collection: TabCollection) {
|
||||||
|
showDeleteCollectionPrompt(collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handlePauseMediaClicked() {
|
||||||
|
MediaStateMachine.state.pauseIfPlaying()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handlePlayMediaClicked() {
|
||||||
|
MediaStateMachine.state.playIfPaused()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handlePrivateBrowsingLearnMoreClicked() {
|
||||||
|
activity.openToBrowserAndLoad(
|
||||||
|
searchTermOrURL = SupportUtils.getGenericSumoURLForTopic
|
||||||
|
(SupportUtils.SumoTopic.PRIVATE_BROWSING_MYTHS),
|
||||||
|
newTab = true,
|
||||||
|
from = BrowserDirection.FromHome
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleRenameCollectionTapped(collection: TabCollection) {
|
||||||
|
showCollectionCreationFragment(
|
||||||
|
step = SaveCollectionStep.RenameCollection,
|
||||||
|
selectedTabCollectionId = collection.id
|
||||||
|
)
|
||||||
|
metrics.track(Event.CollectionRenamePressed)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleSaveTabToCollection(selectedTabId: String?) {
|
||||||
|
if (browsingModeManager.mode.isPrivate) return
|
||||||
|
|
||||||
|
invokePendingDeleteJobs()
|
||||||
|
|
||||||
|
val tabs = getListOfTabs()
|
||||||
|
val step = when {
|
||||||
|
// Show the SelectTabs fragment if there are multiple opened tabs to select which tabs
|
||||||
|
// you want to save to a collection.
|
||||||
|
tabs.size > 1 -> SaveCollectionStep.SelectTabs
|
||||||
|
// If there is an existing tab collection, show the SelectCollection fragment to save
|
||||||
|
// the selected tab to a collection of your choice.
|
||||||
|
tabCollectionStorage.cachedTabCollections.isNotEmpty() -> SaveCollectionStep.SelectCollection
|
||||||
|
// Show the NameCollection fragment to create a new collection for the selected tab.
|
||||||
|
else -> SaveCollectionStep.NameCollection
|
||||||
|
}
|
||||||
|
|
||||||
|
showCollectionCreationFragment(step, selectedTabId?.let { arrayOf(it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleSelectTab(tabView: View, sessionId: String) {
|
||||||
|
invokePendingDeleteJobs()
|
||||||
|
val session = sessionManager.findSessionById(sessionId)
|
||||||
|
sessionManager.select(session!!)
|
||||||
|
val directions = HomeFragmentDirections.actionHomeFragmentToBrowserFragment(null)
|
||||||
|
val extras =
|
||||||
|
FragmentNavigator.Extras.Builder()
|
||||||
|
.addSharedElement(tabView,
|
||||||
|
"$TAB_ITEM_TRANSITION_NAME$sessionId"
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
navController.nav(R.id.homeFragment, directions, extras)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleShareTabs() {
|
||||||
|
invokePendingDeleteJobs()
|
||||||
|
val shareData = sessionManager
|
||||||
|
.sessionsOfType(private = browsingModeManager.mode.isPrivate)
|
||||||
|
.map { ShareData(url = it.url, title = it.title) }
|
||||||
|
.toList()
|
||||||
|
showShareFragment(shareData)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleStartBrowsingClicked() {
|
||||||
|
homeLayout.progress = 0F
|
||||||
|
hideOnboarding()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleToggleCollectionExpanded(collection: TabCollection, expand: Boolean) {
|
||||||
|
store.dispatch(HomeFragmentAction.CollectionExpanded(collection, expand))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showCollectionCreationFragment(
|
||||||
|
step: SaveCollectionStep,
|
||||||
|
selectedTabIds: Array<String>? = null,
|
||||||
|
selectedTabCollectionId: Long? = null
|
||||||
|
) {
|
||||||
|
if (navController.currentDestination?.id == R.id.collectionCreationFragment) return
|
||||||
|
|
||||||
|
// Only register the observer right before moving to collection creation
|
||||||
|
registerCollectionStorageObserver()
|
||||||
|
|
||||||
|
val tabIds = getListOfTabs().map { it.sessionId }.toTypedArray()
|
||||||
|
val directions = HomeFragmentDirections.actionHomeFragmentToCreateCollectionFragment(
|
||||||
|
tabIds = tabIds,
|
||||||
|
previousFragmentId = R.id.homeFragment,
|
||||||
|
saveCollectionStep = step,
|
||||||
|
selectedTabIds = selectedTabIds,
|
||||||
|
selectedTabCollectionId = selectedTabCollectionId ?: -1
|
||||||
|
)
|
||||||
|
navController.nav(R.id.homeFragment, directions)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showShareFragment(data: List<ShareData>) {
|
||||||
|
val directions = HomeFragmentDirections.actionHomeFragmentToShareFragment(
|
||||||
|
data = data.toTypedArray()
|
||||||
|
)
|
||||||
|
navController.nav(R.id.homeFragment, directions)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAB_ITEM_TRANSITION_NAME = "tab_item"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,228 @@
|
||||||
|
/* 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.home.sessioncontrol
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import mozilla.components.feature.tab.collections.Tab
|
||||||
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for collection related actions in the [SessionControlInteractor].
|
||||||
|
*/
|
||||||
|
interface CollectionInteractor {
|
||||||
|
/**
|
||||||
|
* Shows the Collection Creation fragment for selecting the tabs to add to the given tab
|
||||||
|
* collection. Called when a user taps on the "Add tab" collection menu item.
|
||||||
|
*
|
||||||
|
* @param collection The collection of tabs that will be modified.
|
||||||
|
*/
|
||||||
|
fun onCollectionAddTabTapped(collection: TabCollection)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the given tab. Called when a user clicks on a tab in the tab collection.
|
||||||
|
*
|
||||||
|
* @param tab The tab to open from the tab collection.
|
||||||
|
*/
|
||||||
|
fun onCollectionOpenTabClicked(tab: Tab)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens all the tabs in a given tab collection. Called when a user taps on the "Open tabs"
|
||||||
|
* collection menu item.
|
||||||
|
*
|
||||||
|
* @param collection The collection of tabs to open.
|
||||||
|
*/
|
||||||
|
fun onCollectionOpenTabsTapped(collection: TabCollection)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the given tab from the given tab collection. Called when a user swipes to remove a
|
||||||
|
* tab or clicks on the tab close button.
|
||||||
|
*
|
||||||
|
* @param collection The collection of tabs that will be modified.
|
||||||
|
* @param tab The tab to remove from the tab collection.
|
||||||
|
*/
|
||||||
|
fun onCollectionRemoveTab(collection: TabCollection, tab: Tab)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shares the tabs in the given tab collection. Called when a user clicks on the Collection
|
||||||
|
* Share button.
|
||||||
|
*
|
||||||
|
* @param collection The collection of tabs to share.
|
||||||
|
*/
|
||||||
|
fun onCollectionShareTabsClicked(collection: TabCollection)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a prompt for deleting the given tab collection. Called when a user taps on the
|
||||||
|
* "Delete collection" collection menu item.
|
||||||
|
*
|
||||||
|
* @param collection The collection of tabs to delete.
|
||||||
|
*/
|
||||||
|
fun onDeleteCollectionTapped(collection: TabCollection)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the Collection Creation fragment for renaming the given tab collection. Called when a
|
||||||
|
* user taps on the "Rename collection" collection menu item.
|
||||||
|
*
|
||||||
|
* @param collection The collection of tabs to rename.
|
||||||
|
*/
|
||||||
|
fun onRenameCollectionTapped(collection: TabCollection)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles expanding or collapsing the given tab collection. Called when a user clicks on a
|
||||||
|
* [CollectionViewHolder].
|
||||||
|
*
|
||||||
|
* @param collection The collection of tabs that will be collapsed.
|
||||||
|
* @param expand True if the given tab collection should be expanded or collapse if false.
|
||||||
|
*/
|
||||||
|
fun onToggleCollectionExpanded(collection: TabCollection, expand: Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for onboarding related actions in the [SessionControlInteractor].
|
||||||
|
*/
|
||||||
|
interface OnboardingInteractor {
|
||||||
|
/**
|
||||||
|
* Hides the onboarding. Called when a user clicks on the "Start Browsing" button.
|
||||||
|
*/
|
||||||
|
fun onStartBrowsingClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for tab related actions in the [SessionControlInteractor].
|
||||||
|
*/
|
||||||
|
interface TabSessionInteractor {
|
||||||
|
/**
|
||||||
|
* Closes the given tab. Called when a user swipes to close a tab or clicks on the Close Tab
|
||||||
|
* button in the tab view.
|
||||||
|
*
|
||||||
|
* @param sessionId The selected tab session id to close.
|
||||||
|
*/
|
||||||
|
fun onCloseTab(sessionId: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes all the tabs. Called when a user clicks on the Close Tabs button or "Close all tabs"
|
||||||
|
* tab header menu item.
|
||||||
|
*
|
||||||
|
* @param isPrivateMode True if the [BrowsingMode] is [Private] and false otherwise.
|
||||||
|
*/
|
||||||
|
fun onCloseAllTabs(isPrivateMode: Boolean)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pauses all playing [Media]. Called when a user clicks on the Pause button in the tab view.
|
||||||
|
*/
|
||||||
|
fun onPauseMediaClicked()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resumes playing all paused [Media]. Called when a user clicks on the Play button in the tab
|
||||||
|
* view.
|
||||||
|
*/
|
||||||
|
fun onPlayMediaClicked()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the Private Browsing Learn More page in a new tab. Called when a user clicks on the
|
||||||
|
* "Common myths about private browsing" link in private mode.
|
||||||
|
*/
|
||||||
|
fun onPrivateBrowsingLearnMoreClicked()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the given tab to collection. Called when a user clicks on the "Save to collection"
|
||||||
|
* button or tab header menu item, and on long click of an open tab.
|
||||||
|
*
|
||||||
|
* @param sessionId The selected tab session id to save.
|
||||||
|
*/
|
||||||
|
fun onSaveToCollection(sessionId: String?)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects the given tab. Called when a user clicks on a tab.
|
||||||
|
*
|
||||||
|
* @param tabView [View] of the current Fragment to match with a View in the Fragment being
|
||||||
|
* navigated to.
|
||||||
|
* @param sessionId The tab session id to select.
|
||||||
|
*/
|
||||||
|
fun onSelectTab(tabView: View, sessionId: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shares the current opened tabs. Called when a user clicks on the Share Tabs button in private
|
||||||
|
* mode or tab header menu item.
|
||||||
|
*/
|
||||||
|
fun onShareTabs()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interactor for the Home screen.
|
||||||
|
* Provides implementations for the CollectionInteractor, OnboardingInteractor and
|
||||||
|
* TabSessionInteractor.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("TooManyFunctions")
|
||||||
|
class SessionControlInteractor(
|
||||||
|
private val controller: SessionControlController
|
||||||
|
) : CollectionInteractor, OnboardingInteractor, TabSessionInteractor {
|
||||||
|
override fun onCloseTab(sessionId: String) {
|
||||||
|
controller.handleCloseTab(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCloseAllTabs(isPrivateMode: Boolean) {
|
||||||
|
controller.handleCloseAllTabs(isPrivateMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCollectionAddTabTapped(collection: TabCollection) {
|
||||||
|
controller.handleCollectionAddTabTapped(collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCollectionOpenTabClicked(tab: Tab) {
|
||||||
|
controller.handleCollectionOpenTabClicked(tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCollectionOpenTabsTapped(collection: TabCollection) {
|
||||||
|
controller.handleCollectionOpenTabsTapped(collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCollectionRemoveTab(collection: TabCollection, tab: Tab) {
|
||||||
|
controller.handleCollectionRemoveTab(collection, tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCollectionShareTabsClicked(collection: TabCollection) {
|
||||||
|
controller.handleCollectionShareTabsClicked(collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDeleteCollectionTapped(collection: TabCollection) {
|
||||||
|
controller.handleDeleteCollectionTapped(collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPauseMediaClicked() {
|
||||||
|
controller.handlePauseMediaClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlayMediaClicked() {
|
||||||
|
controller.handlePlayMediaClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrivateBrowsingLearnMoreClicked() {
|
||||||
|
controller.handlePrivateBrowsingLearnMoreClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRenameCollectionTapped(collection: TabCollection) {
|
||||||
|
controller.handleRenameCollectionTapped(collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveToCollection(sessionId: String?) {
|
||||||
|
controller.handleSaveTabToCollection(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSelectTab(tabView: View, sessionId: String) {
|
||||||
|
controller.handleSelectTab(tabView, sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShareTabs() {
|
||||||
|
controller.handleShareTabs()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartBrowsingClicked() {
|
||||||
|
controller.handleStartBrowsingClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onToggleCollectionExpanded(collection: TabCollection, expand: Boolean) {
|
||||||
|
controller.handleToggleCollectionExpanded(collection, expand)
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,17 +6,22 @@ package org.mozilla.fenix.home.sessioncontrol
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.reactivex.Observable
|
import kotlinx.android.extensions.LayoutContainer
|
||||||
import io.reactivex.Observer
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import io.reactivex.functions.Consumer
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
|
import mozilla.components.lib.state.ext.consumeFrom
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.home.HomeFragmentState
|
||||||
|
import org.mozilla.fenix.home.HomeFragmentStore
|
||||||
import org.mozilla.fenix.home.Mode
|
import org.mozilla.fenix.home.Mode
|
||||||
import org.mozilla.fenix.home.OnboardingState
|
import org.mozilla.fenix.home.OnboardingState
|
||||||
import org.mozilla.fenix.mvi.UIView
|
import org.mozilla.fenix.home.Tab
|
||||||
|
|
||||||
val noTabMessage = AdapterItem.NoContentMessage(
|
val noTabMessage = AdapterItem.NoContentMessage(
|
||||||
R.drawable.ic_tabs,
|
R.drawable.ic_tabs,
|
||||||
|
@ -110,7 +115,7 @@ private fun onboardingAdapterItems(onboardingState: OnboardingState): List<Adapt
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun SessionControlState.toAdapterList(): List<AdapterItem> = when (mode) {
|
private fun HomeFragmentState.toAdapterList(): List<AdapterItem> = when (mode) {
|
||||||
is Mode.Normal -> normalModeAdapterItems(tabs, collections, expandedCollections)
|
is Mode.Normal -> normalModeAdapterItems(tabs, collections, expandedCollections)
|
||||||
is Mode.Private -> privateModeAdapterItems(tabs)
|
is Mode.Private -> privateModeAdapterItems(tabs)
|
||||||
is Mode.Onboarding -> onboardingAdapterItems(mode.state)
|
is Mode.Onboarding -> onboardingAdapterItems(mode.state)
|
||||||
|
@ -120,22 +125,20 @@ private fun collectionTabItems(collection: TabCollection) = collection.tabs.mapI
|
||||||
AdapterItem.TabInCollectionItem(collection, tab, index == collection.tabs.lastIndex)
|
AdapterItem.TabInCollectionItem(collection, tab, index == collection.tabs.lastIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
class SessionControlUIView(
|
@ExperimentalCoroutinesApi
|
||||||
container: ViewGroup,
|
class SessionControlView(
|
||||||
actionEmitter: Observer<SessionControlAction>,
|
private val homeFragmentStore: HomeFragmentStore,
|
||||||
changesObservable: Observable<SessionControlChange>
|
private val container: ViewGroup,
|
||||||
) :
|
interactor: SessionControlInteractor
|
||||||
UIView<SessionControlState, SessionControlAction, SessionControlChange>(
|
) : LayoutContainer {
|
||||||
container,
|
override val containerView: View?
|
||||||
actionEmitter,
|
get() = container
|
||||||
changesObservable
|
|
||||||
) {
|
|
||||||
|
|
||||||
override val view: RecyclerView = LayoutInflater.from(container.context)
|
val view: RecyclerView = LayoutInflater.from(container.context)
|
||||||
.inflate(R.layout.component_session_control, container, true)
|
.inflate(R.layout.component_session_control, container, true)
|
||||||
.findViewById(R.id.home_component)
|
.findViewById(R.id.home_component)
|
||||||
|
|
||||||
private val sessionControlAdapter = SessionControlAdapter(actionEmitter)
|
private val sessionControlAdapter = SessionControlAdapter(interactor)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
view.apply {
|
view.apply {
|
||||||
|
@ -144,18 +147,22 @@ class SessionControlUIView(
|
||||||
val itemTouchHelper =
|
val itemTouchHelper =
|
||||||
ItemTouchHelper(
|
ItemTouchHelper(
|
||||||
SwipeToDeleteCallback(
|
SwipeToDeleteCallback(
|
||||||
actionEmitter
|
interactor
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
itemTouchHelper.attachToRecyclerView(this)
|
itemTouchHelper.attachToRecyclerView(this)
|
||||||
|
|
||||||
|
view.consumeFrom(homeFragmentStore, ProcessLifecycleOwner.get()) {
|
||||||
|
update(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateView() = Consumer<SessionControlState> {
|
fun update(state: HomeFragmentState) {
|
||||||
// Workaround for list not updating until scroll on Android 5 + 6
|
// Workaround for list not updating until scroll on Android 5 + 6
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||||
sessionControlAdapter.submitList(null)
|
sessionControlAdapter.submitList(null)
|
||||||
}
|
}
|
||||||
sessionControlAdapter.submitList(it.toAdapterList())
|
sessionControlAdapter.submitList(state.toAdapterList())
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -10,14 +10,13 @@ import android.graphics.drawable.Drawable
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.reactivex.Observer
|
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.ext.getColorFromAttr
|
import org.mozilla.fenix.ext.getColorFromAttr
|
||||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder
|
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder
|
||||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabViewHolder
|
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabViewHolder
|
||||||
|
|
||||||
class SwipeToDeleteCallback(
|
class SwipeToDeleteCallback(
|
||||||
val actionEmitter: Observer<SessionControlAction>
|
val interactor: SessionControlInteractor
|
||||||
) : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
|
) : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
|
||||||
override fun onMove(
|
override fun onMove(
|
||||||
recyclerView: RecyclerView,
|
recyclerView: RecyclerView,
|
||||||
|
@ -30,9 +29,9 @@ class SwipeToDeleteCallback(
|
||||||
|
|
||||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||||
when (viewHolder) {
|
when (viewHolder) {
|
||||||
is TabViewHolder -> actionEmitter.onNext(TabAction.Close(viewHolder.tab?.sessionId!!))
|
is TabViewHolder -> interactor.onCloseTab(viewHolder.tab?.sessionId!!)
|
||||||
is TabInCollectionViewHolder -> {
|
is TabInCollectionViewHolder -> {
|
||||||
actionEmitter.onNext(CollectionAction.RemoveTab(viewHolder.collection, viewHolder.tab))
|
interactor.onCollectionRemoveTab(viewHolder.collection, viewHolder.tab)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,25 +9,22 @@ import android.graphics.PorterDuff.Mode.SRC_IN
|
||||||
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.extensions.LayoutContainer
|
import kotlinx.android.extensions.LayoutContainer
|
||||||
import kotlinx.android.synthetic.main.collection_home_list_row.*
|
import kotlinx.android.synthetic.main.collection_home_list_row.*
|
||||||
import kotlinx.android.synthetic.main.collection_home_list_row.view.*
|
import kotlinx.android.synthetic.main.collection_home_list_row.view.*
|
||||||
import mozilla.components.browser.menu.BrowserMenuBuilder
|
import mozilla.components.browser.menu.BrowserMenuBuilder
|
||||||
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
|
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
|
||||||
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.theme.ThemeManager
|
import org.mozilla.fenix.theme.ThemeManager
|
||||||
import org.mozilla.fenix.components.description
|
import org.mozilla.fenix.components.description
|
||||||
import org.mozilla.fenix.ext.getIconColor
|
import org.mozilla.fenix.ext.getIconColor
|
||||||
import org.mozilla.fenix.ext.increaseTapArea
|
import org.mozilla.fenix.ext.increaseTapArea
|
||||||
import org.mozilla.fenix.home.sessioncontrol.CollectionAction
|
import org.mozilla.fenix.home.sessioncontrol.CollectionInteractor
|
||||||
import org.mozilla.fenix.home.sessioncontrol.SessionControlAction
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.TabCollection
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.onNext
|
|
||||||
|
|
||||||
class CollectionViewHolder(
|
class CollectionViewHolder(
|
||||||
val view: View,
|
val view: View,
|
||||||
val actionEmitter: Observer<SessionControlAction>,
|
val interactor: CollectionInteractor,
|
||||||
override val containerView: View? = view
|
override val containerView: View? = view
|
||||||
) :
|
) :
|
||||||
RecyclerView.ViewHolder(view), LayoutContainer {
|
RecyclerView.ViewHolder(view), LayoutContainer {
|
||||||
|
@ -40,10 +37,10 @@ class CollectionViewHolder(
|
||||||
init {
|
init {
|
||||||
collectionMenu = CollectionItemMenu(view.context, sessionHasOpenTabs) {
|
collectionMenu = CollectionItemMenu(view.context, sessionHasOpenTabs) {
|
||||||
when (it) {
|
when (it) {
|
||||||
is CollectionItemMenu.Item.DeleteCollection -> actionEmitter.onNext(CollectionAction.Delete(collection))
|
is CollectionItemMenu.Item.DeleteCollection -> interactor.onDeleteCollectionTapped(collection)
|
||||||
is CollectionItemMenu.Item.AddTab -> actionEmitter.onNext(CollectionAction.AddTab(collection))
|
is CollectionItemMenu.Item.AddTab -> interactor.onCollectionAddTabTapped(collection)
|
||||||
is CollectionItemMenu.Item.RenameCollection -> actionEmitter.onNext(CollectionAction.Rename(collection))
|
is CollectionItemMenu.Item.RenameCollection -> interactor.onRenameCollectionTapped(collection)
|
||||||
is CollectionItemMenu.Item.OpenTabs -> actionEmitter.onNext(CollectionAction.OpenTabs(collection))
|
is CollectionItemMenu.Item.OpenTabs -> interactor.onCollectionOpenTabsTapped(collection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,13 +56,13 @@ class CollectionViewHolder(
|
||||||
collection_share_button.run {
|
collection_share_button.run {
|
||||||
increaseTapArea(buttonIncreaseDps)
|
increaseTapArea(buttonIncreaseDps)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
actionEmitter.onNext(CollectionAction.ShareTabs(collection))
|
interactor.onCollectionShareTabsClicked(collection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
view.clipToOutline = true
|
view.clipToOutline = true
|
||||||
view.setOnClickListener {
|
view.setOnClickListener {
|
||||||
handleExpansion(expanded)
|
interactor.onToggleCollectionExpanded(collection, !expanded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,14 +95,6 @@ class CollectionViewHolder(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleExpansion(isExpanded: Boolean) {
|
|
||||||
if (isExpanded) {
|
|
||||||
actionEmitter.onNext(CollectionAction.Collapse(collection))
|
|
||||||
} else {
|
|
||||||
actionEmitter.onNext(CollectionAction.Expand(collection))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val buttonIncreaseDps = 16
|
const val buttonIncreaseDps = 16
|
||||||
const val EXPANDED_PADDING = 60
|
const val EXPANDED_PADDING = 60
|
||||||
|
|
|
@ -9,16 +9,13 @@ import android.text.method.LinkMovementMethod
|
||||||
import android.text.style.UnderlineSpan
|
import android.text.style.UnderlineSpan
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.reactivex.Observer
|
|
||||||
import kotlinx.android.synthetic.main.private_browsing_description.view.*
|
import kotlinx.android.synthetic.main.private_browsing_description.view.*
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.home.sessioncontrol.SessionControlAction
|
import org.mozilla.fenix.home.sessioncontrol.TabSessionInteractor
|
||||||
import org.mozilla.fenix.home.sessioncontrol.TabAction
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.onNext
|
|
||||||
|
|
||||||
class PrivateBrowsingDescriptionViewHolder(
|
class PrivateBrowsingDescriptionViewHolder(
|
||||||
view: View,
|
view: View,
|
||||||
private val actionEmitter: Observer<SessionControlAction>
|
private val interactor: TabSessionInteractor
|
||||||
) : RecyclerView.ViewHolder(view) {
|
) : RecyclerView.ViewHolder(view) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -35,7 +32,7 @@ class PrivateBrowsingDescriptionViewHolder(
|
||||||
movementMethod = LinkMovementMethod.getInstance()
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
text = textWithLink
|
text = textWithLink
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
actionEmitter.onNext(TabAction.PrivateBrowsingLearnMore)
|
interactor.onPrivateBrowsingLearnMoreClicked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,26 +6,22 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.reactivex.Observer
|
|
||||||
import kotlinx.android.synthetic.main.save_tab_group_button.view.*
|
import kotlinx.android.synthetic.main.save_tab_group_button.view.*
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
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.home.sessioncontrol.SessionControlAction
|
import org.mozilla.fenix.home.sessioncontrol.TabSessionInteractor
|
||||||
import org.mozilla.fenix.home.sessioncontrol.TabAction
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.onNext
|
|
||||||
|
|
||||||
class SaveTabGroupViewHolder(
|
class SaveTabGroupViewHolder(
|
||||||
view: View,
|
view: View,
|
||||||
private val actionEmitter: Observer<SessionControlAction>
|
private val interactor: TabSessionInteractor
|
||||||
) : RecyclerView.ViewHolder(view) {
|
) : RecyclerView.ViewHolder(view) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
view.save_tab_group_button.setOnClickListener {
|
view.save_tab_group_button.setOnClickListener {
|
||||||
view.context.components.analytics.metrics
|
view.context.components.analytics.metrics
|
||||||
.track(Event.CollectionSaveButtonPressed(TELEMETRY_HOME_IDENTIFIER))
|
.track(Event.CollectionSaveButtonPressed(TELEMETRY_HOME_IDENTIFIER))
|
||||||
|
interactor.onSaveToCollection(sessionId = null)
|
||||||
actionEmitter.onNext(TabAction.SaveTabGroup(selectedTabSessionId = null))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ import android.widget.PopupWindow
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.reactivex.Observer
|
|
||||||
import kotlinx.android.synthetic.main.tab_header.view.*
|
import kotlinx.android.synthetic.main.tab_header.view.*
|
||||||
import mozilla.components.browser.menu.BrowserMenu
|
import mozilla.components.browser.menu.BrowserMenu
|
||||||
import mozilla.components.browser.menu.BrowserMenuBuilder
|
import mozilla.components.browser.menu.BrowserMenuBuilder
|
||||||
|
@ -18,13 +17,11 @@ import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
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.home.sessioncontrol.SessionControlAction
|
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
|
||||||
import org.mozilla.fenix.home.sessioncontrol.TabAction
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.onNext
|
|
||||||
|
|
||||||
class TabHeaderViewHolder(
|
class TabHeaderViewHolder(
|
||||||
private val view: View,
|
private val view: View,
|
||||||
private val actionEmitter: Observer<SessionControlAction>
|
private val interactor: SessionControlInteractor
|
||||||
) : RecyclerView.ViewHolder(view) {
|
) : RecyclerView.ViewHolder(view) {
|
||||||
private var isPrivate = false
|
private var isPrivate = false
|
||||||
private var tabsMenu: TabHeaderMenu
|
private var tabsMenu: TabHeaderMenu
|
||||||
|
@ -32,10 +29,10 @@ class TabHeaderViewHolder(
|
||||||
init {
|
init {
|
||||||
tabsMenu = TabHeaderMenu(view.context, isPrivate) {
|
tabsMenu = TabHeaderMenu(view.context, isPrivate) {
|
||||||
when (it) {
|
when (it) {
|
||||||
is TabHeaderMenu.Item.Share -> actionEmitter.onNext(TabAction.ShareTabs)
|
is TabHeaderMenu.Item.Share -> interactor.onShareTabs()
|
||||||
is TabHeaderMenu.Item.CloseAll -> actionEmitter.onNext(TabAction.CloseAll(isPrivate))
|
is TabHeaderMenu.Item.CloseAll -> interactor.onCloseAllTabs(isPrivate)
|
||||||
is TabHeaderMenu.Item.SaveToCollection -> {
|
is TabHeaderMenu.Item.SaveToCollection -> {
|
||||||
actionEmitter.onNext(TabAction.SaveTabGroup(null))
|
interactor.onSaveToCollection(null)
|
||||||
view.context.components.analytics.metrics
|
view.context.components.analytics.metrics
|
||||||
.track(Event.CollectionSaveButtonPressed(TELEMETRY_HOME_MENU_IDENITIFIER))
|
.track(Event.CollectionSaveButtonPressed(TELEMETRY_HOME_MENU_IDENITIFIER))
|
||||||
}
|
}
|
||||||
|
@ -45,14 +42,14 @@ class TabHeaderViewHolder(
|
||||||
view.apply {
|
view.apply {
|
||||||
share_tabs_button.run {
|
share_tabs_button.run {
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
actionEmitter.onNext(TabAction.ShareTabs)
|
interactor.onShareTabs()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
close_tabs_button.run {
|
close_tabs_button.run {
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
view.context.components.analytics.metrics.track(Event.PrivateBrowsingGarbageIconTapped)
|
view.context.components.analytics.metrics.track(Event.PrivateBrowsingGarbageIconTapped)
|
||||||
actionEmitter.onNext(TabAction.CloseAll(true))
|
interactor.onCloseAllTabs(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,9 +9,9 @@ import android.view.View
|
||||||
import android.view.ViewOutlineProvider
|
import android.view.ViewOutlineProvider
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.reactivex.Observer
|
|
||||||
import kotlinx.android.extensions.LayoutContainer
|
import kotlinx.android.extensions.LayoutContainer
|
||||||
import kotlinx.android.synthetic.main.tab_in_collection.*
|
import kotlinx.android.synthetic.main.tab_in_collection.*
|
||||||
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
import mozilla.components.support.ktx.android.util.dpToFloat
|
import mozilla.components.support.ktx.android.util.dpToFloat
|
||||||
import org.jetbrains.anko.backgroundColor
|
import org.jetbrains.anko.backgroundColor
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
|
@ -20,15 +20,12 @@ import org.mozilla.fenix.ext.getColorFromAttr
|
||||||
import org.mozilla.fenix.ext.increaseTapArea
|
import org.mozilla.fenix.ext.increaseTapArea
|
||||||
import org.mozilla.fenix.ext.loadIntoView
|
import org.mozilla.fenix.ext.loadIntoView
|
||||||
import org.mozilla.fenix.ext.toShortUrl
|
import org.mozilla.fenix.ext.toShortUrl
|
||||||
import org.mozilla.fenix.home.sessioncontrol.CollectionAction
|
import org.mozilla.fenix.home.sessioncontrol.CollectionInteractor
|
||||||
import org.mozilla.fenix.home.sessioncontrol.SessionControlAction
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.TabCollection
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.onNext
|
|
||||||
import mozilla.components.feature.tab.collections.Tab as ComponentTab
|
import mozilla.components.feature.tab.collections.Tab as ComponentTab
|
||||||
|
|
||||||
class TabInCollectionViewHolder(
|
class TabInCollectionViewHolder(
|
||||||
val view: View,
|
val view: View,
|
||||||
val actionEmitter: Observer<SessionControlAction>,
|
val interactor: CollectionInteractor,
|
||||||
override val containerView: View? = view
|
override val containerView: View? = view
|
||||||
) : RecyclerView.ViewHolder(view), LayoutContainer {
|
) : RecyclerView.ViewHolder(view), LayoutContainer {
|
||||||
|
|
||||||
|
@ -53,12 +50,12 @@ class TabInCollectionViewHolder(
|
||||||
}
|
}
|
||||||
|
|
||||||
view.setOnClickListener {
|
view.setOnClickListener {
|
||||||
actionEmitter.onNext(CollectionAction.OpenTab(tab))
|
interactor.onCollectionOpenTabClicked(tab)
|
||||||
}
|
}
|
||||||
|
|
||||||
collection_tab_close_button.increaseTapArea(buttonIncreaseDps)
|
collection_tab_close_button.increaseTapArea(buttonIncreaseDps)
|
||||||
collection_tab_close_button.setOnClickListener {
|
collection_tab_close_button.setOnClickListener {
|
||||||
actionEmitter.onNext(CollectionAction.RemoveTab(collection, tab))
|
interactor.onCollectionRemoveTab(collection, tab)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,17 +4,13 @@
|
||||||
|
|
||||||
package org.mozilla.fenix.home.sessioncontrol.viewholders
|
package org.mozilla.fenix.home.sessioncontrol.viewholders
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Outline
|
import android.graphics.Outline
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewOutlineProvider
|
import android.view.ViewOutlineProvider
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.reactivex.Observer
|
|
||||||
import kotlinx.android.extensions.LayoutContainer
|
import kotlinx.android.extensions.LayoutContainer
|
||||||
import kotlinx.android.synthetic.main.tab_list_row.*
|
import kotlinx.android.synthetic.main.tab_list_row.*
|
||||||
import mozilla.components.browser.menu.BrowserMenuBuilder
|
|
||||||
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
|
|
||||||
import mozilla.components.feature.media.state.MediaState
|
import mozilla.components.feature.media.state.MediaState
|
||||||
import mozilla.components.support.ktx.android.util.dpToFloat
|
import mozilla.components.support.ktx.android.util.dpToFloat
|
||||||
import org.jetbrains.anko.imageBitmap
|
import org.jetbrains.anko.imageBitmap
|
||||||
|
@ -23,41 +19,31 @@ import org.mozilla.fenix.components.metrics.Event
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.increaseTapArea
|
import org.mozilla.fenix.ext.increaseTapArea
|
||||||
import org.mozilla.fenix.ext.loadIntoView
|
import org.mozilla.fenix.ext.loadIntoView
|
||||||
import org.mozilla.fenix.home.sessioncontrol.SessionControlAction
|
import org.mozilla.fenix.home.Tab
|
||||||
import org.mozilla.fenix.home.sessioncontrol.Tab
|
import org.mozilla.fenix.home.sessioncontrol.TabSessionInteractor
|
||||||
import org.mozilla.fenix.home.sessioncontrol.TabAction
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.onNext
|
|
||||||
|
|
||||||
class TabViewHolder(
|
class TabViewHolder(
|
||||||
view: View,
|
view: View,
|
||||||
actionEmitter: Observer<SessionControlAction>,
|
interactor: TabSessionInteractor,
|
||||||
override val containerView: View? = view
|
override val containerView: View? = view
|
||||||
) :
|
) :
|
||||||
RecyclerView.ViewHolder(view), LayoutContainer {
|
RecyclerView.ViewHolder(view), LayoutContainer {
|
||||||
|
|
||||||
internal var tab: Tab? = null
|
internal var tab: Tab? = null
|
||||||
private var tabMenu: TabItemMenu
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
tabMenu = TabItemMenu(view.context) {
|
|
||||||
when (it) {
|
|
||||||
is TabItemMenu.Item.Share ->
|
|
||||||
actionEmitter.onNext(TabAction.Share(tab?.sessionId!!))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item_tab.setOnClickListener {
|
item_tab.setOnClickListener {
|
||||||
actionEmitter.onNext(TabAction.Select(it, tab?.sessionId!!))
|
interactor.onSelectTab(it, tab?.sessionId!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
item_tab.setOnLongClickListener {
|
item_tab.setOnLongClickListener {
|
||||||
view.context.components.analytics.metrics.track(Event.CollectionTabLongPressed)
|
view.context.components.analytics.metrics.track(Event.CollectionTabLongPressed)
|
||||||
actionEmitter.onNext(TabAction.SaveTabGroup(tab?.sessionId!!))
|
interactor.onSaveToCollection(tab?.sessionId!!)
|
||||||
true
|
return@setOnLongClickListener true
|
||||||
}
|
}
|
||||||
|
|
||||||
close_tab_button.setOnClickListener {
|
close_tab_button.setOnClickListener {
|
||||||
actionEmitter.onNext(TabAction.Close(tab?.sessionId!!))
|
interactor.onCloseTab(tab?.sessionId!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
play_pause_button.increaseTapArea(PLAY_PAUSE_BUTTON_EXTRA_DPS)
|
play_pause_button.increaseTapArea(PLAY_PAUSE_BUTTON_EXTRA_DPS)
|
||||||
|
@ -66,12 +52,12 @@ class TabViewHolder(
|
||||||
when (tab?.mediaState) {
|
when (tab?.mediaState) {
|
||||||
is MediaState.Playing -> {
|
is MediaState.Playing -> {
|
||||||
it.context.components.analytics.metrics.track(Event.TabMediaPlay)
|
it.context.components.analytics.metrics.track(Event.TabMediaPlay)
|
||||||
actionEmitter.onNext(TabAction.PauseMedia(tab?.sessionId!!))
|
interactor.onPauseMediaClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
is MediaState.Paused -> {
|
is MediaState.Paused -> {
|
||||||
it.context.components.analytics.metrics.track(Event.TabMediaPause)
|
it.context.components.analytics.metrics.track(Event.TabMediaPause)
|
||||||
actionEmitter.onNext(TabAction.PlayMedia(tab?.sessionId!!))
|
interactor.onPlayMediaClicked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,24 +141,3 @@ class TabViewHolder(
|
||||||
const val favIconBorderRadiusInPx = 4
|
const val favIconBorderRadiusInPx = 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TabItemMenu(
|
|
||||||
private val context: Context,
|
|
||||||
private val onItemTapped: (Item) -> Unit = {}
|
|
||||||
) {
|
|
||||||
sealed class Item {
|
|
||||||
object Share : Item()
|
|
||||||
}
|
|
||||||
|
|
||||||
val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
|
|
||||||
|
|
||||||
private val menuItems by lazy {
|
|
||||||
listOf(
|
|
||||||
SimpleBrowserMenuItem(
|
|
||||||
context.getString(R.string.tab_share)
|
|
||||||
) {
|
|
||||||
onItemTapped.invoke(Item.Share)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,21 +6,18 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.reactivex.Observer
|
|
||||||
import kotlinx.android.synthetic.main.onboarding_finish.view.*
|
import kotlinx.android.synthetic.main.onboarding_finish.view.*
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.home.sessioncontrol.OnboardingAction
|
import org.mozilla.fenix.home.sessioncontrol.OnboardingInteractor
|
||||||
import org.mozilla.fenix.home.sessioncontrol.SessionControlAction
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.onNext
|
|
||||||
|
|
||||||
class OnboardingFinishViewHolder(
|
class OnboardingFinishViewHolder(
|
||||||
view: View,
|
view: View,
|
||||||
private val actionEmitter: Observer<SessionControlAction>
|
private val interactor: OnboardingInteractor
|
||||||
) : RecyclerView.ViewHolder(view) {
|
) : RecyclerView.ViewHolder(view) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
view.finish_button.setOnClickListener {
|
view.finish_button.setOnClickListener {
|
||||||
actionEmitter.onNext(OnboardingAction.Finish)
|
interactor.onStartBrowsingClicked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,28 +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
|
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import io.mockk.Runs
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.just
|
|
||||||
import io.mockk.mockk
|
|
||||||
import io.reactivex.android.plugins.RxAndroidPlugins
|
|
||||||
import io.reactivex.plugins.RxJavaPlugins
|
|
||||||
import io.reactivex.schedulers.Schedulers
|
|
||||||
import org.mozilla.fenix.mvi.ActionBusFactory
|
|
||||||
|
|
||||||
object TestUtils {
|
|
||||||
fun setRxSchedulers() {
|
|
||||||
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
|
|
||||||
RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }
|
|
||||||
}
|
|
||||||
|
|
||||||
val owner = mockk<LifecycleOwner> {
|
|
||||||
every { lifecycle } returns mockk()
|
|
||||||
every { lifecycle.addObserver(any()) } just Runs
|
|
||||||
}
|
|
||||||
val bus: ActionBusFactory = ActionBusFactory.get(owner)
|
|
||||||
}
|
|
|
@ -29,6 +29,7 @@ import mozilla.components.browser.session.SessionManager
|
||||||
import mozilla.components.concept.engine.EngineView
|
import mozilla.components.concept.engine.EngineView
|
||||||
import mozilla.components.feature.search.SearchUseCases
|
import mozilla.components.feature.search.SearchUseCases
|
||||||
import mozilla.components.feature.session.SessionUseCases
|
import mozilla.components.feature.session.SessionUseCases
|
||||||
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
import mozilla.components.feature.tabs.TabsUseCases
|
import mozilla.components.feature.tabs.TabsUseCases
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
@ -48,8 +49,7 @@ import org.mozilla.fenix.components.metrics.MetricController
|
||||||
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.ext.toTab
|
||||||
import org.mozilla.fenix.home.sessioncontrol.Tab
|
import org.mozilla.fenix.home.Tab
|
||||||
import org.mozilla.fenix.home.sessioncontrol.TabCollection
|
|
||||||
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
|
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
|
|
|
@ -0,0 +1,209 @@
|
||||||
|
/* 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.home
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.MainScope
|
||||||
|
import kotlinx.coroutines.ObsoleteCoroutinesApi
|
||||||
|
import kotlinx.coroutines.newSingleThreadContext
|
||||||
|
import kotlinx.coroutines.test.resetMain
|
||||||
|
import kotlinx.coroutines.test.setMain
|
||||||
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
|
import org.junit.After
|
||||||
|
import mozilla.components.feature.tab.collections.Tab as ComponentTab
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mozilla.fenix.BrowserDirection
|
||||||
|
import org.mozilla.fenix.HomeActivity
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
|
||||||
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
|
import org.mozilla.fenix.components.metrics.MetricController
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.ext.nav
|
||||||
|
import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
|
||||||
|
import org.mozilla.fenix.settings.SupportUtils
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
@UseExperimental(ObsoleteCoroutinesApi::class)
|
||||||
|
class DefaultSessionControlControllerTest {
|
||||||
|
|
||||||
|
private val mainThreadSurrogate = newSingleThreadContext("UI thread")
|
||||||
|
private val activity: HomeActivity = mockk(relaxed = true)
|
||||||
|
private val store: HomeFragmentStore = mockk(relaxed = true)
|
||||||
|
private val navController: NavController = mockk(relaxed = true)
|
||||||
|
private val homeLayout: MotionLayout = mockk(relaxed = true)
|
||||||
|
private val browsingModeManager: BrowsingModeManager = mockk(relaxed = true)
|
||||||
|
private val closeTab: (sessionId: String) -> Unit = mockk(relaxed = true)
|
||||||
|
private val closeAllTabs: (isPrivateMode: Boolean) -> Unit = mockk(relaxed = true)
|
||||||
|
private val getListOfTabs: () -> List<Tab> = { emptyList() }
|
||||||
|
private val hideOnboarding: () -> Unit = mockk(relaxed = true)
|
||||||
|
private val invokePendingDeleteJobs: () -> Unit = mockk(relaxed = true)
|
||||||
|
private val registerCollectionStorageObserver: () -> Unit = mockk(relaxed = true)
|
||||||
|
private val scrollToTheTop: () -> Unit = mockk(relaxed = true)
|
||||||
|
private val showDeleteCollectionPrompt: (tabCollection: TabCollection) -> Unit =
|
||||||
|
mockk(relaxed = true)
|
||||||
|
private val metrics: MetricController = mockk(relaxed = true)
|
||||||
|
private val state: HomeFragmentState = mockk(relaxed = true)
|
||||||
|
|
||||||
|
private lateinit var controller: DefaultSessionControlController
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
Dispatchers.setMain(mainThreadSurrogate)
|
||||||
|
|
||||||
|
every { store.state } returns state
|
||||||
|
every { state.collections } returns emptyList()
|
||||||
|
every { state.expandedCollections } returns emptySet()
|
||||||
|
every { state.mode } returns Mode.Normal
|
||||||
|
every { state.tabs } returns emptyList()
|
||||||
|
every { activity.components.analytics.metrics } returns metrics
|
||||||
|
|
||||||
|
controller = DefaultSessionControlController(
|
||||||
|
activity = activity,
|
||||||
|
store = store,
|
||||||
|
navController = navController,
|
||||||
|
homeLayout = homeLayout,
|
||||||
|
browsingModeManager = browsingModeManager,
|
||||||
|
lifecycleScope = MainScope(),
|
||||||
|
closeTab = closeTab,
|
||||||
|
closeAllTabs = closeAllTabs,
|
||||||
|
getListOfTabs = getListOfTabs,
|
||||||
|
hideOnboarding = hideOnboarding,
|
||||||
|
invokePendingDeleteJobs = invokePendingDeleteJobs,
|
||||||
|
registerCollectionStorageObserver = registerCollectionStorageObserver,
|
||||||
|
scrollToTheTop = scrollToTheTop,
|
||||||
|
showDeleteCollectionPrompt = showDeleteCollectionPrompt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
Dispatchers.resetMain() // reset main dispatcher to the original Main dispatcher
|
||||||
|
mainThreadSurrogate.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleCloseTab() {
|
||||||
|
val sessionId = "hello"
|
||||||
|
controller.handleCloseTab(sessionId)
|
||||||
|
verify { closeTab(sessionId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleCloseAllTabs() {
|
||||||
|
val isPrivateMode = true
|
||||||
|
controller.handleCloseAllTabs(isPrivateMode)
|
||||||
|
verify { closeAllTabs(isPrivateMode) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleCollectionAddTabTapped() {
|
||||||
|
val collection: TabCollection = mockk(relaxed = true)
|
||||||
|
controller.handleCollectionAddTabTapped(collection)
|
||||||
|
verify { metrics.track(Event.CollectionAddTabPressed) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleCollectionOpenTabClicked() {
|
||||||
|
val tab: ComponentTab = mockk(relaxed = true)
|
||||||
|
controller.handleCollectionOpenTabClicked(tab)
|
||||||
|
verify { invokePendingDeleteJobs() }
|
||||||
|
verify { metrics.track(Event.CollectionTabRestored) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleCollectionOpenTabsTapped() {
|
||||||
|
val collection: TabCollection = mockk(relaxed = true)
|
||||||
|
controller.handleCollectionOpenTabsTapped(collection)
|
||||||
|
verify { invokePendingDeleteJobs() }
|
||||||
|
verify { scrollToTheTop() }
|
||||||
|
verify { metrics.track(Event.CollectionAllTabsRestored) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleCollectionRemoveTab() {
|
||||||
|
val collection: TabCollection = mockk(relaxed = true)
|
||||||
|
val tab: ComponentTab = mockk(relaxed = true)
|
||||||
|
controller.handleCollectionRemoveTab(collection, tab)
|
||||||
|
verify { metrics.track(Event.CollectionTabRemoved) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleCollectionShareTabsClicked() {
|
||||||
|
val collection: TabCollection = mockk(relaxed = true)
|
||||||
|
controller.handleCollectionShareTabsClicked(collection)
|
||||||
|
verify { metrics.track(Event.CollectionShared) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleDeleteCollectionTapped() {
|
||||||
|
val collection: TabCollection = mockk(relaxed = true)
|
||||||
|
controller.handleDeleteCollectionTapped(collection)
|
||||||
|
verify { showDeleteCollectionPrompt(collection) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handlePrivateBrowsingLearnMoreClicked() {
|
||||||
|
controller.handlePrivateBrowsingLearnMoreClicked()
|
||||||
|
verify {
|
||||||
|
activity.openToBrowserAndLoad(
|
||||||
|
searchTermOrURL = SupportUtils.getGenericSumoURLForTopic
|
||||||
|
(SupportUtils.SumoTopic.PRIVATE_BROWSING_MYTHS),
|
||||||
|
newTab = true,
|
||||||
|
from = BrowserDirection.FromHome
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleRenameCollectionTapped() {
|
||||||
|
val collection: TabCollection = mockk(relaxed = true)
|
||||||
|
controller.handleRenameCollectionTapped(collection)
|
||||||
|
verify { metrics.track(Event.CollectionRenamePressed) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleSaveTabToCollection() {
|
||||||
|
controller.handleSaveTabToCollection(selectedTabId = null)
|
||||||
|
verify { invokePendingDeleteJobs() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleSelectTab() {
|
||||||
|
val tabView: View = mockk(relaxed = true)
|
||||||
|
val sessionId = "hello"
|
||||||
|
val directions = HomeFragmentDirections.actionHomeFragmentToBrowserFragment(null)
|
||||||
|
controller.handleSelectTab(tabView, sessionId)
|
||||||
|
verify { invokePendingDeleteJobs() }
|
||||||
|
verify { navController.nav(R.id.homeFragment, directions) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleShareTabs() {
|
||||||
|
controller.handleShareTabs()
|
||||||
|
verify { invokePendingDeleteJobs() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleStartBrowsingClicked() {
|
||||||
|
controller.handleStartBrowsingClicked()
|
||||||
|
verify { hideOnboarding() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToggleCollectionExpanded() {
|
||||||
|
val collection: TabCollection = mockk(relaxed = true)
|
||||||
|
controller.handleToggleCollectionExpanded(collection, true)
|
||||||
|
verify { store.dispatch(HomeFragmentAction.CollectionExpanded(collection, true)) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,161 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
package org.mozilla.fenix.home
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import assertk.assertThat
|
||||||
|
import assertk.assertions.isEqualTo
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
|
import mozilla.components.service.fxa.manager.FxaAccountManager
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||||
|
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.onboarding.FenixOnboarding
|
||||||
|
|
||||||
|
class HomeFragmentStoreTest {
|
||||||
|
|
||||||
|
private lateinit var context: Context
|
||||||
|
private lateinit var accountManager: FxaAccountManager
|
||||||
|
private lateinit var onboarding: FenixOnboarding
|
||||||
|
private lateinit var browsingModeManager: BrowsingModeManager
|
||||||
|
private lateinit var dispatchModeChanges: (mode: Mode) -> Unit
|
||||||
|
private lateinit var currentMode: CurrentMode
|
||||||
|
private lateinit var homeFragmentState: HomeFragmentState
|
||||||
|
private lateinit var homeFragmentStore: HomeFragmentStore
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
context = mockk(relaxed = true)
|
||||||
|
accountManager = mockk(relaxed = true)
|
||||||
|
onboarding = mockk(relaxed = true)
|
||||||
|
browsingModeManager = mockk(relaxed = true)
|
||||||
|
dispatchModeChanges = mockk(relaxed = true)
|
||||||
|
|
||||||
|
every { context.components.backgroundServices.accountManager } returns accountManager
|
||||||
|
every { onboarding.userHasBeenOnboarded() } returns true
|
||||||
|
every { browsingModeManager.mode } returns BrowsingMode.Normal
|
||||||
|
|
||||||
|
currentMode = CurrentMode(
|
||||||
|
context,
|
||||||
|
onboarding,
|
||||||
|
browsingModeManager,
|
||||||
|
dispatchModeChanges
|
||||||
|
)
|
||||||
|
|
||||||
|
homeFragmentState = HomeFragmentState(
|
||||||
|
collections = emptyList(),
|
||||||
|
expandedCollections = emptySet(),
|
||||||
|
mode = currentMode.getCurrentMode(),
|
||||||
|
tabs = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
homeFragmentStore = HomeFragmentStore(homeFragmentState)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test toggling the mode in HomeFragmentStore`() = runBlocking {
|
||||||
|
// Verify that the default mode and tab states of the HomeFragment are correct.
|
||||||
|
assertThat(homeFragmentStore.state.mode).isEqualTo(Mode.Normal)
|
||||||
|
assertEquals(0, homeFragmentStore.state.tabs.size)
|
||||||
|
|
||||||
|
// Change the HomeFragmentStore to Private mode.
|
||||||
|
homeFragmentStore.dispatch(HomeFragmentAction.ModeChange(Mode.Private)).join()
|
||||||
|
|
||||||
|
assertThat(homeFragmentStore.state.mode).isEqualTo(Mode.Private)
|
||||||
|
assertEquals(0, homeFragmentStore.state.tabs.size)
|
||||||
|
|
||||||
|
// Change the HomeFragmentStore back to Normal mode.
|
||||||
|
homeFragmentStore.dispatch(HomeFragmentAction.ModeChange(Mode.Normal)).join()
|
||||||
|
|
||||||
|
assertThat(homeFragmentStore.state.mode).isEqualTo(Mode.Normal)
|
||||||
|
assertEquals(0, homeFragmentStore.state.tabs.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test toggling the mode with tabs in HomeFragmentStore`() = runBlocking {
|
||||||
|
// Verify that the default mode and tab states of the HomeFragment are correct.
|
||||||
|
assertThat(homeFragmentStore.state.mode).isEqualTo(Mode.Normal)
|
||||||
|
assertEquals(0, homeFragmentStore.state.tabs.size)
|
||||||
|
|
||||||
|
// Add 2 Tabs to the HomeFragmentStore.
|
||||||
|
val tabs: List<Tab> = listOf(mockk(), mockk())
|
||||||
|
homeFragmentStore.dispatch(HomeFragmentAction.TabsChange(tabs)).join()
|
||||||
|
|
||||||
|
assertEquals(2, homeFragmentStore.state.tabs.size)
|
||||||
|
|
||||||
|
// Change the HomeFragmentStore to Private mode.
|
||||||
|
homeFragmentStore.dispatch(HomeFragmentAction.ModeChange(Mode.Private)).join()
|
||||||
|
|
||||||
|
assertThat(homeFragmentStore.state.mode).isEqualTo(Mode.Private)
|
||||||
|
assertEquals(0, homeFragmentStore.state.tabs.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test changing the collections in HomeFragmentStore`() = runBlocking {
|
||||||
|
assertEquals(0, homeFragmentStore.state.collections.size)
|
||||||
|
|
||||||
|
// Add 2 TabCollections to the HomeFragmentStore.
|
||||||
|
val tabCollections: List<TabCollection> = listOf(mockk(), mockk())
|
||||||
|
homeFragmentStore.dispatch(HomeFragmentAction.CollectionsChange(tabCollections)).join()
|
||||||
|
|
||||||
|
assertThat(homeFragmentStore.state.collections).isEqualTo(tabCollections)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test changing the tab in HomeFragmentStore`() = runBlocking {
|
||||||
|
assertEquals(0, homeFragmentStore.state.tabs.size)
|
||||||
|
|
||||||
|
val tab: Tab = mockk()
|
||||||
|
|
||||||
|
homeFragmentStore.dispatch(HomeFragmentAction.TabsChange(listOf(tab))).join()
|
||||||
|
|
||||||
|
assertTrue(homeFragmentStore.state.tabs.contains(tab))
|
||||||
|
assertEquals(1, homeFragmentStore.state.tabs.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test changing the expanded collections in HomeFragmentStore`() = runBlocking {
|
||||||
|
val collection: TabCollection = mockk<TabCollection>().apply {
|
||||||
|
every { id } returns 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand the given collection.
|
||||||
|
homeFragmentStore.dispatch(HomeFragmentAction.CollectionsChange(listOf(collection))).join()
|
||||||
|
homeFragmentStore.dispatch(HomeFragmentAction.CollectionExpanded(collection, true)).join()
|
||||||
|
|
||||||
|
assertTrue(homeFragmentStore.state.expandedCollections.contains(collection.id))
|
||||||
|
assertEquals(1, homeFragmentStore.state.expandedCollections.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Test changing the collections, mode and tabs in the HomeFragmentStore`() = runBlocking {
|
||||||
|
// Verify that the default state of the HomeFragment is correct.
|
||||||
|
assertEquals(0, homeFragmentStore.state.collections.size)
|
||||||
|
assertEquals(0, homeFragmentStore.state.tabs.size)
|
||||||
|
assertThat(homeFragmentStore.state.mode).isEqualTo(Mode.Normal)
|
||||||
|
|
||||||
|
val collections: List<TabCollection> = listOf(mockk())
|
||||||
|
val tabs: List<Tab> = listOf(mockk(), mockk())
|
||||||
|
|
||||||
|
homeFragmentStore.dispatch(
|
||||||
|
HomeFragmentAction.Change(
|
||||||
|
collections = collections,
|
||||||
|
mode = Mode.Private,
|
||||||
|
tabs = tabs
|
||||||
|
)
|
||||||
|
).join()
|
||||||
|
|
||||||
|
assertEquals(1, homeFragmentStore.state.collections.size)
|
||||||
|
assertThat(homeFragmentStore.state.mode).isEqualTo(Mode.Private)
|
||||||
|
assertEquals(2, homeFragmentStore.state.tabs.size)
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,6 @@ import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.spyk
|
import io.mockk.spyk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import io.reactivex.Observer
|
|
||||||
import mozilla.components.service.fxa.manager.FxaAccountManager
|
import mozilla.components.service.fxa.manager.FxaAccountManager
|
||||||
import mozilla.components.service.fxa.sharing.ShareableAccount
|
import mozilla.components.service.fxa.sharing.ShareableAccount
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
@ -18,7 +17,6 @@ import org.junit.Test
|
||||||
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.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.home.sessioncontrol.SessionControlChange
|
|
||||||
import org.mozilla.fenix.onboarding.FenixOnboarding
|
import org.mozilla.fenix.onboarding.FenixOnboarding
|
||||||
|
|
||||||
class ModeTest {
|
class ModeTest {
|
||||||
|
@ -27,8 +25,8 @@ class ModeTest {
|
||||||
private lateinit var accountManager: FxaAccountManager
|
private lateinit var accountManager: FxaAccountManager
|
||||||
private lateinit var onboarding: FenixOnboarding
|
private lateinit var onboarding: FenixOnboarding
|
||||||
private lateinit var browsingModeManager: BrowsingModeManager
|
private lateinit var browsingModeManager: BrowsingModeManager
|
||||||
private lateinit var emitter: Observer<SessionControlChange>
|
|
||||||
private lateinit var currentMode: CurrentMode
|
private lateinit var currentMode: CurrentMode
|
||||||
|
private lateinit var dispatchModeChanges: (mode: Mode) -> Unit
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
|
@ -36,7 +34,7 @@ class ModeTest {
|
||||||
accountManager = mockk(relaxed = true)
|
accountManager = mockk(relaxed = true)
|
||||||
onboarding = mockk(relaxed = true)
|
onboarding = mockk(relaxed = true)
|
||||||
browsingModeManager = mockk(relaxed = true)
|
browsingModeManager = mockk(relaxed = true)
|
||||||
emitter = mockk(relaxed = true)
|
dispatchModeChanges = mockk(relaxed = true)
|
||||||
|
|
||||||
every { context.components.backgroundServices.accountManager } returns accountManager
|
every { context.components.backgroundServices.accountManager } returns accountManager
|
||||||
|
|
||||||
|
@ -44,7 +42,7 @@ class ModeTest {
|
||||||
context,
|
context,
|
||||||
onboarding,
|
onboarding,
|
||||||
browsingModeManager,
|
browsingModeManager,
|
||||||
emitter
|
dispatchModeChanges
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,7 +99,7 @@ class ModeTest {
|
||||||
|
|
||||||
currentMode.emitModeChanges()
|
currentMode.emitModeChanges()
|
||||||
|
|
||||||
verify { emitter.onNext(SessionControlChange.ModeChange(Mode.Normal)) }
|
verify { dispatchModeChanges(Mode.Normal) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -0,0 +1,142 @@
|
||||||
|
/* 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.home
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import mozilla.components.feature.tab.collections.Tab
|
||||||
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
|
||||||
|
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
|
||||||
|
|
||||||
|
class SessionControlInteractorTest {
|
||||||
|
|
||||||
|
private val controller: DefaultSessionControlController = mockk(relaxed = true)
|
||||||
|
|
||||||
|
private lateinit var interactor: SessionControlInteractor
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
interactor = SessionControlInteractor(controller)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onCloseTab() {
|
||||||
|
val sessionId = "hello"
|
||||||
|
interactor.onCloseTab(sessionId)
|
||||||
|
verify { controller.handleCloseTab(sessionId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onCloseAllTabs() {
|
||||||
|
val isPrivateMode = true
|
||||||
|
interactor.onCloseAllTabs(isPrivateMode)
|
||||||
|
verify { controller.handleCloseAllTabs(isPrivateMode) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onCollectionAddTabTapped() {
|
||||||
|
val collection: TabCollection = mockk(relaxed = true)
|
||||||
|
interactor.onCollectionAddTabTapped(collection)
|
||||||
|
verify { controller.handleCollectionAddTabTapped(collection) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onCollectionOpenTabClicked() {
|
||||||
|
val tab: Tab = mockk(relaxed = true)
|
||||||
|
interactor.onCollectionOpenTabClicked(tab)
|
||||||
|
verify { controller.handleCollectionOpenTabClicked(tab) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onCollectionOpenTabsTapped() {
|
||||||
|
val collection: TabCollection = mockk(relaxed = true)
|
||||||
|
interactor.onCollectionOpenTabsTapped(collection)
|
||||||
|
verify { controller.handleCollectionOpenTabsTapped(collection) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onCollectionRemoveTab() {
|
||||||
|
val collection: TabCollection = mockk(relaxed = true)
|
||||||
|
val tab: Tab = mockk(relaxed = true)
|
||||||
|
interactor.onCollectionRemoveTab(collection, tab)
|
||||||
|
verify { controller.handleCollectionRemoveTab(collection, tab) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onCollectionShareTabsClicked() {
|
||||||
|
val collection: TabCollection = mockk(relaxed = true)
|
||||||
|
interactor.onCollectionShareTabsClicked(collection)
|
||||||
|
verify { controller.handleCollectionShareTabsClicked(collection) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onDeleteCollectionTapped() {
|
||||||
|
val collection: TabCollection = mockk(relaxed = true)
|
||||||
|
interactor.onDeleteCollectionTapped(collection)
|
||||||
|
verify { controller.handleDeleteCollectionTapped(collection) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onPauseMediaClicked() {
|
||||||
|
interactor.onPauseMediaClicked()
|
||||||
|
verify { controller.handlePauseMediaClicked() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onPlayMediaClicked() {
|
||||||
|
interactor.onPlayMediaClicked()
|
||||||
|
verify { controller.handlePlayMediaClicked() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onPrivateBrowsingLearnMoreClicked() {
|
||||||
|
interactor.onPrivateBrowsingLearnMoreClicked()
|
||||||
|
verify { controller.handlePrivateBrowsingLearnMoreClicked() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onRenameCollectionTapped() {
|
||||||
|
val collection: TabCollection = mockk(relaxed = true)
|
||||||
|
interactor.onRenameCollectionTapped(collection)
|
||||||
|
verify { controller.handleRenameCollectionTapped(collection) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onSaveToCollection() {
|
||||||
|
interactor.onSaveToCollection(null)
|
||||||
|
verify { controller.handleSaveTabToCollection(null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onSelectTab() {
|
||||||
|
val tabView: View = mockk(relaxed = true)
|
||||||
|
val sessionId = "hello"
|
||||||
|
interactor.onSelectTab(tabView, sessionId)
|
||||||
|
verify { controller.handleSelectTab(tabView, sessionId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onShareTabs() {
|
||||||
|
interactor.onShareTabs()
|
||||||
|
verify { controller.handleShareTabs() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onStartBrowsingClicked() {
|
||||||
|
interactor.onStartBrowsingClicked()
|
||||||
|
verify { controller.handleStartBrowsingClicked() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onToggleCollectionExpanded() {
|
||||||
|
val collection: TabCollection = mockk(relaxed = true)
|
||||||
|
interactor.onToggleCollectionExpanded(collection, true)
|
||||||
|
verify { controller.handleToggleCollectionExpanded(collection, true) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,49 +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/. */
|
|
||||||
|
|
||||||
apply plugin: 'com.android.library'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
apply plugin: 'kotlin-android-extensions'
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdkVersion 28
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdkVersion 21
|
|
||||||
targetSdkVersion 28
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
consumerProguardFiles 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
androidExtensions {
|
|
||||||
experimental = true
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation Deps.kotlin_stdlib
|
|
||||||
|
|
||||||
implementation Deps.androidx_annotation
|
|
||||||
implementation Deps.androidx_lifecycle_livedata
|
|
||||||
implementation Deps.androidx_lifecycle_viewmodel
|
|
||||||
implementation Deps.mozilla_support_base
|
|
||||||
|
|
||||||
implementation Deps.rxAndroid
|
|
||||||
implementation Deps.rxKotlin
|
|
||||||
|
|
||||||
implementation Deps.autodispose
|
|
||||||
implementation Deps.autodispose_android
|
|
||||||
implementation Deps.autodispose_android_aac
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
# Add project specific ProGuard rules here.
|
|
||||||
# You can control the set of applied configuration files using the
|
|
||||||
# proguardFiles setting in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
|
||||||
# debugging stack traces.
|
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
|
||||||
# hide the original source file name.
|
|
||||||
#-renamesourcefileattribute SourceFile
|
|
||||||
-keep class org.mozilla.fenix.mvi.** { *; }
|
|
|
@ -1,5 +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/. -->
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="org.mozilla.fenix.mvi"/>
|
|
|
@ -1,176 +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/. */
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) 2018 Netflix, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
* Created by Juliano Moraes, Rohan Dhruva, Emmanuel Boudrant.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.mozilla.fenix.mvi
|
|
||||||
|
|
||||||
import androidx.annotation.VisibleForTesting
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.LifecycleObserver
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.OnLifecycleEvent
|
|
||||||
import com.uber.autodispose.AutoDispose.autoDisposable
|
|
||||||
import com.uber.autodispose.ObservableSubscribeProxy
|
|
||||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider
|
|
||||||
import io.reactivex.Observable
|
|
||||||
import io.reactivex.Observer
|
|
||||||
import io.reactivex.rxkotlin.merge
|
|
||||||
import io.reactivex.subjects.PublishSubject
|
|
||||||
import io.reactivex.subjects.Subject
|
|
||||||
|
|
||||||
/**
|
|
||||||
* It implements a Factory pattern generating Rx Subjects based on Event Types.
|
|
||||||
* It maintain a map of Rx Subjects, one per type per instance of ActionBusFactory.
|
|
||||||
*
|
|
||||||
* @param owner is a LifecycleOwner used to auto dispose based on destroy observable
|
|
||||||
*/
|
|
||||||
class ActionBusFactory private constructor(val owner: LifecycleOwner) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private val buses = mutableMapOf<LifecycleOwner, ActionBusFactory>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the [ActionBusFactory] associated to the [LifecycleOwner]. It there is no bus it will create one.
|
|
||||||
* If the [LifecycleOwner] used is a fragment it use [Fragment#getViewLifecycleOwner()]
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun get(lifecycleOwner: LifecycleOwner): ActionBusFactory {
|
|
||||||
return with(lifecycleOwner) {
|
|
||||||
var bus = buses[lifecycleOwner]
|
|
||||||
if (bus == null) {
|
|
||||||
bus = ActionBusFactory(lifecycleOwner)
|
|
||||||
buses[lifecycleOwner] = bus
|
|
||||||
// LifecycleOwner
|
|
||||||
lifecycleOwner.lifecycle.addObserver(bus.observer)
|
|
||||||
}
|
|
||||||
bus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
|
||||||
val map = HashMap<Class<*>, Subject<*>>()
|
|
||||||
|
|
||||||
internal val observer = object : LifecycleObserver {
|
|
||||||
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
|
||||||
fun onDestroy() {
|
|
||||||
map.forEach { entry -> entry.value.onComplete() }
|
|
||||||
buses.remove(owner)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <T> create(clazz: Class<T>): Subject<T> {
|
|
||||||
val subject = PublishSubject.create<T>().toSerialized()
|
|
||||||
map[clazz] = subject
|
|
||||||
return subject
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* emit will create (if needed) or use the existing Rx Subject to send events.
|
|
||||||
*
|
|
||||||
* @param clazz is the Event Class
|
|
||||||
* @param event is the instance of the Event to be sent
|
|
||||||
*/
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun <T : Action> emit(clazz: Class<T>, event: T) {
|
|
||||||
val subject = if (map[clazz] != null) map[clazz] else create(clazz)
|
|
||||||
(subject as Subject<T>).onNext(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**x
|
|
||||||
* getSafeManagedObservable returns an Rx Observable which is
|
|
||||||
* *Safe* against reentrant events as it is serialized and
|
|
||||||
* *Managed* since it disposes itself based on the lifecycle
|
|
||||||
*
|
|
||||||
* @param clazz is the class of the event type used by this observable
|
|
||||||
*/
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun <T : Action> getSafeManagedObservable(clazz: Class<T>): Observable<T> {
|
|
||||||
return if (map[clazz] != null) map[clazz] as Observable<T> else create(clazz)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T : Action> getAutoDisposeObservable(clazz: Class<T>): ObservableSubscribeProxy<T> {
|
|
||||||
return getSafeManagedObservable(clazz)
|
|
||||||
.`as`(autoDisposable(AndroidLifecycleScopeProvider.from(owner)))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun <T : Action> getManagedEmitter(clazz: Class<T>): Observer<T> {
|
|
||||||
return if (map[clazz] != null) map[clazz] as Observer<T> else create(clazz)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun logMergedObservables() {
|
|
||||||
// TODO make this observe new items in the map and combine them
|
|
||||||
map.values.merge().compose(logState()).subscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getDestroyObservable observes to Lifecycle owner and fires during ON_DESTROY
|
|
||||||
*/
|
|
||||||
fun getDestroyObservable(): Observable<Unit> {
|
|
||||||
return owner.createDestroyObservable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extension on [LifecycleOwner] used to emit an event.
|
|
||||||
*/
|
|
||||||
inline fun <reified T : Action> LifecycleOwner.emit(event: T) =
|
|
||||||
with(ActionBusFactory.get(this)) {
|
|
||||||
getSafeManagedObservable(T::class.java)
|
|
||||||
emit(T::class.java, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extension on [LifecycleOwner] used used to get the state observable.
|
|
||||||
*/
|
|
||||||
inline fun <reified T : Action> LifecycleOwner.getSafeManagedObservable(): Observable<T> =
|
|
||||||
ActionBusFactory.get(this).getSafeManagedObservable(T::class.java)
|
|
||||||
|
|
||||||
inline fun <reified T : Action> LifecycleOwner.getAutoDisposeObservable(): ObservableSubscribeProxy<T> =
|
|
||||||
ActionBusFactory.get(this).getAutoDisposeObservable(T::class.java)
|
|
||||||
|
|
||||||
inline fun <reified T : Action> LifecycleOwner.getManagedEmitter(): Observer<T> =
|
|
||||||
ActionBusFactory.get(this).getManagedEmitter(T::class.java)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method returns a destroy observable that can be passed to [org.mozilla.fenix.mvi.UIView]s as needed.
|
|
||||||
*/
|
|
||||||
fun LifecycleOwner?.createDestroyObservable(): Observable<Unit> {
|
|
||||||
return Observable.create { emitter ->
|
|
||||||
if (this == null || this.lifecycle.currentState == Lifecycle.State.DESTROYED) {
|
|
||||||
emitter.onNext(Unit)
|
|
||||||
emitter.onComplete()
|
|
||||||
return@create
|
|
||||||
}
|
|
||||||
this.lifecycle.addObserver(object : LifecycleObserver {
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
|
||||||
fun emitDestroy() {
|
|
||||||
if (emitter.isDisposed) {
|
|
||||||
emitter.onNext(Unit)
|
|
||||||
emitter.onComplete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,49 +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.mvi
|
|
||||||
|
|
||||||
import io.reactivex.ObservableTransformer
|
|
||||||
import io.reactivex.subjects.Subject
|
|
||||||
import mozilla.components.support.base.log.logger.Logger
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An action is a command or intent the user performed
|
|
||||||
*/
|
|
||||||
interface Action
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Change is a change to the view coming from the model
|
|
||||||
* (Extending action so we can reuse the ActionBusFactory)
|
|
||||||
*/
|
|
||||||
interface Change : Action
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A ViewState is a model reflecting the current state of the view
|
|
||||||
*/
|
|
||||||
interface ViewState
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Reducer applies changes to the ViewState
|
|
||||||
*/
|
|
||||||
typealias Reducer<S, C> = (S, C) -> S
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple logger for tracking ViewState changes
|
|
||||||
*/
|
|
||||||
fun <S> logState(): ObservableTransformer<S, S> = ObservableTransformer { observable ->
|
|
||||||
observable.doOnNext {
|
|
||||||
if (BuildConfig.DEBUG) Logger("State").debug(it.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For capturing state to a Subject for testing
|
|
||||||
*/
|
|
||||||
fun <S> captureState(subject: Subject<S>):
|
|
||||||
ObservableTransformer<S, S> = ObservableTransformer { observable ->
|
|
||||||
observable.doOnNext {
|
|
||||||
subject.onNext(it)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,72 +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.mvi
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import io.reactivex.Observable
|
|
||||||
import io.reactivex.Observer
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.disposables.CompositeDisposable
|
|
||||||
import io.reactivex.rxkotlin.withLatestFrom
|
|
||||||
import io.reactivex.schedulers.Schedulers
|
|
||||||
import io.reactivex.subjects.BehaviorSubject
|
|
||||||
import io.reactivex.subjects.PublishSubject
|
|
||||||
|
|
||||||
interface UIComponentViewModel<S : ViewState, C : Change> {
|
|
||||||
val changes: Observer<C>
|
|
||||||
val state: Observable<S>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UIComponentViewModelProvider<S : ViewState, C : Change> {
|
|
||||||
fun fetchViewModel(): UIComponentViewModel<S, C>
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class UIComponent<S : ViewState, A : Action, C : Change>(
|
|
||||||
protected val actionEmitter: Observer<A>,
|
|
||||||
protected val changesObservable: Observable<C>,
|
|
||||||
private val viewModelProvider: UIComponentViewModelProvider<S, C>
|
|
||||||
) {
|
|
||||||
open val uiView: UIView<S, A, C> by lazy { initView() }
|
|
||||||
|
|
||||||
abstract fun initView(): UIView<S, A, C>
|
|
||||||
open fun getContainerId() = uiView.containerId
|
|
||||||
|
|
||||||
fun bind(): CompositeDisposable {
|
|
||||||
val compositeDisposable = CompositeDisposable()
|
|
||||||
val viewModel = viewModelProvider.fetchViewModel()
|
|
||||||
|
|
||||||
compositeDisposable.add(changesObservable.subscribe(viewModel.changes::onNext))
|
|
||||||
compositeDisposable.add(
|
|
||||||
viewModel
|
|
||||||
.state
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(uiView.updateView())
|
|
||||||
)
|
|
||||||
|
|
||||||
return compositeDisposable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class UIComponentViewModelBase<S : ViewState, C : Change>(
|
|
||||||
initialState: S,
|
|
||||||
reducer: Reducer<S, C>
|
|
||||||
) : ViewModel(), UIComponentViewModel<S, C> {
|
|
||||||
|
|
||||||
final override val changes: Observer<C>
|
|
||||||
private var _state: BehaviorSubject<S> = BehaviorSubject.createDefault(initialState)
|
|
||||||
override val state: Observable<S>
|
|
||||||
get() = _state
|
|
||||||
|
|
||||||
init {
|
|
||||||
changes = PublishSubject.create<C>()
|
|
||||||
|
|
||||||
changes
|
|
||||||
.withLatestFrom(_state)
|
|
||||||
.map { reducer(it.second, it.first) }
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.subscribe(_state)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,54 +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.mvi
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.annotation.IdRes
|
|
||||||
import io.reactivex.Observable
|
|
||||||
import io.reactivex.Observer
|
|
||||||
import io.reactivex.functions.Consumer
|
|
||||||
import kotlinx.android.extensions.LayoutContainer
|
|
||||||
|
|
||||||
abstract class UIView<S : ViewState, A : Action, C : Change>(
|
|
||||||
private val container: ViewGroup,
|
|
||||||
protected val actionEmitter: Observer<A>,
|
|
||||||
protected val changesObservable: Observable<C>
|
|
||||||
) : LayoutContainer {
|
|
||||||
|
|
||||||
abstract val view: View
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the XML id for the UIView
|
|
||||||
*/
|
|
||||||
@get:IdRes
|
|
||||||
val containerId: Int
|
|
||||||
get() = container.id
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides container to empower Kotlin Android Extensions
|
|
||||||
*/
|
|
||||||
override val containerView: View?
|
|
||||||
get() = container
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the UIView
|
|
||||||
*/
|
|
||||||
open fun show() {
|
|
||||||
view.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide the UIView
|
|
||||||
*/
|
|
||||||
open fun hide() {
|
|
||||||
view.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the view from the ViewState
|
|
||||||
*/
|
|
||||||
abstract fun updateView(): Consumer<S>
|
|
||||||
}
|
|
|
@ -53,7 +53,7 @@ task clean(type: Delete) {
|
||||||
detekt {
|
detekt {
|
||||||
// The version number is duplicated, please refer to plugins block for more details
|
// The version number is duplicated, please refer to plugins block for more details
|
||||||
version = "1.0.0-RC16"
|
version = "1.0.0-RC16"
|
||||||
input = files("$projectDir/app/src", "$projectDir/architecture/src")
|
input = files("$projectDir/app/src")
|
||||||
config = files("$projectDir/config/detekt.yml")
|
config = files("$projectDir/config/detekt.yml")
|
||||||
filters = ".*test.*,.*/resources/.*,.*/tmp/.*"
|
filters = ".*test.*,.*/resources/.*,.*/tmp/.*"
|
||||||
|
|
||||||
|
@ -80,5 +80,5 @@ task ktlint(type: JavaExec, group: "verification") {
|
||||||
description = "Check Kotlin code style."
|
description = "Check Kotlin code style."
|
||||||
classpath = configurations.ktlint
|
classpath = configurations.ktlint
|
||||||
main = "com.pinterest.ktlint.Main"
|
main = "com.pinterest.ktlint.Main"
|
||||||
args "app/src/**/*.kt", "architecture/src/**/*.kt"
|
args "app/src/**/*.kt"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
include ':app', ':architecture'
|
include ':app'
|
||||||
|
|
||||||
def log(message) {
|
def log(message) {
|
||||||
logger.lifecycle("[settings] ${message}")
|
logger.lifecycle("[settings] ${message}")
|
||||||
|
|
|
@ -52,6 +52,6 @@ jobs:
|
||||||
description: 'Running lint over all modules'
|
description: 'Running lint over all modules'
|
||||||
run:
|
run:
|
||||||
using: gradlew
|
using: gradlew
|
||||||
gradlew: ['lintDebug', 'app:lintGeckoNightlyDebug']
|
gradlew: ['lintGeckoNightlyDebug']
|
||||||
treeherder:
|
treeherder:
|
||||||
symbol: lint
|
symbol: lint
|
||||||
|
|
Loading…
Reference in New Issue