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