/* 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 androidx.navigation.NavController import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import mozilla.components.browser.session.SessionManager import mozilla.components.concept.engine.Engine import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.ext.restore import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.feature.top.sites.TopSite import mozilla.components.support.ktx.kotlin.isUrl import org.mozilla.fenix.BrowserDirection 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.TabCollectionStorage import org.mozilla.fenix.components.TopSiteStorage import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricsUtils import org.mozilla.fenix.components.tips.Tip import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.sessionsOfType import org.mozilla.fenix.ext.settings 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.settings.SupportUtils import org.mozilla.fenix.utils.Settings import mozilla.components.feature.tab.collections.Tab as ComponentTab /** * [HomeFragment] controller. An interface that handles the view manipulation of the Tabs triggered * by the Interactor. */ @Suppress("TooManyFunctions") interface SessionControlController { /** * @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, wasSwiped: Boolean) /** * @see [CollectionInteractor.onCollectionShareTabsClicked] */ fun handleCollectionShareTabsClicked(collection: TabCollection) /** * @see [CollectionInteractor.onDeleteCollectionTapped] */ fun handleDeleteCollectionTapped(collection: TabCollection) /** * @see [TopSiteInteractor.onOpenInPrivateTabClicked] */ fun handleOpenInPrivateTabClicked(topSite: TopSite) /** * @see [TabSessionInteractor.onPrivateBrowsingLearnMoreClicked] */ fun handlePrivateBrowsingLearnMoreClicked() /** * @see [TopSiteInteractor.onRemoveTopSiteClicked] */ fun handleRemoveTopSiteClicked(topSite: TopSite) /** * @see [CollectionInteractor.onRenameCollectionTapped] */ fun handleRenameCollectionTapped(collection: TabCollection) /** * @see [TopSiteInteractor.onSelectTopSite] */ fun handleSelectTopSite(url: String, isDefault: Boolean) /** * @see [OnboardingInteractor.onStartBrowsingClicked] */ fun handleStartBrowsingClicked() /** * @see [OnboardingInteractor.onOpenSettingsClicked] */ fun handleOpenSettingsClicked() /** * @see [OnboardingInteractor.onWhatsNewGetAnswersClicked] */ fun handleWhatsNewGetAnswersClicked() /** * @see [OnboardingInteractor.onReadPrivacyNoticeClicked] */ fun handleReadPrivacyNoticeClicked() /** * @see [CollectionInteractor.onToggleCollectionExpanded] */ fun handleToggleCollectionExpanded(collection: TabCollection, expand: Boolean) /** * @see [TipInteractor.onCloseTip] */ fun handleCloseTip(tip: Tip) /** * @see [ToolbarInteractor.onPasteAndGo] */ fun handlePasteAndGo(clipboardText: String) /** * @see [ToolbarInteractor.onPaste] */ fun handlePaste(clipboardText: String) /** * @see [CollectionInteractor.onAddTabsToCollectionTapped] */ fun handleCreateCollection() /** * @see [CollectionInteractor.onRemoveCollectionsPlaceholder] */ fun handleRemoveCollectionsPlaceholder() } @Suppress("TooManyFunctions", "LargeClass") class DefaultSessionControlController( private val activity: HomeActivity, private val settings: Settings, private val engine: Engine, private val metrics: MetricController, private val sessionManager: SessionManager, private val tabCollectionStorage: TabCollectionStorage, private val topSiteStorage: TopSiteStorage, private val addTabUseCase: TabsUseCases.AddNewTabUseCase, private val fragmentStore: HomeFragmentStore, private val navController: NavController, private val viewLifecycleScope: CoroutineScope, private val hideOnboarding: () -> Unit, private val registerCollectionStorageObserver: () -> Unit, private val showDeleteCollectionPrompt: ( tabCollection: TabCollection, title: String?, message: String, wasSwiped: Boolean, handleSwipedItemDeletionCancel: () -> Unit ) -> Unit, private val showTabTray: () -> Unit, private val handleSwipedItemDeletionCancel: () -> Unit ) : SessionControlController { override fun handleCollectionAddTabTapped(collection: TabCollection) { metrics.track(Event.CollectionAddTabPressed) showCollectionCreationFragment( step = SaveCollectionStep.SelectTabs, selectedTabCollectionId = collection.id ) } override fun handleCollectionOpenTabClicked(tab: ComponentTab) { sessionManager.restore( activity, engine, tab, onTabRestored = { activity.openToBrowser(BrowserDirection.FromHome) }, onFailure = { activity.openToBrowserAndLoad( searchTermOrURL = tab.url, newTab = true, from = BrowserDirection.FromHome ) } ) metrics.track(Event.CollectionTabRestored) } override fun handleCollectionOpenTabsTapped(collection: TabCollection) { sessionManager.restore( activity, engine, collection, onFailure = { url -> addTabUseCase.invoke(url) } ) showTabTray() metrics.track(Event.CollectionAllTabsRestored) } override fun handleCollectionRemoveTab( collection: TabCollection, tab: ComponentTab, wasSwiped: Boolean ) { metrics.track(Event.CollectionTabRemoved) if (collection.tabs.size == 1) { val title = activity.resources.getString( R.string.delete_tab_and_collection_dialog_title, collection.title ) val message = activity.resources.getString(R.string.delete_tab_and_collection_dialog_message) showDeleteCollectionPrompt( collection, title, message, wasSwiped, handleSwipedItemDeletionCancel ) } else { viewLifecycleScope.launch(Dispatchers.IO) { tabCollectionStorage.removeTabFromCollection(collection, tab) } } } override fun handleCollectionShareTabsClicked(collection: TabCollection) { showShareFragment( collection.title, collection.tabs.map { ShareData(url = it.url, title = it.title) } ) metrics.track(Event.CollectionShared) } override fun handleDeleteCollectionTapped(collection: TabCollection) { val message = activity.resources.getString(R.string.tab_collection_dialog_message, collection.title) showDeleteCollectionPrompt(collection, null, message, false, handleSwipedItemDeletionCancel) } override fun handleOpenInPrivateTabClicked(topSite: TopSite) { metrics.track(Event.TopSiteOpenInPrivateTab) with(activity) { browsingModeManager.mode = BrowsingMode.Private openToBrowserAndLoad( searchTermOrURL = topSite.url, newTab = true, from = BrowserDirection.FromHome ) } } override fun handlePrivateBrowsingLearnMoreClicked() { activity.openToBrowserAndLoad( searchTermOrURL = SupportUtils.getGenericSumoURLForTopic (SupportUtils.SumoTopic.PRIVATE_BROWSING_MYTHS), newTab = true, from = BrowserDirection.FromHome ) } override fun handleRemoveTopSiteClicked(topSite: TopSite) { metrics.track(Event.TopSiteRemoved) if (topSite.url == SupportUtils.POCKET_TRENDING_URL) { metrics.track(Event.PocketTopSiteRemoved) } viewLifecycleScope.launch(Dispatchers.IO) { topSiteStorage.removeTopSite(topSite) } } override fun handleRenameCollectionTapped(collection: TabCollection) { showCollectionCreationFragment( step = SaveCollectionStep.RenameCollection, selectedTabCollectionId = collection.id ) metrics.track(Event.CollectionRenamePressed) } override fun handleSelectTopSite(url: String, isDefault: Boolean) { metrics.track(Event.TopSiteOpenInNewTab) if (isDefault) { metrics.track(Event.TopSiteOpenDefault) } if (url == SupportUtils.POCKET_TRENDING_URL) { metrics.track(Event.PocketTopSiteClicked) } addTabUseCase.invoke( url = url, selectTab = true, startLoading = true ) activity.openToBrowser(BrowserDirection.FromHome) } override fun handleStartBrowsingClicked() { hideOnboarding() } override fun handleOpenSettingsClicked() { val directions = HomeFragmentDirections.actionGlobalPrivateBrowsingFragment() navController.nav(R.id.homeFragment, directions) } override fun handleWhatsNewGetAnswersClicked() { activity.openToBrowserAndLoad( searchTermOrURL = SupportUtils.getWhatsNewUrl(activity), newTab = true, from = BrowserDirection.FromHome ) } override fun handleReadPrivacyNoticeClicked() { activity.openToBrowserAndLoad( searchTermOrURL = SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.PRIVATE_NOTICE), newTab = true, from = BrowserDirection.FromHome ) } override fun handleToggleCollectionExpanded(collection: TabCollection, expand: Boolean) { fragmentStore.dispatch(HomeFragmentAction.CollectionExpanded(collection, expand)) } override fun handleCloseTip(tip: Tip) { fragmentStore.dispatch(HomeFragmentAction.RemoveTip(tip)) } private fun showTabTrayCollectionCreation() { val directions = HomeFragmentDirections.actionGlobalTabTrayDialogFragment( enterMultiselect = true ) navController.nav(R.id.homeFragment, directions) } 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 = sessionManager .sessionsOfType(private = activity.browsingModeManager.mode.isPrivate) .map { session -> session.id } .toList() .toTypedArray() val directions = HomeFragmentDirections.actionGlobalCollectionCreationFragment( tabIds = tabIds, saveCollectionStep = step, selectedTabIds = selectedTabIds, selectedTabCollectionId = selectedTabCollectionId ?: -1 ) navController.nav(R.id.homeFragment, directions) } override fun handleCreateCollection() { showTabTrayCollectionCreation() } override fun handleRemoveCollectionsPlaceholder() { settings.showCollectionsPlaceholderOnHome = false fragmentStore.dispatch(HomeFragmentAction.RemoveCollectionsPlaceholder) } private fun showShareFragment(shareSubject: String, data: List) { val directions = HomeFragmentDirections.actionGlobalShareFragment( shareSubject = shareSubject, data = data.toTypedArray() ) navController.nav(R.id.homeFragment, directions) } override fun handlePasteAndGo(clipboardText: String) { activity.openToBrowserAndLoad( searchTermOrURL = clipboardText, newTab = true, from = BrowserDirection.FromHome, engine = activity.components.search.provider.getDefaultEngine(activity) ) val event = if (clipboardText.isUrl()) { Event.EnteredUrl(false) } else { val searchAccessPoint = Event.PerformedSearch.SearchAccessPoint.ACTION activity.settings().incrementActiveSearchCount() searchAccessPoint.let { sap -> MetricsUtils.createSearchEvent( activity.components.search.provider.getDefaultEngine(activity), activity, sap ) } } event?.let { activity.metrics.track(it) } } override fun handlePaste(clipboardText: String) { val directions = HomeFragmentDirections.actionGlobalSearch( sessionId = null, pastedText = clipboardText ) navController.nav(R.id.homeFragment, directions) } }