Bug 1820169 - Ensure shortcuts snackbars are shown
[gecko.git] / mobile / android / fenix / app / src / main / java / org / mozilla / fenix / components / toolbar / BrowserToolbarMenuController.kt
blob38dfc48a0c5b6f788a60e1b74a008794aaced5e0
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
53 /**
54  * An interface that handles events from the BrowserToolbar menu, triggered by the Interactor
55  */
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.
87     @VisibleForTesting
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
98             // todo === Start ===
99             is ToolbarMenu.Item.InstallPwaToHomeScreen -> {
100                 settings.installPwaOpened = true
101                 MainScope().launch {
102                     with(activity.components.useCases.webAppUseCases) {
103                         if (isInstallable()) {
104                             addToHomescreen()
105                         } else {
106                             val directions =
107                                 BrowserFragmentDirections.actionBrowserFragmentToCreateShortcutFragment()
108                             navController.navigateSafe(R.id.browserFragment, directions)
109                         }
110                     }
111                 }
112             }
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
131                         },
132                     )
134                     // Close this activity (and the task) since it is no longer displaying any session
135                     activity.finishAndRemoveTask()
136                 }
137             }
138             // todo === End ===
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)
148                 }
149             }
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
153                 // is dismissed.
154                 val snackbar: FenixSnackbar? = activity.getRootView()?.let { v ->
155                     FenixSnackbar.make(
156                         view = v,
157                         duration = Snackbar.LENGTH_LONG,
158                         isDisplayedWithBrowserToolbar = true,
159                     )
160                         .setText(v.context.getString(R.string.deleting_browsing_data_in_progress))
161                 }
163                 deleteAndQuit(activity, scope, snackbar)
164             }
165             is ToolbarMenu.Item.CustomizeReaderView -> {
166                 readerModeController.showControls()
167                 ReaderMode.appearance.record(NoExtras())
168             }
169             is ToolbarMenu.Item.Back -> {
170                 if (item.viewHistory) {
171                     navController.navigate(
172                         BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment(
173                             activeSessionId = customTabSessionId,
174                         ),
175                     )
176                 } else {
177                     currentSession?.let {
178                         sessionUseCases.goBack.invoke(it.id)
179                     }
180                 }
181             }
182             is ToolbarMenu.Item.Forward -> {
183                 if (item.viewHistory) {
184                     navController.navigate(
185                         BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment(
186                             activeSessionId = customTabSessionId,
187                         ),
188                     )
189                 } else {
190                     currentSession?.let {
191                         sessionUseCases.goForward.invoke(it.id)
192                     }
193                 }
194             }
195             is ToolbarMenu.Item.Reload -> {
196                 val flags = if (item.bypassCache) {
197                     LoadUrlFlags.select(LoadUrlFlags.BYPASS_CACHE)
198                 } else {
199                     LoadUrlFlags.none()
200                 }
202                 currentSession?.let {
203                     sessionUseCases.reload.invoke(it.id, flags = flags)
204                 }
205             }
206             is ToolbarMenu.Item.Stop -> {
207                 currentSession?.let {
208                     sessionUseCases.stopLoading.invoke(it.id)
209                 }
210             }
211             is ToolbarMenu.Item.Share -> {
212                 val directions = NavGraphDirections.actionGlobalShareFragment(
213                     sessionId = currentSession?.id,
214                     data = arrayOf(
215                         ShareData(
216                             url = getProperUrl(currentSession),
217                             title = currentSession?.content?.title,
218                         ),
219                     ),
220                     showPage = true,
221                 )
222                 navController.navigate(directions)
223             }
224             is ToolbarMenu.Item.Settings -> browserAnimator.captureEngineViewAndDrawStatically {
225                 val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment()
226                 navController.nav(R.id.browserFragment, directions)
227             }
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()
236                 }
237                 browserAnimator.captureEngineViewAndDrawStatically {
238                     navController.nav(
239                         R.id.browserFragment,
240                         directions,
241                     )
242                 }
243             }
244             is ToolbarMenu.Item.RequestDesktop -> {
245                 currentSession?.let {
246                     sessionUseCases.requestDesktopSite.invoke(
247                         item.isChecked,
248                         it.id,
249                     )
250                 }
251             }
252             is ToolbarMenu.Item.AddToTopSites -> {
253                 scope.launch {
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, _ ->
263                                 dialog.dismiss()
264                             }
265                             create()
266                         }.show()
267                     } else {
268                         ioScope.launch {
269                             currentSession?.let {
270                                 with(activity.components.useCases.topSitesUseCase) {
271                                     addPinnedSites(it.content.title, it.content.url)
272                                 }
273                             }
274                         }.join()
276                         FenixSnackbar.make(
277                             view = snackbarParent,
278                             duration = Snackbar.LENGTH_SHORT,
279                             isDisplayedWithBrowserToolbar = true,
280                         )
281                             .setText(
282                                 context.getString(R.string.snackbar_added_to_shortcuts),
283                             )
284                             .show()
285                     }
286                 }
287             }
288             is ToolbarMenu.Item.AddToHomeScreen -> {
289                 settings.installPwaOpened = true
290                 MainScope().launch {
291                     with(activity.components.useCases.webAppUseCases) {
292                         if (isInstallable()) {
293                             addToHomescreen()
294                         } else {
295                             val directions =
296                                 BrowserFragmentDirections.actionBrowserFragmentToCreateShortcutFragment()
297                             navController.navigateSafe(R.id.browserFragment, directions)
298                         }
299                     }
300                 }
301             }
302             is ToolbarMenu.Item.FindInPage -> {
303                 findInPageLauncher()
304             }
305             is ToolbarMenu.Item.AddonsManager -> browserAnimator.captureEngineViewAndDrawStatically {
306                 navController.nav(
307                     R.id.browserFragment,
308                     BrowserFragmentDirections.actionGlobalAddonsManagementFragment(),
309                 )
310             }
311             is ToolbarMenu.Item.SaveToCollection -> {
312                 Collections.saveButton.record(
313                     Collections.SaveButtonExtra(
314                         TELEMETRY_BROWSER_IDENTIFIER,
315                     ),
316                 )
318                 currentSession?.let { currentSession ->
319                     val directions =
320                         BrowserFragmentDirections.actionGlobalCollectionCreationFragment(
321                             tabIds = arrayOf(currentSession.id),
322                             selectedTabIds = arrayOf(currentSession.id),
323                             saveCollectionStep = if (tabCollectionStorage.cachedTabCollections.isEmpty()) {
324                                 SaveCollectionStep.NameCollection
325                             } else {
326                                 SaveCollectionStep.SelectCollection
327                             },
328                         )
329                     navController.nav(R.id.browserFragment, directions)
330                 }
331             }
332             is ToolbarMenu.Item.Bookmark -> {
333                 store.state.selectedTab?.let {
334                     getProperUrl(it)?.let { url -> bookmarkTapped(url, it.content.title) }
335                 }
336             }
337             is ToolbarMenu.Item.Bookmarks -> browserAnimator.captureEngineViewAndDrawStatically {
338                 navController.nav(
339                     R.id.browserFragment,
340                     BrowserFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id),
341                 )
342             }
343             is ToolbarMenu.Item.History -> browserAnimator.captureEngineViewAndDrawStatically {
344                 navController.nav(
345                     R.id.browserFragment,
346                     BrowserFragmentDirections.actionGlobalHistoryFragment(),
347                 )
348             }
350             is ToolbarMenu.Item.Downloads -> browserAnimator.captureEngineViewAndDrawStatically {
351                 navController.nav(
352                     R.id.browserFragment,
353                     BrowserFragmentDirections.actionGlobalDownloadsFragment(),
354                 )
355             }
356             is ToolbarMenu.Item.NewTab -> {
357                 navController.navigate(
358                     BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true),
359                 )
360             }
361             is ToolbarMenu.Item.SetDefaultBrowser -> {
362                 activity.openSetDefaultBrowserOption()
363             }
364             is ToolbarMenu.Item.RemoveFromTopSites -> {
365                 scope.launch {
366                     val removedTopSite: TopSite? =
367                         pinnedSiteStorage
368                             .getPinnedSites()
369                             .find { it.url == currentSession?.content?.url }
370                     if (removedTopSite != null) {
371                         ioScope.launch {
372                             currentSession?.let {
373                                 with(activity.components.useCases.topSitesUseCase) {
374                                     removeTopSites(removedTopSite)
375                                 }
376                             }
377                         }.join()
378                     }
380                     FenixSnackbar.make(
381                         view = snackbarParent,
382                         duration = Snackbar.LENGTH_SHORT,
383                         isDisplayedWithBrowserToolbar = true,
384                     )
385                         .setText(
386                             snackbarParent.context.getString(R.string.snackbar_top_site_removed),
387                         )
388                         .show()
389                 }
390             }
391         }
392     }
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
399             } else {
400                 currentSession.content.url
401             }
402         }
403     }
405     @Suppress("ComplexMethod")
406     private fun trackToolbarItemInteraction(item: ToolbarMenu.Item) {
407         when (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"))
433                 } else {
434                     Events.browserMenuAction.record(Events.BrowserMenuActionExtra("desktop_view_off"))
435                 }
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"))
462         }
463     }
465     companion object {
466         internal const val TELEMETRY_BROWSER_IDENTIFIER = "browserMenu"
467     }