From 56b21426eb3c7398603eac383ab098c9b941dc38 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 4 Dec 2019 22:06:05 -0500 Subject: [PATCH] For #5574 - Migrate SessionControl to LibState (#6651) * 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.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.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 varient --- app/build.gradle | 3 +- .../mozilla/fenix/FenixViewModelProvider.kt | 34 -- .../mozilla/fenix/browser/BrowserFragment.kt | 17 +- .../CollectionCreationController.kt | 4 +- .../collections/CollectionCreationFragment.kt | 2 +- .../CollectionCreationInteractor.kt | 4 +- .../collections/CollectionCreationStore.kt | 2 +- .../CollectionCreationTabListAdapter.kt | 2 +- .../collections/CollectionCreationView.kt | 4 +- .../collections/SaveCollectionListAdapter.kt | 4 +- .../java/org/mozilla/fenix/ext/Session.kt | 2 +- .../org/mozilla/fenix/ext/TabCollection.kt | 2 +- .../org/mozilla/fenix/home/HomeFragment.kt | 407 +++++------------- .../mozilla/fenix/home/HomeFragmentStore.kt | 89 ++++ .../main/java/org/mozilla/fenix/home/Mode.kt | 6 +- .../sessioncontrol/SessionControlAdapter.kt | 19 +- .../sessioncontrol/SessionControlComponent.kt | 164 ------- .../SessionControlController.kt | 350 +++++++++++++++ .../SessionControlInteractor.kt | 228 ++++++++++ ...ControlUIView.kt => SessionControlView.kt} | 47 +- .../sessioncontrol/SwipeToDeleteCallback.kt | 7 +- .../viewholders/CollectionViewHolder.kt | 29 +- .../PrivateBrowsingDescriptionViewHolder.kt | 9 +- .../viewholders/SaveTabGroupViewHolder.kt | 10 +- .../viewholders/TabHeaderViewHolder.kt | 17 +- .../viewholders/TabInCollectionViewHolder.kt | 13 +- .../viewholders/TabViewHolder.kt | 53 +-- .../onboarding/OnboardingFinishViewHolder.kt | 9 +- .../java/org/mozilla/fenix/test/OpenClass.kt | 0 .../test/java/org/mozilla/fenix/TestUtils.kt | 28 -- .../DefaultBrowserToolbarControllerTest.kt | 4 +- .../DefaultSessionControlControllerTest.kt | 209 +++++++++ .../fenix/home/HomeFragmentStoreTest.kt | 161 +++++++ .../java/org/mozilla/fenix/home/ModeTest.kt | 10 +- .../home/SessionControlInteractorTest.kt | 142 ++++++ architecture/build.gradle | 49 --- architecture/proguard-rules.pro | 22 - architecture/src/main/AndroidManifest.xml | 5 - .../org/mozilla/fenix/mvi/ActionBusFactory.kt | 176 -------- .../java/org/mozilla/fenix/mvi/Concepts.kt | 49 --- .../java/org/mozilla/fenix/mvi/UIComponent.kt | 72 ---- .../main/java/org/mozilla/fenix/mvi/UIView.kt | 54 --- build.gradle | 4 +- settings.gradle | 2 +- taskcluster/ci/lint/kind.yml | 2 +- 45 files changed, 1384 insertions(+), 1142 deletions(-) delete mode 100644 app/src/main/java/org/mozilla/fenix/FenixViewModelProvider.kt create mode 100644 app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt delete mode 100644 app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlComponent.kt create mode 100644 app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt create mode 100644 app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt rename app/src/main/java/org/mozilla/fenix/home/sessioncontrol/{SessionControlUIView.kt => SessionControlView.kt} (79%) rename {architecture => app}/src/main/java/org/mozilla/fenix/test/OpenClass.kt (100%) delete mode 100644 app/src/test/java/org/mozilla/fenix/TestUtils.kt create mode 100644 app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/home/HomeFragmentStoreTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt delete mode 100644 architecture/build.gradle delete mode 100644 architecture/proguard-rules.pro delete mode 100644 architecture/src/main/AndroidManifest.xml delete mode 100644 architecture/src/main/java/org/mozilla/fenix/mvi/ActionBusFactory.kt delete mode 100644 architecture/src/main/java/org/mozilla/fenix/mvi/Concepts.kt delete mode 100644 architecture/src/main/java/org/mozilla/fenix/mvi/UIComponent.kt delete mode 100644 architecture/src/main/java/org/mozilla/fenix/mvi/UIView.kt diff --git a/app/build.gradle b/app/build.gradle index d15863b67..097d35a47 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -363,8 +363,6 @@ androidExtensions { } dependencies { - implementation project(':architecture') - geckoNightlyImplementation Deps.mozilla_browser_engine_gecko_nightly geckoBetaImplementation Deps.mozilla_browser_engine_gecko_beta @@ -437,6 +435,7 @@ dependencies { implementation Deps.mozilla_service_glean implementation Deps.mozilla_service_experiments + implementation Deps.mozilla_support_base implementation Deps.mozilla_support_ktx implementation Deps.mozilla_support_rustlog implementation Deps.mozilla_support_utils diff --git a/app/src/main/java/org/mozilla/fenix/FenixViewModelProvider.kt b/app/src/main/java/org/mozilla/fenix/FenixViewModelProvider.kt deleted file mode 100644 index c9d049795..000000000 --- a/app/src/main/java/org/mozilla/fenix/FenixViewModelProvider.kt +++ /dev/null @@ -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 > create( - fragment: Fragment, - modelClass: Class, - viewModelCreator: () -> T - ): UIComponentViewModelProvider { - val factory = object : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return viewModelCreator() as T - } - } - - return object : UIComponentViewModelProvider { - override fun fetchViewModel(): T { - return ViewModelProvider(fragment, factory).get(modelClass) - } - } - } -} diff --git a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt index a318ad464..4bc44b588 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -14,7 +14,6 @@ import android.widget.Button import android.widget.RadioButton import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat -import androidx.lifecycle.Observer import androidx.transition.TransitionInflater import com.google.android.material.snackbar.Snackbar 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.session.TrackingProtectionUseCases import mozilla.components.feature.sitepermissions.SitePermissions +import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tabs.WindowFeature import mozilla.components.lib.state.ext.consumeFrom 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.requireComponents 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 /** @@ -119,7 +116,6 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { override fun onStart() { super.onStart() - subscribeToTabCollections() val toolbarSessionObserver = TrackingProtectionOverlay( context = requireContext(), 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().onNext( - SessionControlChange.CollectionsChange( - it - ) - ) - }) - } - private val collectionStorageObserver = object : TabCollectionStorage.Observer { override fun onCollectionCreated(title: String, sessions: List) { showTabSavedToCollectionSnackbar() diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationController.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationController.kt index fcd3e269e..5e42c3852 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationController.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationController.kt @@ -17,8 +17,8 @@ import org.mozilla.fenix.R import org.mozilla.fenix.components.Analytics import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.metrics.Event -import org.mozilla.fenix.home.sessioncontrol.Tab -import org.mozilla.fenix.home.sessioncontrol.toSessionBundle +import org.mozilla.fenix.home.Tab +import org.mozilla.fenix.home.toSessionBundle interface CollectionCreationController { diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationFragment.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationFragment.kt index b510e4436..c421b1f7f 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationFragment.kt @@ -22,7 +22,7 @@ import org.mozilla.fenix.R import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.toTab -import org.mozilla.fenix.home.sessioncontrol.Tab +import org.mozilla.fenix.home.Tab @ExperimentalCoroutinesApi class CollectionCreationFragment : DialogFragment() { diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationInteractor.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationInteractor.kt index 1dba731c7..962bc3196 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationInteractor.kt @@ -6,8 +6,8 @@ package org.mozilla.fenix.collections -import org.mozilla.fenix.home.sessioncontrol.Tab -import org.mozilla.fenix.home.sessioncontrol.TabCollection +import mozilla.components.feature.tab.collections.TabCollection +import org.mozilla.fenix.home.Tab interface CollectionCreationInteractor { diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationStore.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationStore.kt index f23f28ea7..5f3cac998 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationStore.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationStore.kt @@ -9,7 +9,7 @@ import mozilla.components.lib.state.Action import mozilla.components.lib.state.State import mozilla.components.lib.state.Store import org.mozilla.fenix.collections.CollectionCreationAction.StepChanged -import org.mozilla.fenix.home.sessioncontrol.Tab +import org.mozilla.fenix.home.Tab class CollectionCreationStore( initialState: CollectionCreationState diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationTabListAdapter.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationTabListAdapter.kt index c52e1f994..a936608b2 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationTabListAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationTabListAdapter.kt @@ -15,7 +15,7 @@ import kotlinx.android.synthetic.main.collection_tab_list_row.view.* import org.mozilla.fenix.R import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.loadIntoView -import org.mozilla.fenix.home.sessioncontrol.Tab +import org.mozilla.fenix.home.Tab class CollectionCreationTabListAdapter( private val interactor: CollectionCreationInteractor diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationView.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationView.kt index 84fa051aa..4286c22e6 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationView.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationView.kt @@ -21,6 +21,7 @@ import androidx.transition.TransitionManager import kotlinx.android.extensions.LayoutContainer import kotlinx.android.synthetic.main.component_collection_creation.* import kotlinx.android.synthetic.main.component_collection_creation.view.* +import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.android.view.showKeyboard 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.increaseTapArea import org.mozilla.fenix.ext.toShortUrl -import org.mozilla.fenix.home.sessioncontrol.Tab -import org.mozilla.fenix.home.sessioncontrol.TabCollection +import org.mozilla.fenix.home.Tab @SuppressWarnings("LargeClass") class CollectionCreationView( diff --git a/app/src/main/java/org/mozilla/fenix/collections/SaveCollectionListAdapter.kt b/app/src/main/java/org/mozilla/fenix/collections/SaveCollectionListAdapter.kt index 484ad5c21..fdc4e952d 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/SaveCollectionListAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/SaveCollectionListAdapter.kt @@ -10,11 +10,11 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView 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.components.description import org.mozilla.fenix.ext.getIconColor -import org.mozilla.fenix.home.sessioncontrol.Tab -import org.mozilla.fenix.home.sessioncontrol.TabCollection +import org.mozilla.fenix.home.Tab class SaveCollectionListAdapter( private val interactor: CollectionCreationInteractor diff --git a/app/src/main/java/org/mozilla/fenix/ext/Session.kt b/app/src/main/java/org/mozilla/fenix/ext/Session.kt index aa615e965..1474de5ea 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/Session.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/Session.kt @@ -8,7 +8,7 @@ import android.content.Context import mozilla.components.browser.session.Session import mozilla.components.feature.media.state.MediaState import mozilla.components.lib.publicsuffixlist.PublicSuffixList -import org.mozilla.fenix.home.sessioncontrol.Tab +import org.mozilla.fenix.home.Tab fun Session.toTab(context: Context, selected: Boolean? = null, mediaState: MediaState? = null): Tab = this.toTab(context.components.publicSuffixList, selected, mediaState) diff --git a/app/src/main/java/org/mozilla/fenix/ext/TabCollection.kt b/app/src/main/java/org/mozilla/fenix/ext/TabCollection.kt index b871cb6c9..8f66bd933 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/TabCollection.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/TabCollection.kt @@ -7,8 +7,8 @@ package org.mozilla.fenix.ext import android.content.Context import androidx.annotation.ColorInt import androidx.core.content.ContextCompat +import mozilla.components.feature.tab.collections.TabCollection import org.mozilla.fenix.R -import org.mozilla.fenix.home.sessioncontrol.TabCollection import kotlin.math.abs /** diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index 6b84b81d2..6f6a771ba 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -36,22 +36,20 @@ import androidx.transition.TransitionInflater import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.fragment_home.* 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.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main import mozilla.appservices.places.BookmarkRoot import mozilla.components.browser.menu.BrowserMenu import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager -import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.OAuthAccount 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.MediaStateMachine 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.applyConstraintSet import org.mozilla.fenix.BrowserDirection -import org.mozilla.fenix.FenixViewModelProvider import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode -import org.mozilla.fenix.collections.SaveCollectionStep import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.PrivateShortcutCreateManager +import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.metrics.Event 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.settings import org.mozilla.fenix.ext.toTab -import org.mozilla.fenix.home.sessioncontrol.CollectionAction -import org.mozilla.fenix.home.sessioncontrol.OnboardingAction -import org.mozilla.fenix.home.sessioncontrol.SessionControlAction -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.DefaultSessionControlController +import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor +import org.mozilla.fenix.home.sessioncontrol.SessionControlView 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.settings.SupportUtils import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit @@ -100,11 +87,9 @@ import org.mozilla.fenix.utils.allowUndo import org.mozilla.fenix.whatsnew.WhatsNew import kotlin.math.min +@ExperimentalCoroutinesApi @SuppressWarnings("TooManyFunctions", "LargeClass") class HomeFragment : Fragment() { - - private val bus = ActionBusFactory.get(this) - private val browsingModeManager get() = (activity as HomeActivity).browsingModeManager private val singleSessionObserver = object : Session.Observer { @@ -142,7 +127,9 @@ class HomeFragment : Fragment() { data class PendingSessionDeletion(val deletionJob: (suspend () -> Unit), val sessionId: String) 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 override fun onCreate(savedInstanceState: Bundle?) { @@ -169,34 +156,49 @@ class HomeFragment : Fragment() { savedInstanceState: Bundle? ): View? { val view = inflater.inflate(R.layout.fragment_home, container, false) + val activity = activity as HomeActivity currentMode = CurrentMode( view.context, onboarding, browsingModeManager, - getManagedEmitter() + ::dispatchModeChanges ) - sessionControlComponent = SessionControlComponent( - view.homeLayout, - bus, - FenixViewModelProvider.create( - this, - SessionControlViewModel::class.java - ) { - SessionControlViewModel( - SessionControlState( - emptyList(), - emptySet(), - requireComponents.core.tabCollectionStorage.cachedTabCollections, - currentMode.getCurrentMode() - ) + homeFragmentStore = StoreProvider.get(this) { + HomeFragmentStore( + HomeFragmentState( + collections = requireComponents.core.tabCollectionStorage.cachedTabCollections, + expandedCollections = emptySet(), + mode = currentMode.getCurrentMode(), + tabs = emptyList() ) - } + ) + } + + 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 { - sessionControlComponent.view { + sessionControlView.view { connect( TOP to BOTTOM of view.wordmark_spacer, 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) return view } + @ExperimentalCoroutinesApi @SuppressWarnings("LongMethod") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -222,7 +223,7 @@ class HomeFragment : Fragment() { ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652 } homeViewModel.layoutManagerState?.also { parcelable -> - sessionControlComponent.view.layoutManager?.onRestoreInstanceState(parcelable) + sessionControlView.view.layoutManager?.onRestoreInstanceState(parcelable) } homeLayout?.progress = homeViewModel.motionLayoutProgress homeViewModel.layoutManagerState = null @@ -292,9 +293,8 @@ class HomeFragment : Fragment() { } if (onboarding.userHasBeenOnboarded()) { - getManagedEmitter().onNext( - SessionControlChange.ModeChange(Mode.fromBrowsingMode(newMode)) - ) + homeFragmentStore.dispatch( + HomeFragmentAction.ModeChange(Mode.fromBrowsingMode(newMode))) } } @@ -311,25 +311,14 @@ class HomeFragment : Fragment() { super.onStart() subscribeToTabCollections() - getAutoDisposeObservable() - .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 components = context.components - getManagedEmitter().onNext( - SessionControlChange.Change( - tabs = getListOfSessions().toTabs(), - mode = currentMode.getCurrentMode(), - collections = components.core.tabCollectionStorage.cachedTabCollections - ) - ) + homeFragmentStore.dispatch(HomeFragmentAction.Change( + collections = components.core.tabCollectionStorage.cachedTabCollections, + mode = currentMode.getCurrentMode(), + tabs = getListOfSessions().toTabs() + )) hideToolbar() @@ -357,103 +346,46 @@ class HomeFragment : Fragment() { requireComponents.core.tabCollectionStorage.unregister(collectionStorageObserver) } - private fun handleOnboardingAction(action: OnboardingAction) { - Do exhaustive when (action) { - is OnboardingAction.Finish -> { - homeLayout?.progress = 0F - hideOnboarding() + private fun closeTab(sessionId: String) { + val deletionJob = pendingSessionDeletion?.deletionJob + + if (deletionJob == null) { + removeTabWithUndo(sessionId, browsingModeManager.mode.isPrivate) + } else { + viewLifecycleOwner.lifecycleScope.launch { + deletionJob.invoke() + }.invokeOnCompletion { + pendingSessionDeletion = null + removeTabWithUndo(sessionId, browsingModeManager.mode.isPrivate) } } } - @SuppressWarnings("ComplexMethod", "LongMethod") - private fun handleTabAction(action: TabAction) { - Do exhaustive when (action) { - is TabAction.SaveTabGroup -> { - if (browsingModeManager.mode.isPrivate) return - invokePendingDeleteJobs() - saveTabToCollection(action.selectedTabSessionId) - } - is TabAction.Select -> { - invokePendingDeleteJobs() - val session = sessionManager.findSessionById(action.sessionId) - sessionManager.select(session!!) - val directions = HomeFragmentDirections.actionHomeFragmentToBrowserFragment(null) - val extras = - FragmentNavigator.Extras.Builder() - .addSharedElement( - 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 + private fun closeAllTabs(isPrivateMode: Boolean) { + val deletionJob = pendingSessionDeletion?.deletionJob + + if (deletionJob == null) { + removeAllTabsWithUndo( + sessionManager.sessionsOfType(private = isPrivateMode), + isPrivateMode + ) + } else { + viewLifecycleOwner.lifecycleScope.launch { + deletionJob.invoke() + }.invokeOnCompletion { + pendingSessionDeletion = null + removeAllTabsWithUndo( + sessionManager.sessionsOfType(private = isPrivateMode), + isPrivateMode ) } - - 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() { pendingSessionDeletion?.deletionJob?.let { viewLifecycleOwner.lifecycleScope.launch { @@ -472,7 +404,7 @@ class HomeFragment : Fragment() { } } - private fun createDeleteCollectionPrompt(tabCollection: TabCollection) { + private fun showDeleteCollectionPrompt(tabCollection: TabCollection) { val context = context ?: return AlertDialog.Builder(context).apply { val message = @@ -493,107 +425,6 @@ class HomeFragment : Fragment() { }.show() } - @SuppressWarnings("LongMethod") - private fun handleCollectionAction(action: CollectionAction) { - when (action) { - is CollectionAction.Expand -> { - getManagedEmitter() - .onNext(SessionControlChange.ExpansionChange(action.collection, true)) - } - is CollectionAction.Collapse -> { - getManagedEmitter() - .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() { invokePendingDeleteJobs() super.onStop() @@ -601,7 +432,7 @@ class HomeFragment : Fragment() { ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652 } homeViewModel.layoutManagerState = - sessionControlComponent.view.layoutManager?.onSaveInstanceState() + sessionControlView.view.layoutManager?.onSaveInstanceState() homeViewModel.motionLayoutProgress = homeLayout?.progress ?: 0F } @@ -725,20 +556,14 @@ class HomeFragment : Fragment() { private fun subscribeToTabCollections(): Observer> { return Observer> { requireComponents.core.tabCollectionStorage.cachedTabCollections = it - getManagedEmitter().onNext( - SessionControlChange.CollectionsChange( - it - ) - ) + homeFragmentStore.dispatch(HomeFragmentAction.CollectionsChange(it)) }.also { observer -> requireComponents.core.tabCollectionStorage.getCollections().observe(this, observer) } } private fun removeAllTabsWithUndo(listOfSessionsToDelete: Sequence, private: Boolean) { - val sessionManager = requireComponents.core.sessionManager - - getManagedEmitter().onNext(SessionControlChange.TabsChange(listOf())) + homeFragmentStore.dispatch(HomeFragmentAction.TabsChange(emptyList())) val deleteOperation: (suspend () -> Unit) = { listOfSessionsToDelete.forEach { @@ -802,11 +627,7 @@ class HomeFragment : Fragment() { } private fun emitSessionChanges() { - getManagedEmitter().onNext( - SessionControlChange.TabsChange( - getListOfSessions().toTabs() - ) - ) + homeFragmentStore.dispatch(HomeFragmentAction.TabsChange(getListOfTabs())) } private fun getListOfSessions(): List { @@ -815,48 +636,19 @@ class HomeFragment : Fragment() { .toList() } - private fun showCollectionCreationFragment( - step: SaveCollectionStep, - selectedTabIds: Array? = 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 getListOfTabs(): List { + return getListOfSessions().toTabs() } - private fun saveTabToCollection(selectedTabId: String?) { - val tabs = getListOfSessions().toTabs() - 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 registerCollectionStorageObserver() { + requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this) } - private fun share(data: List) { - val directions = HomeFragmentDirections.actionHomeFragmentToShareFragment( - data = data.toTypedArray() - ) - nav(R.id.homeFragment, directions) + private fun scrollToTheTop() { + lifecycleScope.launch(Main) { + delay(ANIM_SCROLL_DELAY) + sessionControlView.view.smoothScrollToPosition(0) + } } private fun scrollAndAnimateCollection( @@ -865,7 +657,7 @@ class HomeFragment : Fragment() { ) { if (view != null) { viewLifecycleOwner.lifecycleScope.launch { - val recyclerView = sessionControlComponent.view + val recyclerView = sessionControlView.view delay(ANIM_SCROLL_DELAY) val tabsSize = getListOfSessions().size @@ -908,7 +700,7 @@ class HomeFragment : Fragment() { private fun animateCollection(addedTabsSize: Int, indexOfCollection: Int) { viewLifecycleOwner.lifecycleScope.launch { val viewHolder = - sessionControlComponent.view.findViewHolderForAdapterPosition(indexOfCollection) + sessionControlView.view.findViewHolderForAdapterPosition(indexOfCollection) val border = (viewHolder as? CollectionViewHolder)?.view?.findViewById(R.id.selected_border) val listener = object : Animator.AnimatorListener { @@ -982,7 +774,6 @@ class HomeFragment : Fragment() { private const val FADE_ANIM_DURATION = 150L private const val ANIM_SNACKBAR_DELAY = 100L 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_Y_OFFSET = -20 } diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt new file mode 100644 index 000000000..99d5d2e4d --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt @@ -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( + 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.toSessionBundle(sessionManager: SessionManager): List { + 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, + val expandedCollections: Set, + val mode: Mode, + val tabs: List +) : State + +sealed class HomeFragmentAction : Action { + data class Change(val tabs: List, val mode: Mode, val collections: List) : + HomeFragmentAction() + data class CollectionExpanded(val collection: TabCollection, val expand: Boolean) : HomeFragmentAction() + data class CollectionsChange(val collections: List) : HomeFragmentAction() + data class ModeChange(val mode: Mode) : HomeFragmentAction() + data class TabsChange(val tabs: List) : 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) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/home/Mode.kt b/app/src/main/java/org/mozilla/fenix/home/Mode.kt index a20dd82dd..370e9a8cd 100644 --- a/app/src/main/java/org/mozilla/fenix/home/Mode.kt +++ b/app/src/main/java/org/mozilla/fenix/home/Mode.kt @@ -5,7 +5,6 @@ package org.mozilla.fenix.home import android.content.Context -import io.reactivex.Observer import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AuthType 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.BrowsingModeManager import org.mozilla.fenix.ext.components -import org.mozilla.fenix.home.sessioncontrol.SessionControlChange import org.mozilla.fenix.onboarding.FenixOnboarding /** @@ -49,7 +47,7 @@ class CurrentMode( private val context: Context, private val onboarding: FenixOnboarding, private val browsingModeManager: BrowsingModeManager, - private val emitter: Observer + private val dispatchModeChanges: (mode: Mode) -> Unit ) : AccountObserver { private val accountManager = context.components.backgroundServices.accountManager @@ -71,7 +69,7 @@ class CurrentMode( } fun emitModeChanges() { - emitter.onNext(SessionControlChange.ModeChange(getCurrentMode())) + dispatchModeChanges(getCurrentMode()) } override fun onAuthenticated(account: OAuthAccount, authType: AuthType) = emitModeChanges() diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt index af9a98173..abf1b51fd 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt @@ -14,10 +14,11 @@ import androidx.annotation.StringRes import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView -import io.reactivex.Observer import kotlinx.android.synthetic.main.tab_list_row.* 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.Tab import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.NoContentMessageViewHolder @@ -139,7 +140,7 @@ class AdapterItemDiffCallback : DiffUtil.ItemCallback() { } class SessionControlAdapter( - private val actionEmitter: Observer + private val interactor: SessionControlInteractor ) : ListAdapter(AdapterItemDiffCallback()) { // 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 { val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) return when (viewType) { - TabHeaderViewHolder.LAYOUT_ID -> TabHeaderViewHolder(view, actionEmitter) - TabViewHolder.LAYOUT_ID -> TabViewHolder(view, actionEmitter) - SaveTabGroupViewHolder.LAYOUT_ID -> SaveTabGroupViewHolder(view, actionEmitter) - PrivateBrowsingDescriptionViewHolder.LAYOUT_ID -> PrivateBrowsingDescriptionViewHolder(view, actionEmitter) + TabHeaderViewHolder.LAYOUT_ID -> TabHeaderViewHolder(view, interactor) + TabViewHolder.LAYOUT_ID -> TabViewHolder(view, interactor) + SaveTabGroupViewHolder.LAYOUT_ID -> SaveTabGroupViewHolder(view, interactor) + PrivateBrowsingDescriptionViewHolder.LAYOUT_ID -> PrivateBrowsingDescriptionViewHolder(view, interactor) NoContentMessageViewHolder.LAYOUT_ID -> NoContentMessageViewHolder(view) CollectionHeaderViewHolder.LAYOUT_ID -> CollectionHeaderViewHolder(view) - CollectionViewHolder.LAYOUT_ID -> CollectionViewHolder(view, actionEmitter) - TabInCollectionViewHolder.LAYOUT_ID -> TabInCollectionViewHolder(view, actionEmitter) + CollectionViewHolder.LAYOUT_ID -> CollectionViewHolder(view, interactor) + TabInCollectionViewHolder.LAYOUT_ID -> TabInCollectionViewHolder(view, interactor) OnboardingHeaderViewHolder.LAYOUT_ID -> OnboardingHeaderViewHolder(view) OnboardingSectionHeaderViewHolder.LAYOUT_ID -> OnboardingSectionHeaderViewHolder(view) OnboardingAutomaticSignInViewHolder.LAYOUT_ID -> OnboardingAutomaticSignInViewHolder(view) @@ -163,7 +164,7 @@ class SessionControlAdapter( OnboardingTrackingProtectionViewHolder.LAYOUT_ID -> OnboardingTrackingProtectionViewHolder(view) OnboardingPrivateBrowsingViewHolder.LAYOUT_ID -> OnboardingPrivateBrowsingViewHolder(view) OnboardingPrivacyNoticeViewHolder.LAYOUT_ID -> OnboardingPrivacyNoticeViewHolder(view) - OnboardingFinishViewHolder.LAYOUT_ID -> OnboardingFinishViewHolder(view, actionEmitter) + OnboardingFinishViewHolder.LAYOUT_ID -> OnboardingFinishViewHolder(view, interactor) else -> throw IllegalStateException() } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlComponent.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlComponent.kt deleted file mode 100644 index f0f719a9c..000000000 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlComponent.kt +++ /dev/null @@ -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 -) : - UIComponent( - 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.toSessionBundle(context: Context): MutableList = - this.toSessionBundle(context.components.core.sessionManager) - -fun List.toSessionBundle(sessionManager: SessionManager): MutableList { - val sessionBundle = mutableListOf() - this.forEach { - sessionManager.findSessionById(it.sessionId)?.let { session -> - sessionBundle.add(session) - } - } - return sessionBundle -} - -data class SessionControlState( - val tabs: List, - val expandedCollections: Set, - val collections: List, - 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.onNext(tabAction: TabAction) { - onNext(SessionControlAction.Tab(tabAction)) -} - -fun Observer.onNext(collectionAction: CollectionAction) { - onNext(SessionControlAction.Collection(collectionAction)) -} - -fun Observer.onNext(onboardingAction: OnboardingAction) { - onNext(SessionControlAction.Onboarding(onboardingAction)) -} - -sealed class SessionControlChange : Change { - data class Change(val tabs: List, val mode: Mode, val collections: List) : - SessionControlChange() - data class TabsChange(val tabs: List) : SessionControlChange() - data class ModeChange(val mode: Mode) : SessionControlChange() - data class CollectionsChange(val collections: List) : SessionControlChange() - data class ExpansionChange(val collection: TabCollection, val expand: Boolean) : SessionControlChange() -} - -class SessionControlViewModel( - initialState: SessionControlState -) : UIComponentViewModelBase(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 - ) - } - } - } -} diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt new file mode 100644 index 000000000..3137b1cc4 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt @@ -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, + 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? = 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) { + val directions = HomeFragmentDirections.actionHomeFragmentToShareFragment( + data = data.toTypedArray() + ) + navController.nav(R.id.homeFragment, directions) + } + + companion object { + private const val TAB_ITEM_TRANSITION_NAME = "tab_item" + } +} diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt new file mode 100644 index 000000000..a89ef75db --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt @@ -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) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlUIView.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt similarity index 79% rename from app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlUIView.kt rename to app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt index 673e4dad4..f19ec9ce3 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlUIView.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt @@ -6,17 +6,22 @@ package org.mozilla.fenix.home.sessioncontrol import android.os.Build import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup +import androidx.lifecycle.ProcessLifecycleOwner import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import io.reactivex.Observable -import io.reactivex.Observer -import io.reactivex.functions.Consumer +import kotlinx.android.extensions.LayoutContainer +import kotlinx.coroutines.ExperimentalCoroutinesApi +import mozilla.components.feature.tab.collections.TabCollection +import mozilla.components.lib.state.ext.consumeFrom 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.OnboardingState -import org.mozilla.fenix.mvi.UIView +import org.mozilla.fenix.home.Tab val noTabMessage = AdapterItem.NoContentMessage( R.drawable.ic_tabs, @@ -110,7 +115,7 @@ private fun onboardingAdapterItems(onboardingState: OnboardingState): List = when (mode) { +private fun HomeFragmentState.toAdapterList(): List = when (mode) { is Mode.Normal -> normalModeAdapterItems(tabs, collections, expandedCollections) is Mode.Private -> privateModeAdapterItems(tabs) 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) } -class SessionControlUIView( - container: ViewGroup, - actionEmitter: Observer, - changesObservable: Observable -) : - UIView( - container, - actionEmitter, - changesObservable - ) { +@ExperimentalCoroutinesApi +class SessionControlView( + private val homeFragmentStore: HomeFragmentStore, + private val container: ViewGroup, + interactor: SessionControlInteractor +) : LayoutContainer { + override val containerView: View? + get() = container - override val view: RecyclerView = LayoutInflater.from(container.context) + val view: RecyclerView = LayoutInflater.from(container.context) .inflate(R.layout.component_session_control, container, true) .findViewById(R.id.home_component) - private val sessionControlAdapter = SessionControlAdapter(actionEmitter) + private val sessionControlAdapter = SessionControlAdapter(interactor) init { view.apply { @@ -144,18 +147,22 @@ class SessionControlUIView( val itemTouchHelper = ItemTouchHelper( SwipeToDeleteCallback( - actionEmitter + interactor ) ) itemTouchHelper.attachToRecyclerView(this) + + view.consumeFrom(homeFragmentStore, ProcessLifecycleOwner.get()) { + update(it) + } } } - override fun updateView() = Consumer { + fun update(state: HomeFragmentState) { // Workaround for list not updating until scroll on Android 5 + 6 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { sessionControlAdapter.submitList(null) } - sessionControlAdapter.submitList(it.toAdapterList()) + sessionControlAdapter.submitList(state.toAdapterList()) } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SwipeToDeleteCallback.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SwipeToDeleteCallback.kt index b01cecb39..48d193e26 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SwipeToDeleteCallback.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SwipeToDeleteCallback.kt @@ -10,14 +10,13 @@ import android.graphics.drawable.Drawable import androidx.core.content.ContextCompat import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView -import io.reactivex.Observer import org.mozilla.fenix.R import org.mozilla.fenix.ext.getColorFromAttr import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.TabViewHolder class SwipeToDeleteCallback( - val actionEmitter: Observer + val interactor: SessionControlInteractor ) : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { override fun onMove( recyclerView: RecyclerView, @@ -30,9 +29,9 @@ class SwipeToDeleteCallback( override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { when (viewHolder) { - is TabViewHolder -> actionEmitter.onNext(TabAction.Close(viewHolder.tab?.sessionId!!)) + is TabViewHolder -> interactor.onCloseTab(viewHolder.tab?.sessionId!!) is TabInCollectionViewHolder -> { - actionEmitter.onNext(CollectionAction.RemoveTab(viewHolder.collection, viewHolder.tab)) + interactor.onCollectionRemoveTab(viewHolder.collection, viewHolder.tab) } } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/CollectionViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/CollectionViewHolder.kt index 293774726..d24e826a6 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/CollectionViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/CollectionViewHolder.kt @@ -9,25 +9,22 @@ import android.graphics.PorterDuff.Mode.SRC_IN import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import io.reactivex.Observer import kotlinx.android.extensions.LayoutContainer import kotlinx.android.synthetic.main.collection_home_list_row.* import kotlinx.android.synthetic.main.collection_home_list_row.view.* import mozilla.components.browser.menu.BrowserMenuBuilder import mozilla.components.browser.menu.item.SimpleBrowserMenuItem +import mozilla.components.feature.tab.collections.TabCollection import org.mozilla.fenix.R import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.components.description import org.mozilla.fenix.ext.getIconColor import org.mozilla.fenix.ext.increaseTapArea -import org.mozilla.fenix.home.sessioncontrol.CollectionAction -import org.mozilla.fenix.home.sessioncontrol.SessionControlAction -import org.mozilla.fenix.home.sessioncontrol.TabCollection -import org.mozilla.fenix.home.sessioncontrol.onNext +import org.mozilla.fenix.home.sessioncontrol.CollectionInteractor class CollectionViewHolder( val view: View, - val actionEmitter: Observer, + val interactor: CollectionInteractor, override val containerView: View? = view ) : RecyclerView.ViewHolder(view), LayoutContainer { @@ -40,10 +37,10 @@ class CollectionViewHolder( init { collectionMenu = CollectionItemMenu(view.context, sessionHasOpenTabs) { when (it) { - is CollectionItemMenu.Item.DeleteCollection -> actionEmitter.onNext(CollectionAction.Delete(collection)) - is CollectionItemMenu.Item.AddTab -> actionEmitter.onNext(CollectionAction.AddTab(collection)) - is CollectionItemMenu.Item.RenameCollection -> actionEmitter.onNext(CollectionAction.Rename(collection)) - is CollectionItemMenu.Item.OpenTabs -> actionEmitter.onNext(CollectionAction.OpenTabs(collection)) + is CollectionItemMenu.Item.DeleteCollection -> interactor.onDeleteCollectionTapped(collection) + is CollectionItemMenu.Item.AddTab -> interactor.onCollectionAddTabTapped(collection) + is CollectionItemMenu.Item.RenameCollection -> interactor.onRenameCollectionTapped(collection) + is CollectionItemMenu.Item.OpenTabs -> interactor.onCollectionOpenTabsTapped(collection) } } @@ -59,13 +56,13 @@ class CollectionViewHolder( collection_share_button.run { increaseTapArea(buttonIncreaseDps) setOnClickListener { - actionEmitter.onNext(CollectionAction.ShareTabs(collection)) + interactor.onCollectionShareTabsClicked(collection) } } view.clipToOutline = true 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 { const val buttonIncreaseDps = 16 const val EXPANDED_PADDING = 60 diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/PrivateBrowsingDescriptionViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/PrivateBrowsingDescriptionViewHolder.kt index 461570761..a271e40bd 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/PrivateBrowsingDescriptionViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/PrivateBrowsingDescriptionViewHolder.kt @@ -9,16 +9,13 @@ import android.text.method.LinkMovementMethod import android.text.style.UnderlineSpan import android.view.View import androidx.recyclerview.widget.RecyclerView -import io.reactivex.Observer import kotlinx.android.synthetic.main.private_browsing_description.view.* import org.mozilla.fenix.R -import org.mozilla.fenix.home.sessioncontrol.SessionControlAction -import org.mozilla.fenix.home.sessioncontrol.TabAction -import org.mozilla.fenix.home.sessioncontrol.onNext +import org.mozilla.fenix.home.sessioncontrol.TabSessionInteractor class PrivateBrowsingDescriptionViewHolder( view: View, - private val actionEmitter: Observer + private val interactor: TabSessionInteractor ) : RecyclerView.ViewHolder(view) { init { @@ -35,7 +32,7 @@ class PrivateBrowsingDescriptionViewHolder( movementMethod = LinkMovementMethod.getInstance() text = textWithLink setOnClickListener { - actionEmitter.onNext(TabAction.PrivateBrowsingLearnMore) + interactor.onPrivateBrowsingLearnMoreClicked() } } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/SaveTabGroupViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/SaveTabGroupViewHolder.kt index c8d48b351..6462645c3 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/SaveTabGroupViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/SaveTabGroupViewHolder.kt @@ -6,26 +6,22 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders import android.view.View import androidx.recyclerview.widget.RecyclerView -import io.reactivex.Observer import kotlinx.android.synthetic.main.save_tab_group_button.view.* import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.components -import org.mozilla.fenix.home.sessioncontrol.SessionControlAction -import org.mozilla.fenix.home.sessioncontrol.TabAction -import org.mozilla.fenix.home.sessioncontrol.onNext +import org.mozilla.fenix.home.sessioncontrol.TabSessionInteractor class SaveTabGroupViewHolder( view: View, - private val actionEmitter: Observer + private val interactor: TabSessionInteractor ) : RecyclerView.ViewHolder(view) { init { view.save_tab_group_button.setOnClickListener { view.context.components.analytics.metrics .track(Event.CollectionSaveButtonPressed(TELEMETRY_HOME_IDENTIFIER)) - - actionEmitter.onNext(TabAction.SaveTabGroup(selectedTabSessionId = null)) + interactor.onSaveToCollection(sessionId = null) } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabHeaderViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabHeaderViewHolder.kt index ce019a4aa..9d30f9b25 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabHeaderViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabHeaderViewHolder.kt @@ -10,7 +10,6 @@ import android.widget.PopupWindow import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView -import io.reactivex.Observer import kotlinx.android.synthetic.main.tab_header.view.* import mozilla.components.browser.menu.BrowserMenu 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.components.metrics.Event import org.mozilla.fenix.ext.components -import org.mozilla.fenix.home.sessioncontrol.SessionControlAction -import org.mozilla.fenix.home.sessioncontrol.TabAction -import org.mozilla.fenix.home.sessioncontrol.onNext +import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor class TabHeaderViewHolder( private val view: View, - private val actionEmitter: Observer + private val interactor: SessionControlInteractor ) : RecyclerView.ViewHolder(view) { private var isPrivate = false private var tabsMenu: TabHeaderMenu @@ -32,10 +29,10 @@ class TabHeaderViewHolder( init { tabsMenu = TabHeaderMenu(view.context, isPrivate) { when (it) { - is TabHeaderMenu.Item.Share -> actionEmitter.onNext(TabAction.ShareTabs) - is TabHeaderMenu.Item.CloseAll -> actionEmitter.onNext(TabAction.CloseAll(isPrivate)) + is TabHeaderMenu.Item.Share -> interactor.onShareTabs() + is TabHeaderMenu.Item.CloseAll -> interactor.onCloseAllTabs(isPrivate) is TabHeaderMenu.Item.SaveToCollection -> { - actionEmitter.onNext(TabAction.SaveTabGroup(null)) + interactor.onSaveToCollection(null) view.context.components.analytics.metrics .track(Event.CollectionSaveButtonPressed(TELEMETRY_HOME_MENU_IDENITIFIER)) } @@ -45,14 +42,14 @@ class TabHeaderViewHolder( view.apply { share_tabs_button.run { setOnClickListener { - actionEmitter.onNext(TabAction.ShareTabs) + interactor.onShareTabs() } } close_tabs_button.run { setOnClickListener { view.context.components.analytics.metrics.track(Event.PrivateBrowsingGarbageIconTapped) - actionEmitter.onNext(TabAction.CloseAll(true)) + interactor.onCloseAllTabs(true) } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabInCollectionViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabInCollectionViewHolder.kt index 94187b63a..bbe5a55c3 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabInCollectionViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabInCollectionViewHolder.kt @@ -9,9 +9,9 @@ import android.view.View import android.view.ViewOutlineProvider import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView -import io.reactivex.Observer import kotlinx.android.extensions.LayoutContainer import kotlinx.android.synthetic.main.tab_in_collection.* +import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.support.ktx.android.util.dpToFloat import org.jetbrains.anko.backgroundColor 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.loadIntoView import org.mozilla.fenix.ext.toShortUrl -import org.mozilla.fenix.home.sessioncontrol.CollectionAction -import org.mozilla.fenix.home.sessioncontrol.SessionControlAction -import org.mozilla.fenix.home.sessioncontrol.TabCollection -import org.mozilla.fenix.home.sessioncontrol.onNext +import org.mozilla.fenix.home.sessioncontrol.CollectionInteractor import mozilla.components.feature.tab.collections.Tab as ComponentTab class TabInCollectionViewHolder( val view: View, - val actionEmitter: Observer, + val interactor: CollectionInteractor, override val containerView: View? = view ) : RecyclerView.ViewHolder(view), LayoutContainer { @@ -53,12 +50,12 @@ class TabInCollectionViewHolder( } view.setOnClickListener { - actionEmitter.onNext(CollectionAction.OpenTab(tab)) + interactor.onCollectionOpenTabClicked(tab) } collection_tab_close_button.increaseTapArea(buttonIncreaseDps) collection_tab_close_button.setOnClickListener { - actionEmitter.onNext(CollectionAction.RemoveTab(collection, tab)) + interactor.onCollectionRemoveTab(collection, tab) } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabViewHolder.kt index 2c44004c0..6003f1e55 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TabViewHolder.kt @@ -4,17 +4,13 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders -import android.content.Context import android.graphics.Bitmap import android.graphics.Outline import android.view.View import android.view.ViewOutlineProvider import androidx.recyclerview.widget.RecyclerView -import io.reactivex.Observer import kotlinx.android.extensions.LayoutContainer 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.support.ktx.android.util.dpToFloat 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.increaseTapArea import org.mozilla.fenix.ext.loadIntoView -import org.mozilla.fenix.home.sessioncontrol.SessionControlAction -import org.mozilla.fenix.home.sessioncontrol.Tab -import org.mozilla.fenix.home.sessioncontrol.TabAction -import org.mozilla.fenix.home.sessioncontrol.onNext +import org.mozilla.fenix.home.Tab +import org.mozilla.fenix.home.sessioncontrol.TabSessionInteractor class TabViewHolder( view: View, - actionEmitter: Observer, + interactor: TabSessionInteractor, override val containerView: View? = view ) : RecyclerView.ViewHolder(view), LayoutContainer { internal var tab: Tab? = null - private var tabMenu: TabItemMenu init { - tabMenu = TabItemMenu(view.context) { - when (it) { - is TabItemMenu.Item.Share -> - actionEmitter.onNext(TabAction.Share(tab?.sessionId!!)) - } - } - item_tab.setOnClickListener { - actionEmitter.onNext(TabAction.Select(it, tab?.sessionId!!)) + interactor.onSelectTab(it, tab?.sessionId!!) } item_tab.setOnLongClickListener { view.context.components.analytics.metrics.track(Event.CollectionTabLongPressed) - actionEmitter.onNext(TabAction.SaveTabGroup(tab?.sessionId!!)) - true + interactor.onSaveToCollection(tab?.sessionId!!) + return@setOnLongClickListener true } close_tab_button.setOnClickListener { - actionEmitter.onNext(TabAction.Close(tab?.sessionId!!)) + interactor.onCloseTab(tab?.sessionId!!) } play_pause_button.increaseTapArea(PLAY_PAUSE_BUTTON_EXTRA_DPS) @@ -66,12 +52,12 @@ class TabViewHolder( when (tab?.mediaState) { is MediaState.Playing -> { it.context.components.analytics.metrics.track(Event.TabMediaPlay) - actionEmitter.onNext(TabAction.PauseMedia(tab?.sessionId!!)) + interactor.onPauseMediaClicked() } is MediaState.Paused -> { 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 } } - -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) - } - ) - } -} diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingFinishViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingFinishViewHolder.kt index bd44a5e0b..23cac243d 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingFinishViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingFinishViewHolder.kt @@ -6,21 +6,18 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding import android.view.View import androidx.recyclerview.widget.RecyclerView -import io.reactivex.Observer import kotlinx.android.synthetic.main.onboarding_finish.view.* import org.mozilla.fenix.R -import org.mozilla.fenix.home.sessioncontrol.OnboardingAction -import org.mozilla.fenix.home.sessioncontrol.SessionControlAction -import org.mozilla.fenix.home.sessioncontrol.onNext +import org.mozilla.fenix.home.sessioncontrol.OnboardingInteractor class OnboardingFinishViewHolder( view: View, - private val actionEmitter: Observer + private val interactor: OnboardingInteractor ) : RecyclerView.ViewHolder(view) { init { view.finish_button.setOnClickListener { - actionEmitter.onNext(OnboardingAction.Finish) + interactor.onStartBrowsingClicked() } } diff --git a/architecture/src/main/java/org/mozilla/fenix/test/OpenClass.kt b/app/src/main/java/org/mozilla/fenix/test/OpenClass.kt similarity index 100% rename from architecture/src/main/java/org/mozilla/fenix/test/OpenClass.kt rename to app/src/main/java/org/mozilla/fenix/test/OpenClass.kt diff --git a/app/src/test/java/org/mozilla/fenix/TestUtils.kt b/app/src/test/java/org/mozilla/fenix/TestUtils.kt deleted file mode 100644 index a3d207222..000000000 --- a/app/src/test/java/org/mozilla/fenix/TestUtils.kt +++ /dev/null @@ -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 { - every { lifecycle } returns mockk() - every { lifecycle.addObserver(any()) } just Runs - } - val bus: ActionBusFactory = ActionBusFactory.get(owner) -} diff --git a/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt b/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt index 3338307d4..3e108e306 100644 --- a/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt @@ -29,6 +29,7 @@ import mozilla.components.browser.session.SessionManager import mozilla.components.concept.engine.EngineView import mozilla.components.feature.search.SearchUseCases import mozilla.components.feature.session.SessionUseCases +import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tabs.TabsUseCases import org.junit.After 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.nav import org.mozilla.fenix.ext.toTab -import org.mozilla.fenix.home.sessioncontrol.Tab -import org.mozilla.fenix.home.sessioncontrol.TabCollection +import org.mozilla.fenix.home.Tab import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit @ExperimentalCoroutinesApi diff --git a/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt b/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt new file mode 100644 index 000000000..ea6bf4098 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt @@ -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 = { 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)) } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/home/HomeFragmentStoreTest.kt b/app/src/test/java/org/mozilla/fenix/home/HomeFragmentStoreTest.kt new file mode 100644 index 000000000..aa7dd48b5 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/home/HomeFragmentStoreTest.kt @@ -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 = 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 = 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().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 = listOf(mockk()) + val tabs: List = 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) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/home/ModeTest.kt b/app/src/test/java/org/mozilla/fenix/home/ModeTest.kt index 2c8d86405..1b273b87f 100644 --- a/app/src/test/java/org/mozilla/fenix/home/ModeTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/ModeTest.kt @@ -9,7 +9,6 @@ import io.mockk.every import io.mockk.mockk import io.mockk.spyk import io.mockk.verify -import io.reactivex.Observer import mozilla.components.service.fxa.manager.FxaAccountManager import mozilla.components.service.fxa.sharing.ShareableAccount 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.BrowsingModeManager import org.mozilla.fenix.ext.components -import org.mozilla.fenix.home.sessioncontrol.SessionControlChange import org.mozilla.fenix.onboarding.FenixOnboarding class ModeTest { @@ -27,8 +25,8 @@ class ModeTest { private lateinit var accountManager: FxaAccountManager private lateinit var onboarding: FenixOnboarding private lateinit var browsingModeManager: BrowsingModeManager - private lateinit var emitter: Observer private lateinit var currentMode: CurrentMode + private lateinit var dispatchModeChanges: (mode: Mode) -> Unit @Before fun setup() { @@ -36,7 +34,7 @@ class ModeTest { accountManager = mockk(relaxed = true) onboarding = mockk(relaxed = true) browsingModeManager = mockk(relaxed = true) - emitter = mockk(relaxed = true) + dispatchModeChanges = mockk(relaxed = true) every { context.components.backgroundServices.accountManager } returns accountManager @@ -44,7 +42,7 @@ class ModeTest { context, onboarding, browsingModeManager, - emitter + dispatchModeChanges ) } @@ -101,7 +99,7 @@ class ModeTest { currentMode.emitModeChanges() - verify { emitter.onNext(SessionControlChange.ModeChange(Mode.Normal)) } + verify { dispatchModeChanges(Mode.Normal) } } @Test diff --git a/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt new file mode 100644 index 000000000..f8fc323ef --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt @@ -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) } + } +} diff --git a/architecture/build.gradle b/architecture/build.gradle deleted file mode 100644 index 8b98edd0d..000000000 --- a/architecture/build.gradle +++ /dev/null @@ -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 -} diff --git a/architecture/proguard-rules.pro b/architecture/proguard-rules.pro deleted file mode 100644 index 2c860f1ee..000000000 --- a/architecture/proguard-rules.pro +++ /dev/null @@ -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.** { *; } diff --git a/architecture/src/main/AndroidManifest.xml b/architecture/src/main/AndroidManifest.xml deleted file mode 100644 index 888b439e1..000000000 --- a/architecture/src/main/AndroidManifest.xml +++ /dev/null @@ -1,5 +0,0 @@ - - diff --git a/architecture/src/main/java/org/mozilla/fenix/mvi/ActionBusFactory.kt b/architecture/src/main/java/org/mozilla/fenix/mvi/ActionBusFactory.kt deleted file mode 100644 index eead2262b..000000000 --- a/architecture/src/main/java/org/mozilla/fenix/mvi/ActionBusFactory.kt +++ /dev/null @@ -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() - - /** - * 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, Subject<*>>() - - internal val observer = object : LifecycleObserver { - - @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) - fun onDestroy() { - map.forEach { entry -> entry.value.onComplete() } - buses.remove(owner) - } - } - - private fun create(clazz: Class): Subject { - val subject = PublishSubject.create().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 emit(clazz: Class, event: T) { - val subject = if (map[clazz] != null) map[clazz] else create(clazz) - (subject as Subject).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 getSafeManagedObservable(clazz: Class): Observable { - return if (map[clazz] != null) map[clazz] as Observable else create(clazz) - } - - fun getAutoDisposeObservable(clazz: Class): ObservableSubscribeProxy { - return getSafeManagedObservable(clazz) - .`as`(autoDisposable(AndroidLifecycleScopeProvider.from(owner))) - } - - @Suppress("UNCHECKED_CAST") - fun getManagedEmitter(clazz: Class): Observer { - return if (map[clazz] != null) map[clazz] as Observer 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 { - return owner.createDestroyObservable() - } -} - -/** - * Extension on [LifecycleOwner] used to emit an event. - */ -inline fun 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 LifecycleOwner.getSafeManagedObservable(): Observable = - ActionBusFactory.get(this).getSafeManagedObservable(T::class.java) - -inline fun LifecycleOwner.getAutoDisposeObservable(): ObservableSubscribeProxy = - ActionBusFactory.get(this).getAutoDisposeObservable(T::class.java) - -inline fun LifecycleOwner.getManagedEmitter(): Observer = - 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 { - 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() - } - } - }) - } -} diff --git a/architecture/src/main/java/org/mozilla/fenix/mvi/Concepts.kt b/architecture/src/main/java/org/mozilla/fenix/mvi/Concepts.kt deleted file mode 100644 index 5a6c28a0d..000000000 --- a/architecture/src/main/java/org/mozilla/fenix/mvi/Concepts.kt +++ /dev/null @@ -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 - -/** - * Simple logger for tracking ViewState changes - */ -fun logState(): ObservableTransformer = ObservableTransformer { observable -> - observable.doOnNext { - if (BuildConfig.DEBUG) Logger("State").debug(it.toString()) - } -} - -/** - * For capturing state to a Subject for testing - */ -fun captureState(subject: Subject): - ObservableTransformer = ObservableTransformer { observable -> - observable.doOnNext { - subject.onNext(it) - } -} diff --git a/architecture/src/main/java/org/mozilla/fenix/mvi/UIComponent.kt b/architecture/src/main/java/org/mozilla/fenix/mvi/UIComponent.kt deleted file mode 100644 index 0206f4ff1..000000000 --- a/architecture/src/main/java/org/mozilla/fenix/mvi/UIComponent.kt +++ /dev/null @@ -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 { - val changes: Observer - val state: Observable -} - -interface UIComponentViewModelProvider { - fun fetchViewModel(): UIComponentViewModel -} - -abstract class UIComponent( - protected val actionEmitter: Observer, - protected val changesObservable: Observable, - private val viewModelProvider: UIComponentViewModelProvider -) { - open val uiView: UIView by lazy { initView() } - - abstract fun initView(): UIView - 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( - initialState: S, - reducer: Reducer -) : ViewModel(), UIComponentViewModel { - - final override val changes: Observer - private var _state: BehaviorSubject = BehaviorSubject.createDefault(initialState) - override val state: Observable - get() = _state - - init { - changes = PublishSubject.create() - - changes - .withLatestFrom(_state) - .map { reducer(it.second, it.first) } - .distinctUntilChanged() - .subscribe(_state) - } -} diff --git a/architecture/src/main/java/org/mozilla/fenix/mvi/UIView.kt b/architecture/src/main/java/org/mozilla/fenix/mvi/UIView.kt deleted file mode 100644 index 9819a55b1..000000000 --- a/architecture/src/main/java/org/mozilla/fenix/mvi/UIView.kt +++ /dev/null @@ -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( - private val container: ViewGroup, - protected val actionEmitter: Observer, - protected val changesObservable: Observable -) : 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 -} diff --git a/build.gradle b/build.gradle index c3818f5e1..08f1be0a9 100644 --- a/build.gradle +++ b/build.gradle @@ -53,7 +53,7 @@ task clean(type: Delete) { detekt { // The version number is duplicated, please refer to plugins block for more details version = "1.0.0-RC16" - input = files("$projectDir/app/src", "$projectDir/architecture/src") + input = files("$projectDir/app/src") config = files("$projectDir/config/detekt.yml") filters = ".*test.*,.*/resources/.*,.*/tmp/.*" @@ -80,5 +80,5 @@ task ktlint(type: JavaExec, group: "verification") { description = "Check Kotlin code style." classpath = configurations.ktlint main = "com.pinterest.ktlint.Main" - args "app/src/**/*.kt", "architecture/src/**/*.kt" + args "app/src/**/*.kt" } diff --git a/settings.gradle b/settings.gradle index e8aaca471..81abe7c1d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,4 @@ -include ':app', ':architecture' +include ':app' def log(message) { logger.lifecycle("[settings] ${message}") diff --git a/taskcluster/ci/lint/kind.yml b/taskcluster/ci/lint/kind.yml index 6977e19ab..63d00de8b 100644 --- a/taskcluster/ci/lint/kind.yml +++ b/taskcluster/ci/lint/kind.yml @@ -52,6 +52,6 @@ jobs: description: 'Running lint over all modules' run: using: gradlew - gradlew: ['lintDebug', 'app:lintGeckoNightlyDebug'] + gradlew: ['lintGeckoNightlyDebug'] treeherder: symbol: lint