1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 package org.mozilla.fenix.components.toolbar
7 import android.content.Intent
8 import android.view.ViewGroup
9 import androidx.annotation.VisibleForTesting
10 import androidx.appcompat.app.AlertDialog
11 import androidx.navigation.NavController
12 import com.google.android.material.snackbar.Snackbar
13 import kotlinx.coroutines.CoroutineScope
14 import kotlinx.coroutines.Dispatchers
15 import kotlinx.coroutines.MainScope
16 import kotlinx.coroutines.launch
17 import mozilla.appservices.places.BookmarkRoot
18 import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
19 import mozilla.components.browser.state.selector.findTab
20 import mozilla.components.browser.state.selector.selectedTab
21 import mozilla.components.browser.state.state.SessionState
22 import mozilla.components.browser.state.store.BrowserStore
23 import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
24 import mozilla.components.concept.engine.prompt.ShareData
25 import mozilla.components.feature.session.SessionFeature
26 import mozilla.components.feature.top.sites.DefaultTopSitesStorage
27 import mozilla.components.feature.top.sites.PinnedSiteStorage
28 import mozilla.components.feature.top.sites.TopSite
29 import mozilla.components.service.glean.private.NoExtras
30 import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
31 import org.mozilla.fenix.GleanMetrics.Collections
32 import org.mozilla.fenix.GleanMetrics.Events
33 import org.mozilla.fenix.GleanMetrics.ReaderMode
34 import org.mozilla.fenix.HomeActivity
35 import org.mozilla.fenix.NavGraphDirections
36 import org.mozilla.fenix.R
37 import org.mozilla.fenix.browser.BrowserAnimator
38 import org.mozilla.fenix.browser.BrowserFragmentDirections
39 import org.mozilla.fenix.browser.readermode.ReaderModeController
40 import org.mozilla.fenix.collections.SaveCollectionStep
41 import org.mozilla.fenix.components.FenixSnackbar
42 import org.mozilla.fenix.components.TabCollectionStorage
43 import org.mozilla.fenix.components.accounts.AccountState
44 import org.mozilla.fenix.ext.components
45 import org.mozilla.fenix.ext.getRootView
46 import org.mozilla.fenix.ext.nav
47 import org.mozilla.fenix.ext.navigateSafe
48 import org.mozilla.fenix.ext.openSetDefaultBrowserOption
49 import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
50 import org.mozilla.fenix.utils.Do
51 import org.mozilla.fenix.utils.Settings
54 * An interface that handles events from the BrowserToolbar menu, triggered by the Interactor
56 interface BrowserToolbarMenuController {
57 fun handleToolbarItemInteraction(item: ToolbarMenu.Item)
60 @Suppress("LargeClass", "ForbiddenComment", "LongParameterList")
61 class DefaultBrowserToolbarMenuController(
62 private val store: BrowserStore,
63 private val activity: HomeActivity,
64 private val navController: NavController,
65 private val settings: Settings,
66 private val readerModeController: ReaderModeController,
67 private val sessionFeature: ViewBoundFeatureWrapper<SessionFeature>,
68 private val findInPageLauncher: () -> Unit,
69 private val browserAnimator: BrowserAnimator,
70 private val snackbarParent: ViewGroup,
71 private val customTabSessionId: String?,
72 private val openInFenixIntent: Intent,
73 private val bookmarkTapped: (String, String) -> Unit,
74 private val scope: CoroutineScope,
75 private val tabCollectionStorage: TabCollectionStorage,
76 private val topSitesStorage: DefaultTopSitesStorage,
77 private val pinnedSiteStorage: PinnedSiteStorage,
78 private val browserStore: BrowserStore,
79 ) : BrowserToolbarMenuController {
81 private val currentSession
82 get() = store.state.findCustomTabOrSelectedTab(customTabSessionId)
84 // We hold onto a reference of the inner scope so that we can override this with the
85 // TestCoroutineScope to ensure sequential execution. If we didn't have this, our tests
86 // would fail intermittently due to the async nature of coroutine scheduling.
88 internal var ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
90 @Suppress("ComplexMethod", "LongMethod")
91 override fun handleToolbarItemInteraction(item: ToolbarMenu.Item) {
92 val sessionUseCases = activity.components.useCases.sessionUseCases
93 val customTabUseCases = activity.components.useCases.customTabsUseCases
94 trackToolbarItemInteraction(item)
96 Do exhaustive when (item) {
97 // TODO: These can be removed for https://github.com/mozilla-mobile/fenix/issues/17870
99 is ToolbarMenu.Item.InstallPwaToHomeScreen -> {
100 settings.installPwaOpened = true
102 with(activity.components.useCases.webAppUseCases) {
103 if (isInstallable()) {
107 BrowserFragmentDirections.actionBrowserFragmentToCreateShortcutFragment()
108 navController.navigateSafe(R.id.browserFragment, directions)
113 is ToolbarMenu.Item.OpenInFenix -> {
114 customTabSessionId?.let {
115 // Stop the SessionFeature from updating the EngineView and let it release the session
116 // from the EngineView so that it can immediately be rendered by a different view once
117 // we switch to the actual browser.
118 sessionFeature.get()?.release()
120 // Turn this Session into a regular tab and then select it
121 customTabUseCases.migrate(customTabSessionId, select = true)
123 // Switch to the actual browser which should now display our new selected session
124 activity.startActivity(
125 openInFenixIntent.apply {
126 // We never want to launch the browser in the same task as the external app
127 // activity. So we force a new task here. IntentReceiverActivity will do the
128 // right thing and take care of routing to an already existing browser and avoid
129 // cloning a new one.
130 flags = flags or Intent.FLAG_ACTIVITY_NEW_TASK
134 // Close this activity (and the task) since it is no longer displaying any session
135 activity.finishAndRemoveTask()
139 is ToolbarMenu.Item.OpenInApp -> {
140 settings.openInAppOpened = true
142 val appLinksUseCases = activity.components.useCases.appLinksUseCases
143 val getRedirect = appLinksUseCases.appLinkRedirect
144 currentSession?.let {
145 val redirect = getRedirect.invoke(it.content.url)
146 redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK
147 appLinksUseCases.openAppLink.invoke(redirect.appIntent)
150 is ToolbarMenu.Item.Quit -> {
151 // We need to show the snackbar while the browsing data is deleting (if "Delete
152 // browsing data on quit" is activated). After the deletion is over, the snackbar
154 val snackbar: FenixSnackbar? = activity.getRootView()?.let { v ->
157 duration = Snackbar.LENGTH_LONG,
158 isDisplayedWithBrowserToolbar = true,
160 .setText(v.context.getString(R.string.deleting_browsing_data_in_progress))
163 deleteAndQuit(activity, scope, snackbar)
165 is ToolbarMenu.Item.CustomizeReaderView -> {
166 readerModeController.showControls()
167 ReaderMode.appearance.record(NoExtras())
169 is ToolbarMenu.Item.Back -> {
170 if (item.viewHistory) {
171 navController.navigate(
172 BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment(
173 activeSessionId = customTabSessionId,
177 currentSession?.let {
178 sessionUseCases.goBack.invoke(it.id)
182 is ToolbarMenu.Item.Forward -> {
183 if (item.viewHistory) {
184 navController.navigate(
185 BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment(
186 activeSessionId = customTabSessionId,
190 currentSession?.let {
191 sessionUseCases.goForward.invoke(it.id)
195 is ToolbarMenu.Item.Reload -> {
196 val flags = if (item.bypassCache) {
197 LoadUrlFlags.select(LoadUrlFlags.BYPASS_CACHE)
202 currentSession?.let {
203 sessionUseCases.reload.invoke(it.id, flags = flags)
206 is ToolbarMenu.Item.Stop -> {
207 currentSession?.let {
208 sessionUseCases.stopLoading.invoke(it.id)
211 is ToolbarMenu.Item.Share -> {
212 val directions = NavGraphDirections.actionGlobalShareFragment(
213 sessionId = currentSession?.id,
216 url = getProperUrl(currentSession),
217 title = currentSession?.content?.title,
222 navController.navigate(directions)
224 is ToolbarMenu.Item.Settings -> browserAnimator.captureEngineViewAndDrawStatically {
225 val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment()
226 navController.nav(R.id.browserFragment, directions)
228 is ToolbarMenu.Item.SyncAccount -> {
229 val directions = when (item.accountState) {
230 AccountState.AUTHENTICATED ->
231 BrowserFragmentDirections.actionGlobalAccountSettingsFragment()
232 AccountState.NEEDS_REAUTHENTICATION ->
233 BrowserFragmentDirections.actionGlobalAccountProblemFragment()
234 AccountState.NO_ACCOUNT ->
235 BrowserFragmentDirections.actionGlobalTurnOnSync()
237 browserAnimator.captureEngineViewAndDrawStatically {
239 R.id.browserFragment,
244 is ToolbarMenu.Item.RequestDesktop -> {
245 currentSession?.let {
246 sessionUseCases.requestDesktopSite.invoke(
252 is ToolbarMenu.Item.AddToTopSites -> {
254 val context = snackbarParent.context
255 val numPinnedSites = topSitesStorage.cachedTopSites
256 .filter { it is TopSite.Default || it is TopSite.Pinned }.size
258 if (numPinnedSites >= settings.topSitesMaxLimit) {
259 AlertDialog.Builder(snackbarParent.context).apply {
260 setTitle(R.string.shortcut_max_limit_title)
261 setMessage(R.string.shortcut_max_limit_content)
262 setPositiveButton(R.string.top_sites_max_limit_confirmation_button) { dialog, _ ->
269 currentSession?.let {
270 with(activity.components.useCases.topSitesUseCase) {
271 addPinnedSites(it.content.title, it.content.url)
277 view = snackbarParent,
278 duration = Snackbar.LENGTH_SHORT,
279 isDisplayedWithBrowserToolbar = true,
282 context.getString(R.string.snackbar_added_to_shortcuts),
288 is ToolbarMenu.Item.AddToHomeScreen -> {
289 settings.installPwaOpened = true
291 with(activity.components.useCases.webAppUseCases) {
292 if (isInstallable()) {
296 BrowserFragmentDirections.actionBrowserFragmentToCreateShortcutFragment()
297 navController.navigateSafe(R.id.browserFragment, directions)
302 is ToolbarMenu.Item.FindInPage -> {
305 is ToolbarMenu.Item.AddonsManager -> browserAnimator.captureEngineViewAndDrawStatically {
307 R.id.browserFragment,
308 BrowserFragmentDirections.actionGlobalAddonsManagementFragment(),
311 is ToolbarMenu.Item.SaveToCollection -> {
312 Collections.saveButton.record(
313 Collections.SaveButtonExtra(
314 TELEMETRY_BROWSER_IDENTIFIER,
318 currentSession?.let { currentSession ->
320 BrowserFragmentDirections.actionGlobalCollectionCreationFragment(
321 tabIds = arrayOf(currentSession.id),
322 selectedTabIds = arrayOf(currentSession.id),
323 saveCollectionStep = if (tabCollectionStorage.cachedTabCollections.isEmpty()) {
324 SaveCollectionStep.NameCollection
326 SaveCollectionStep.SelectCollection
329 navController.nav(R.id.browserFragment, directions)
332 is ToolbarMenu.Item.Bookmark -> {
333 store.state.selectedTab?.let {
334 getProperUrl(it)?.let { url -> bookmarkTapped(url, it.content.title) }
337 is ToolbarMenu.Item.Bookmarks -> browserAnimator.captureEngineViewAndDrawStatically {
339 R.id.browserFragment,
340 BrowserFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id),
343 is ToolbarMenu.Item.History -> browserAnimator.captureEngineViewAndDrawStatically {
345 R.id.browserFragment,
346 BrowserFragmentDirections.actionGlobalHistoryFragment(),
350 is ToolbarMenu.Item.Downloads -> browserAnimator.captureEngineViewAndDrawStatically {
352 R.id.browserFragment,
353 BrowserFragmentDirections.actionGlobalDownloadsFragment(),
356 is ToolbarMenu.Item.NewTab -> {
357 navController.navigate(
358 BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true),
361 is ToolbarMenu.Item.SetDefaultBrowser -> {
362 activity.openSetDefaultBrowserOption()
364 is ToolbarMenu.Item.RemoveFromTopSites -> {
366 val removedTopSite: TopSite? =
369 .find { it.url == currentSession?.content?.url }
370 if (removedTopSite != null) {
372 currentSession?.let {
373 with(activity.components.useCases.topSitesUseCase) {
374 removeTopSites(removedTopSite)
381 view = snackbarParent,
382 duration = Snackbar.LENGTH_SHORT,
383 isDisplayedWithBrowserToolbar = true,
386 snackbarParent.context.getString(R.string.snackbar_top_site_removed),
394 private fun getProperUrl(currentSession: SessionState?): String? {
395 return currentSession?.id?.let {
396 val currentTab = browserStore.state.findTab(it)
397 if (currentTab?.readerState?.active == true) {
398 currentTab.readerState.activeUrl
400 currentSession.content.url
405 @Suppress("ComplexMethod")
406 private fun trackToolbarItemInteraction(item: ToolbarMenu.Item) {
408 is ToolbarMenu.Item.OpenInFenix ->
409 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("open_in_fenix"))
410 is ToolbarMenu.Item.InstallPwaToHomeScreen ->
411 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("add_to_homescreen"))
412 is ToolbarMenu.Item.Quit ->
413 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("quit"))
414 is ToolbarMenu.Item.OpenInApp ->
415 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("open_in_app"))
416 is ToolbarMenu.Item.CustomizeReaderView ->
417 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("reader_mode_appearance"))
418 is ToolbarMenu.Item.Back ->
419 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("back"))
420 is ToolbarMenu.Item.Forward ->
421 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("forward"))
422 is ToolbarMenu.Item.Reload ->
423 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("reload"))
424 is ToolbarMenu.Item.Stop ->
425 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("stop"))
426 is ToolbarMenu.Item.Share ->
427 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("share"))
428 is ToolbarMenu.Item.Settings ->
429 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("settings"))
430 is ToolbarMenu.Item.RequestDesktop ->
431 if (item.isChecked) {
432 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("desktop_view_on"))
434 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("desktop_view_off"))
436 is ToolbarMenu.Item.FindInPage ->
437 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("find_in_page"))
438 is ToolbarMenu.Item.SaveToCollection ->
439 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("save_to_collection"))
440 is ToolbarMenu.Item.AddToTopSites ->
441 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("add_to_top_sites"))
442 is ToolbarMenu.Item.AddToHomeScreen ->
443 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("add_to_homescreen"))
444 is ToolbarMenu.Item.SyncAccount ->
445 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("sync_account"))
446 is ToolbarMenu.Item.Bookmark ->
447 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("bookmark"))
448 is ToolbarMenu.Item.AddonsManager ->
449 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("addons_manager"))
450 is ToolbarMenu.Item.Bookmarks ->
451 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("bookmarks"))
452 is ToolbarMenu.Item.History ->
453 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("history"))
454 is ToolbarMenu.Item.Downloads ->
455 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("downloads"))
456 is ToolbarMenu.Item.NewTab ->
457 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("new_tab"))
458 is ToolbarMenu.Item.SetDefaultBrowser ->
459 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("set_default_browser"))
460 is ToolbarMenu.Item.RemoveFromTopSites ->
461 Events.browserMenuAction.record(Events.BrowserMenuActionExtra("remove_from_top_sites"))
466 internal const val TELEMETRY_BROWSER_IDENTIFIER = "browserMenu"