/* 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.components.toolbar import android.content.Intent import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AlertDialog import androidx.navigation.NavController import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import mozilla.appservices.places.BookmarkRoot import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.selectedTab import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.EngineSession.LoadUrlFlags import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.feature.session.SessionFeature import mozilla.components.feature.top.sites.DefaultTopSitesStorage import mozilla.components.feature.top.sites.TopSite import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.R import org.mozilla.fenix.browser.BrowserAnimator import org.mozilla.fenix.browser.BrowserFragmentDirections import org.mozilla.fenix.browser.readermode.ReaderModeController import org.mozilla.fenix.collections.SaveCollectionStep import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.TabCollectionStorage 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.getRootView import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.navigateSafe import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit import org.mozilla.fenix.utils.Do import org.mozilla.fenix.utils.Settings /** * An interface that handles events from the BrowserToolbar menu, triggered by the Interactor */ interface BrowserToolbarMenuController { fun handleToolbarItemInteraction(item: ToolbarMenu.Item) } @Suppress("LargeClass", "ForbiddenComment") class DefaultBrowserToolbarMenuController( private val store: BrowserStore, private val activity: HomeActivity, private val navController: NavController, private val metrics: MetricController, private val settings: Settings, private val readerModeController: ReaderModeController, private val sessionFeature: ViewBoundFeatureWrapper, private val findInPageLauncher: () -> Unit, private val browserAnimator: BrowserAnimator, private val swipeRefresh: SwipeRefreshLayout, private val customTabSessionId: String?, private val openInFenixIntent: Intent, private val bookmarkTapped: (String, String) -> Unit, private val scope: CoroutineScope, private val tabCollectionStorage: TabCollectionStorage, private val topSitesStorage: DefaultTopSitesStorage, private val browserStore: BrowserStore ) : BrowserToolbarMenuController { private val currentSession get() = store.state.findCustomTabOrSelectedTab(customTabSessionId) // We hold onto a reference of the inner scope so that we can override this with the // TestCoroutineScope to ensure sequential execution. If we didn't have this, our tests // would fail intermittently due to the async nature of coroutine scheduling. @VisibleForTesting internal var ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO) @Suppress("ComplexMethod", "LongMethod") override fun handleToolbarItemInteraction(item: ToolbarMenu.Item) { val sessionUseCases = activity.components.useCases.sessionUseCases val customTabUseCases = activity.components.useCases.customTabsUseCases trackToolbarItemInteraction(item) Do exhaustive when (item) { // TODO: These can be removed for https://github.com/mozilla-mobile/fenix/issues/17870 // todo === Start === is ToolbarMenu.Item.InstallToHomeScreen -> { settings.installPwaOpened = true MainScope().launch { with(activity.components.useCases.webAppUseCases) { if (isInstallable()) { addToHomescreen() } else { val directions = BrowserFragmentDirections.actionBrowserFragmentToCreateShortcutFragment() navController.navigateSafe(R.id.browserFragment, directions) } } } } is ToolbarMenu.Item.OpenInFenix -> { customTabSessionId?.let { // Stop the SessionFeature from updating the EngineView and let it release the session // from the EngineView so that it can immediately be rendered by a different view once // we switch to the actual browser. sessionFeature.get()?.release() // Turn this Session into a regular tab and then select it customTabUseCases.migrate(customTabSessionId, select = true) // Switch to the actual browser which should now display our new selected session activity.startActivity(openInFenixIntent.apply { // We never want to launch the browser in the same task as the external app // activity. So we force a new task here. IntentReceiverActivity will do the // right thing and take care of routing to an already existing browser and avoid // cloning a new one. flags = flags or Intent.FLAG_ACTIVITY_NEW_TASK }) // Close this activity (and the task) since it is no longer displaying any session activity.finishAndRemoveTask() } } is ToolbarMenu.Item.Quit -> { // We need to show the snackbar while the browsing data is deleting (if "Delete // browsing data on quit" is activated). After the deletion is over, the snackbar // is dismissed. val snackbar: FenixSnackbar? = activity.getRootView()?.let { v -> FenixSnackbar.make( view = v, duration = Snackbar.LENGTH_LONG, isDisplayedWithBrowserToolbar = true ) .setText(v.context.getString(R.string.deleting_browsing_data_in_progress)) } deleteAndQuit(activity, scope, snackbar) } is ToolbarMenu.Item.ReaderModeAppearance -> { readerModeController.showControls() metrics.track(Event.ReaderModeAppearanceOpened) } is ToolbarMenu.Item.OpenInApp -> { settings.openInAppOpened = true val appLinksUseCases = activity.components.useCases.appLinksUseCases val getRedirect = appLinksUseCases.appLinkRedirect currentSession?.let { val redirect = getRedirect.invoke(it.content.url) redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK appLinksUseCases.openAppLink.invoke(redirect.appIntent) } } // todo === End === is ToolbarMenu.Item.Back -> { if (item.viewHistory) { navController.navigate( BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment( activeSessionId = customTabSessionId ) ) } else { currentSession?.let { sessionUseCases.goBack.invoke(it.id) } } } is ToolbarMenu.Item.Forward -> { if (item.viewHistory) { navController.navigate( BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment( activeSessionId = customTabSessionId ) ) } else { currentSession?.let { sessionUseCases.goForward.invoke(it.id) } } } is ToolbarMenu.Item.Reload -> { val flags = if (item.bypassCache) { LoadUrlFlags.select(LoadUrlFlags.BYPASS_CACHE) } else { LoadUrlFlags.none() } currentSession?.let { sessionUseCases.reload.invoke(it.id, flags = flags) } } is ToolbarMenu.Item.Stop -> { currentSession?.let { sessionUseCases.stopLoading.invoke(it.id) } } is ToolbarMenu.Item.Share -> { val directions = NavGraphDirections.actionGlobalShareFragment( data = arrayOf( ShareData( url = getProperUrl(currentSession), title = currentSession?.content?.title ) ), showPage = true ) navController.navigate(directions) } is ToolbarMenu.Item.Settings -> browserAnimator.captureEngineViewAndDrawStatically { val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment() navController.nav(R.id.browserFragment, directions) } is ToolbarMenu.Item.SyncedTabs -> browserAnimator.captureEngineViewAndDrawStatically { navController.nav( R.id.browserFragment, BrowserFragmentDirections.actionBrowserFragmentToSyncedTabsFragment() ) } is ToolbarMenu.Item.RequestDesktop -> { currentSession?.let { sessionUseCases.requestDesktopSite.invoke( item.isChecked, it.id ) } } is ToolbarMenu.Item.AddToTopSites -> { scope.launch { val context = swipeRefresh.context val numPinnedSites = topSitesStorage.cachedTopSites.filter { it.type != TopSite.Type.FRECENT }.size if (numPinnedSites >= settings.topSitesMaxLimit) { AlertDialog.Builder(swipeRefresh.context).apply { setTitle(R.string.top_sites_max_limit_title) setMessage(R.string.top_sites_max_limit_content_2) setPositiveButton(R.string.top_sites_max_limit_confirmation_button) { dialog, _ -> dialog.dismiss() } create() }.show() } else { ioScope.launch { currentSession?.let { with(activity.components.useCases.topSitesUseCase) { addPinnedSites(it.content.title, it.content.url) } } }.join() FenixSnackbar.make( view = swipeRefresh, duration = Snackbar.LENGTH_SHORT, isDisplayedWithBrowserToolbar = true ) .setText( context.getString(R.string.snackbar_added_to_top_sites) ) .show() } } } is ToolbarMenu.Item.AddToHomeScreen -> { settings.installPwaOpened = true MainScope().launch { with(activity.components.useCases.webAppUseCases) { if (isInstallable()) { addToHomescreen() } else { val directions = BrowserFragmentDirections.actionBrowserFragmentToCreateShortcutFragment() navController.navigateSafe(R.id.browserFragment, directions) } } } } is ToolbarMenu.Item.FindInPage -> { findInPageLauncher() metrics.track(Event.FindInPageOpened) } is ToolbarMenu.Item.AddonsManager -> browserAnimator.captureEngineViewAndDrawStatically { navController.nav( R.id.browserFragment, BrowserFragmentDirections.actionGlobalAddonsManagementFragment() ) } is ToolbarMenu.Item.SaveToCollection -> { metrics .track(Event.CollectionSaveButtonPressed(TELEMETRY_BROWSER_IDENTIFIER)) currentSession?.let { currentSession -> val directions = BrowserFragmentDirections.actionGlobalCollectionCreationFragment( tabIds = arrayOf(currentSession.id), selectedTabIds = arrayOf(currentSession.id), saveCollectionStep = if (tabCollectionStorage.cachedTabCollections.isEmpty()) { SaveCollectionStep.NameCollection } else { SaveCollectionStep.SelectCollection } ) navController.nav(R.id.browserFragment, directions) } } is ToolbarMenu.Item.Bookmark -> { store.state.selectedTab?.let { getProperUrl(it)?.let { url -> bookmarkTapped(url, it.content.title) } } } is ToolbarMenu.Item.Bookmarks -> browserAnimator.captureEngineViewAndDrawStatically { navController.nav( R.id.browserFragment, BrowserFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id) ) } is ToolbarMenu.Item.History -> browserAnimator.captureEngineViewAndDrawStatically { navController.nav( R.id.browserFragment, BrowserFragmentDirections.actionGlobalHistoryFragment() ) } is ToolbarMenu.Item.Downloads -> browserAnimator.captureEngineViewAndDrawStatically { navController.nav( R.id.browserFragment, BrowserFragmentDirections.actionGlobalDownloadsFragment() ) } is ToolbarMenu.Item.NewTab -> { navController.navigate( BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true) ) } } } private fun getProperUrl(currentSession: SessionState?): String? { return currentSession?.id?.let { val currentTab = browserStore.state.findTab(it) if (currentTab?.readerState?.active == true) { currentTab.readerState.activeUrl } else { currentSession.content.url } } } @Suppress("ComplexMethod") private fun trackToolbarItemInteraction(item: ToolbarMenu.Item) { val eventItem = when (item) { // TODO: These can be removed for https://github.com/mozilla-mobile/fenix/issues/17870 // todo === Start === is ToolbarMenu.Item.OpenInFenix -> Event.BrowserMenuItemTapped.Item.OPEN_IN_FENIX is ToolbarMenu.Item.InstallToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN is ToolbarMenu.Item.Quit -> Event.BrowserMenuItemTapped.Item.QUIT is ToolbarMenu.Item.ReaderModeAppearance -> Event.BrowserMenuItemTapped.Item.READER_MODE_APPEARANCE is ToolbarMenu.Item.OpenInApp -> Event.BrowserMenuItemTapped.Item.OPEN_IN_APP // todo === End === is ToolbarMenu.Item.Back -> Event.BrowserMenuItemTapped.Item.BACK is ToolbarMenu.Item.Forward -> Event.BrowserMenuItemTapped.Item.FORWARD is ToolbarMenu.Item.Reload -> Event.BrowserMenuItemTapped.Item.RELOAD is ToolbarMenu.Item.Stop -> Event.BrowserMenuItemTapped.Item.STOP is ToolbarMenu.Item.Share -> Event.BrowserMenuItemTapped.Item.SHARE is ToolbarMenu.Item.Settings -> Event.BrowserMenuItemTapped.Item.SETTINGS is ToolbarMenu.Item.RequestDesktop -> if (item.isChecked) { Event.BrowserMenuItemTapped.Item.DESKTOP_VIEW_ON } else { Event.BrowserMenuItemTapped.Item.DESKTOP_VIEW_OFF } is ToolbarMenu.Item.FindInPage -> Event.BrowserMenuItemTapped.Item.FIND_IN_PAGE is ToolbarMenu.Item.SaveToCollection -> Event.BrowserMenuItemTapped.Item.SAVE_TO_COLLECTION is ToolbarMenu.Item.AddToTopSites -> Event.BrowserMenuItemTapped.Item.ADD_TO_TOP_SITES is ToolbarMenu.Item.AddToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN is ToolbarMenu.Item.SyncedTabs -> Event.BrowserMenuItemTapped.Item.SYNC_TABS is ToolbarMenu.Item.Bookmark -> Event.BrowserMenuItemTapped.Item.BOOKMARK is ToolbarMenu.Item.AddonsManager -> Event.BrowserMenuItemTapped.Item.ADDONS_MANAGER is ToolbarMenu.Item.Bookmarks -> Event.BrowserMenuItemTapped.Item.BOOKMARKS is ToolbarMenu.Item.History -> Event.BrowserMenuItemTapped.Item.HISTORY is ToolbarMenu.Item.Downloads -> Event.BrowserMenuItemTapped.Item.DOWNLOADS is ToolbarMenu.Item.NewTab -> Event.BrowserMenuItemTapped.Item.NEW_TAB } metrics.track(Event.BrowserMenuItemTapped(eventItem)) } companion object { internal const val TELEMETRY_BROWSER_IDENTIFIER = "browserMenu" } }