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.browser
7 import android.content.Context
8 import android.content.Intent
9 import android.os.Build
10 import android.os.Bundle
11 import android.view.Gravity
12 import android.view.LayoutInflater
13 import android.view.View
14 import android.view.ViewGroup
15 import android.view.accessibility.AccessibilityManager
16 import androidx.annotation.CallSuper
17 import androidx.annotation.VisibleForTesting
18 import androidx.coordinatorlayout.widget.CoordinatorLayout
19 import androidx.core.net.toUri
20 import androidx.core.view.isVisible
21 import androidx.fragment.app.Fragment
22 import androidx.fragment.app.activityViewModels
23 import androidx.lifecycle.lifecycleScope
24 import androidx.navigation.NavController
25 import androidx.navigation.fragment.findNavController
26 import androidx.preference.PreferenceManager
27 import com.google.android.material.snackbar.Snackbar
28 import kotlinx.android.synthetic.main.fragment_browser.*
29 import kotlinx.android.synthetic.main.fragment_browser.view.*
30 import kotlinx.coroutines.Dispatchers.IO
31 import kotlinx.coroutines.Dispatchers.Main
32 import kotlinx.coroutines.ExperimentalCoroutinesApi
33 import kotlinx.coroutines.Job
34 import kotlinx.coroutines.flow.collect
35 import kotlinx.coroutines.flow.map
36 import kotlinx.coroutines.flow.mapNotNull
37 import kotlinx.coroutines.launch
38 import kotlinx.coroutines.withContext
39 import mozilla.appservices.places.BookmarkRoot
40 import mozilla.components.browser.session.Session
41 import mozilla.components.browser.state.action.ContentAction
42 import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
43 import mozilla.components.browser.state.selector.findTab
44 import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
45 import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
46 import mozilla.components.browser.state.selector.selectedTab
47 import mozilla.components.browser.state.state.SessionState
48 import mozilla.components.browser.state.state.TabSessionState
49 import mozilla.components.browser.state.state.content.DownloadState
50 import mozilla.components.browser.state.store.BrowserStore
51 import mozilla.components.browser.thumbnails.BrowserThumbnails
52 import mozilla.components.concept.engine.prompt.ShareData
53 import mozilla.components.feature.accounts.FxaCapability
54 import mozilla.components.feature.accounts.FxaWebChannelFeature
55 import mozilla.components.feature.app.links.AppLinksFeature
56 import mozilla.components.feature.contextmenu.ContextMenuCandidate
57 import mozilla.components.feature.contextmenu.ContextMenuFeature
58 import mozilla.components.feature.downloads.DownloadsFeature
59 import mozilla.components.feature.downloads.manager.FetchDownloadManager
60 import mozilla.components.feature.intent.ext.EXTRA_SESSION_ID
61 import mozilla.components.feature.media.fullscreen.MediaSessionFullscreenFeature
62 import mozilla.components.feature.privatemode.feature.SecureWindowFeature
63 import mozilla.components.feature.prompts.PromptFeature
64 import mozilla.components.feature.prompts.share.ShareDelegate
65 import mozilla.components.feature.readerview.ReaderViewFeature
66 import mozilla.components.feature.search.SearchFeature
67 import mozilla.components.feature.session.FullScreenFeature
68 import mozilla.components.feature.session.PictureInPictureFeature
69 import mozilla.components.feature.session.SessionFeature
70 import mozilla.components.feature.session.SwipeRefreshFeature
71 import mozilla.components.feature.session.behavior.EngineViewBottomBehavior
72 import mozilla.components.feature.sitepermissions.SitePermissions
73 import mozilla.components.feature.sitepermissions.SitePermissionsFeature
74 import mozilla.components.lib.state.ext.consumeFlow
75 import mozilla.components.lib.state.ext.flowScoped
76 import mozilla.components.service.sync.logins.DefaultLoginValidationDelegate
77 import mozilla.components.support.base.feature.PermissionsFeature
78 import mozilla.components.support.base.feature.UserInteractionHandler
79 import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
80 import mozilla.components.support.ktx.android.view.exitImmersiveModeIfNeeded
81 import mozilla.components.support.ktx.android.view.hideKeyboard
82 import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
83 import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
84 import org.mozilla.fenix.FeatureFlags
85 import org.mozilla.fenix.HomeActivity
86 import org.mozilla.fenix.IntentReceiverActivity
87 import org.mozilla.fenix.NavGraphDirections
88 import org.mozilla.fenix.OnBackLongPressedListener
89 import org.mozilla.fenix.addons.runIfFragmentIsAttached
90 import org.mozilla.fenix.R
91 import org.mozilla.fenix.browser.browsingmode.BrowsingMode
92 import org.mozilla.fenix.browser.readermode.DefaultReaderModeController
93 import org.mozilla.fenix.components.FenixSnackbar
94 import org.mozilla.fenix.components.FindInPageIntegration
95 import org.mozilla.fenix.components.StoreProvider
96 import org.mozilla.fenix.components.metrics.Event
97 import org.mozilla.fenix.components.toolbar.BrowserFragmentState
98 import org.mozilla.fenix.components.toolbar.BrowserFragmentStore
99 import org.mozilla.fenix.components.toolbar.BrowserInteractor
100 import org.mozilla.fenix.components.toolbar.BrowserToolbarView
101 import org.mozilla.fenix.components.toolbar.BrowserToolbarViewInteractor
102 import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarController
103 import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarMenuController
104 import org.mozilla.fenix.components.toolbar.SwipeRefreshScrollingViewBehavior
105 import org.mozilla.fenix.components.toolbar.ToolbarIntegration
106 import org.mozilla.fenix.components.toolbar.ToolbarPosition
107 import org.mozilla.fenix.downloads.DownloadService
108 import org.mozilla.fenix.downloads.DynamicDownloadDialog
109 import org.mozilla.fenix.ext.accessibilityManager
110 import org.mozilla.fenix.ext.breadcrumb
111 import org.mozilla.fenix.ext.components
112 import org.mozilla.fenix.ext.enterToImmersiveMode
113 import org.mozilla.fenix.ext.getPreferenceKey
114 import org.mozilla.fenix.ext.hideToolbar
115 import org.mozilla.fenix.ext.metrics
116 import org.mozilla.fenix.ext.nav
117 import org.mozilla.fenix.ext.requireComponents
118 import org.mozilla.fenix.ext.settings
119 import org.mozilla.fenix.home.HomeScreenViewModel
120 import org.mozilla.fenix.home.SharedViewModel
121 import org.mozilla.fenix.onboarding.FenixOnboarding
122 import org.mozilla.fenix.settings.SupportUtils
123 import org.mozilla.fenix.theme.ThemeManager
124 import org.mozilla.fenix.utils.allowUndo
125 import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration
126 import java.lang.ref.WeakReference
127 import mozilla.components.feature.media.fullscreen.MediaFullscreenOrientationFeature
128 import org.mozilla.fenix.FeatureFlags.newMediaSessionApi
131 * Base fragment extended by [BrowserFragment].
132 * This class only contains shared code focused on the main browsing content.
133 * UI code specific to the app or to custom tabs can be found in the subclasses.
135 @ExperimentalCoroutinesApi
136 @Suppress("TooManyFunctions", "LargeClass")
137 abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
138 OnBackLongPressedListener, AccessibilityManager.AccessibilityStateChangeListener {
140 private lateinit var browserFragmentStore: BrowserFragmentStore
141 private lateinit var browserAnimator: BrowserAnimator
143 private var _browserInteractor: BrowserToolbarViewInteractor? = null
144 protected val browserInteractor: BrowserToolbarViewInteractor
145 get() = _browserInteractor!!
147 private var _browserToolbarView: BrowserToolbarView? = null
149 internal val browserToolbarView: BrowserToolbarView
150 get() = _browserToolbarView!!
152 protected val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewFeature>()
153 protected val thumbnailsFeature = ViewBoundFeatureWrapper<BrowserThumbnails>()
155 private val sessionFeature = ViewBoundFeatureWrapper<SessionFeature>()
156 private val contextMenuFeature = ViewBoundFeatureWrapper<ContextMenuFeature>()
157 private val downloadsFeature = ViewBoundFeatureWrapper<DownloadsFeature>()
158 private val appLinksFeature = ViewBoundFeatureWrapper<AppLinksFeature>()
159 private val promptsFeature = ViewBoundFeatureWrapper<PromptFeature>()
160 private val findInPageIntegration = ViewBoundFeatureWrapper<FindInPageIntegration>()
161 private val toolbarIntegration = ViewBoundFeatureWrapper<ToolbarIntegration>()
162 private val sitePermissionsFeature = ViewBoundFeatureWrapper<SitePermissionsFeature>()
163 private val fullScreenFeature = ViewBoundFeatureWrapper<FullScreenFeature>()
164 private val swipeRefreshFeature = ViewBoundFeatureWrapper<SwipeRefreshFeature>()
165 private val webchannelIntegration = ViewBoundFeatureWrapper<FxaWebChannelFeature>()
166 private val sitePermissionWifiIntegration =
167 ViewBoundFeatureWrapper<SitePermissionsWifiIntegration>()
168 private val secureWindowFeature = ViewBoundFeatureWrapper<SecureWindowFeature>()
169 private var fullScreenMediaFeature =
170 ViewBoundFeatureWrapper<MediaFullscreenOrientationFeature>()
171 private var fullScreenMediaSessionFeature =
172 ViewBoundFeatureWrapper<MediaSessionFullscreenFeature>()
173 private val searchFeature = ViewBoundFeatureWrapper<SearchFeature>()
174 private var pipFeature: PictureInPictureFeature? = null
176 var customTabSessionId: String? = null
179 internal var browserInitialized: Boolean = false
180 private var initUIJob: Job? = null
181 protected var webAppToolbarShouldBeVisible = true
183 private val sharedViewModel: SharedViewModel by activityViewModels()
186 internal val onboarding by lazy { FenixOnboarding(requireContext()) }
189 override fun onCreateView(
190 inflater: LayoutInflater,
191 container: ViewGroup?,
192 savedInstanceState: Bundle?
194 customTabSessionId = requireArguments().getString(EXTRA_SESSION_ID)
196 // Diagnostic breadcrumb for "Display already aquired" crash:
197 // https://github.com/mozilla-mobile/android-components/issues/7960
199 message = "onCreateView()",
201 "customTabSessionId" to customTabSessionId.toString()
205 val view = inflater.inflate(R.layout.fragment_browser, container, false)
207 val activity = activity as HomeActivity
208 activity.themeManager.applyStatusBarTheme(activity)
210 browserFragmentStore = StoreProvider.get(this) {
211 BrowserFragmentStore(
212 BrowserFragmentState()
219 final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
220 browserInitialized = initializeUI(view) != null
222 if (customTabSessionId == null) {
223 // We currently only need this observer to navigate to home
224 // in case all tabs have been removed on startup. No need to
225 // this if we have a known session to display.
226 observeRestoreComplete(requireComponents.core.store, findNavController())
229 observeTabSelection(requireComponents.core.store)
231 lifecycleScope.launch(IO) {
232 if (!onboarding.userHasBeenOnboarded()) {
233 observeTabSource(requireComponents.core.store)
237 requireContext().accessibilityManager.addAccessibilityStateChangeListener(this)
240 private val homeViewModel: HomeScreenViewModel by activityViewModels()
242 @Suppress("ComplexMethod", "LongMethod")
245 internal open fun initializeUI(view: View): Session? {
246 val context = requireContext()
247 val sessionManager = context.components.core.sessionManager
248 val store = context.components.core.store
249 val activity = requireActivity() as HomeActivity
251 val toolbarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height)
253 browserAnimator = BrowserAnimator(
254 fragment = WeakReference(this),
255 engineView = WeakReference(engineView),
256 swipeRefresh = WeakReference(swipeRefresh),
257 viewLifecycleScope = WeakReference(viewLifecycleOwner.lifecycleScope)
259 beginAnimateInIfNecessary()
262 return getSessionById()?.also { _ ->
263 val openInFenixIntent = Intent(context, IntentReceiverActivity::class.java).apply {
264 action = Intent.ACTION_VIEW
265 putExtra(HomeActivity.OPEN_TO_BROWSER, true)
268 val readerMenuController = DefaultReaderModeController(
270 view.readerViewControlsBar,
271 isPrivate = activity.browsingModeManager.mode.isPrivate
273 val browserToolbarController = DefaultBrowserToolbarController(
276 navController = findNavController(),
277 metrics = requireComponents.analytics.metrics,
278 readerModeController = readerMenuController,
279 sessionManager = requireComponents.core.sessionManager,
280 engineView = engineView,
281 homeViewModel = homeViewModel,
282 customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) },
283 onTabCounterClicked = {
284 thumbnailsFeature.get()?.requestScreenshot()
285 findNavController().nav(
286 R.id.browserFragment,
287 BrowserFragmentDirections.actionGlobalTabTrayDialogFragment()
290 onCloseTab = { closedSession ->
291 val tab = store.state.findTab(closedSession.id) ?: return@DefaultBrowserToolbarController
293 val snackbarMessage = if (tab.content.private) {
294 requireContext().getString(R.string.snackbar_private_tab_closed)
296 requireContext().getString(R.string.snackbar_tab_closed)
299 viewLifecycleOwner.lifecycleScope.allowUndo(
300 requireView().browserLayout,
302 requireContext().getString(R.string.snackbar_deleted_undo),
304 requireComponents.useCases.tabsUseCases.undo.invoke()
306 paddedForBottomToolbar = true,
311 val browserToolbarMenuController = DefaultBrowserToolbarMenuController(
313 navController = findNavController(),
314 metrics = requireComponents.analytics.metrics,
315 settings = context.settings(),
316 readerModeController = readerMenuController,
317 sessionManager = requireComponents.core.sessionManager,
318 sessionFeature = sessionFeature,
319 findInPageLauncher = { findInPageIntegration.withFeature { it.launch() } },
320 swipeRefresh = swipeRefresh,
321 browserAnimator = browserAnimator,
322 customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) },
323 openInFenixIntent = openInFenixIntent,
324 bookmarkTapped = { url: String, title: String ->
325 viewLifecycleOwner.lifecycleScope.launch {
326 bookmarkTapped(url, title)
329 scope = viewLifecycleOwner.lifecycleScope,
330 tabCollectionStorage = requireComponents.core.tabCollectionStorage,
331 topSitesStorage = requireComponents.core.topSitesStorage,
335 _browserInteractor = BrowserInteractor(
336 browserToolbarController,
337 browserToolbarMenuController
340 _browserToolbarView = BrowserToolbarView(
341 container = view.browserLayout,
342 toolbarPosition = context.settings().toolbarPosition,
343 interactor = browserInteractor,
344 customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) },
345 lifecycleOwner = viewLifecycleOwner
348 toolbarIntegration.set(
349 feature = browserToolbarView.toolbarIntegration,
354 findInPageIntegration.set(
355 feature = FindInPageIntegration(
357 sessionId = customTabSessionId,
358 stub = view.stubFindInPage,
359 engineView = view.engineView,
360 toolbar = browserToolbarView.view
366 browserToolbarView.view.display.setOnSiteSecurityClickedListener {
367 showQuickSettingsDialog()
370 browserToolbarView.view.display.setOnTrackingProtectionClickedListener {
371 context.metrics.track(Event.TrackingProtectionIconPressed)
372 showTrackingProtectionPanel()
375 contextMenuFeature.set(
376 feature = ContextMenuFeature(
377 fragmentManager = parentFragmentManager,
379 candidates = getContextMenuCandidates(context, view.browserLayout),
380 engineView = view.engineView,
381 useCases = context.components.useCases.contextMenuUseCases,
382 tabId = customTabSessionId
388 val allowScreenshotsInPrivateMode = context.settings().allowScreenshotsInPrivateMode
389 secureWindowFeature.set(
390 feature = SecureWindowFeature(
391 window = requireActivity().window,
393 customTabId = customTabSessionId,
394 isSecure = { !allowScreenshotsInPrivateMode && it.content.private }
400 if (newMediaSessionApi) {
401 fullScreenMediaSessionFeature.set(
402 feature = MediaSessionFullscreenFeature(
404 context.components.core.store
410 fullScreenMediaFeature.set(
411 feature = MediaFullscreenOrientationFeature(
413 context.components.core.store
420 val downloadFeature = DownloadsFeature(
421 context.applicationContext,
423 useCases = context.components.useCases.downloadUseCases,
424 fragmentManager = childFragmentManager,
425 tabId = customTabSessionId,
426 downloadManager = FetchDownloadManager(
427 context.applicationContext,
429 DownloadService::class
431 shouldForwardToThirdParties = {
432 PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
433 context.getPreferenceKey(R.string.pref_key_external_download_manager), false
436 promptsStyling = DownloadsFeature.PromptsStyling(
437 gravity = Gravity.BOTTOM,
438 shouldWidthMatchParent = true,
439 positiveButtonBackgroundColor = ThemeManager.resolveAttribute(
443 positiveButtonTextColor = ThemeManager.resolveAttribute(
447 positiveButtonRadius = (resources.getDimensionPixelSize(R.dimen.tab_corner_radius)).toFloat()
449 onNeedToRequestPermissions = { permissions ->
450 requestPermissions(permissions, REQUEST_CODE_DOWNLOAD_PERMISSIONS)
454 downloadFeature.onDownloadStopped = { downloadState, _, downloadJobStatus ->
455 // If the download is just paused, don't show any in-app notification
456 if (downloadJobStatus == DownloadState.Status.COMPLETED ||
457 downloadJobStatus == DownloadState.Status.FAILED
460 saveDownloadDialogState(
461 downloadState.sessionId,
466 val dynamicDownloadDialog = DynamicDownloadDialog(
467 container = view.browserLayout,
468 downloadState = downloadState,
469 didFail = downloadJobStatus == DownloadState.Status.FAILED,
470 tryAgain = downloadFeature::tryAgain,
473 view = view.browserLayout,
474 duration = Snackbar.LENGTH_SHORT,
475 isDisplayedWithBrowserToolbar = true
477 .setText(context.getString(R.string.mozac_feature_downloads_could_not_open_file))
480 view = view.viewDynamicDownloadDialog,
481 toolbarHeight = toolbarHeight,
482 onDismiss = { sharedViewModel.downloadDialogState.remove(downloadState.sessionId) }
485 // Don't show the dialog if we aren't in the tab that started the download
486 if (downloadState.sessionId == sessionManager.selectedSession?.id) {
487 dynamicDownloadDialog.show()
488 browserToolbarView.expand()
493 resumeDownloadDialogState(
494 sessionManager.selectedSession?.id,
495 store, view, context, toolbarHeight
498 downloadsFeature.set(
504 pipFeature = PictureInPictureFeature(
506 activity = requireActivity(),
507 crashReporting = context.components.analytics.crashReporter,
508 tabId = customTabSessionId
512 feature = AppLinksFeature(
514 sessionManager = sessionManager,
515 sessionId = customTabSessionId,
516 fragmentManager = parentFragmentManager,
517 launchInApp = { context.settings().openLinksInExternalApp },
518 loadUrlUseCase = context.components.useCases.sessionUseCases.loadUrl
525 feature = PromptFeature(
528 customTabId = customTabSessionId,
529 fragmentManager = parentFragmentManager,
530 loginValidationDelegate = DefaultLoginValidationDelegate(
531 context.components.core.lazyPasswordsStorage
533 isSaveLoginEnabled = {
534 context.settings().shouldPromptToSaveLogins
536 loginExceptionStorage = context.components.core.loginExceptionStorage,
537 shareDelegate = object : ShareDelegate {
538 override fun showShareSheet(
540 shareData: ShareData,
541 onDismiss: () -> Unit,
542 onSuccess: () -> Unit
544 val directions = NavGraphDirections.actionGlobalShareFragment(
545 data = arrayOf(shareData),
547 sessionId = getSessionById()?.id
549 findNavController().navigate(directions)
552 onNeedToRequestPermissions = { permissions ->
553 requestPermissions(permissions, REQUEST_CODE_PROMPT_PERMISSIONS)
555 loginPickerView = loginSelectBar,
557 browserAnimator.captureEngineViewAndDrawStatically {
559 NavGraphDirections.actionGlobalSavedLoginsAuthFragment()
560 findNavController().navigate(directions)
569 feature = SessionFeature(
570 requireComponents.core.store,
571 requireComponents.useCases.sessionUseCases.goBack,
580 feature = SearchFeature(store, customTabSessionId) { request, tabId ->
581 val parentSession = sessionManager.findSessionById(tabId)
582 val useCase = if (request.isPrivate) {
583 requireComponents.useCases.searchUseCases.newPrivateTabSearch
585 requireComponents.useCases.searchUseCases.newTabSearch
588 if (parentSession?.isCustomTabSession() == true) {
589 useCase.invoke(request.query)
590 requireActivity().startActivity(openInFenixIntent)
592 useCase.invoke(request.query, parentSessionId = parentSession?.id)
599 val accentHighContrastColor =
600 ThemeManager.resolveAttribute(R.attr.accentHighContrast, context)
602 sitePermissionsFeature.set(
603 feature = SitePermissionsFeature(
605 storage = context.components.core.permissionStorage.permissionsStorage,
606 fragmentManager = parentFragmentManager,
607 promptsStyling = SitePermissionsFeature.PromptsStyling(
608 gravity = getAppropriateLayoutGravity(),
609 shouldWidthMatchParent = true,
610 positiveButtonBackgroundColor = accentHighContrastColor,
611 positiveButtonTextColor = R.color.photonWhite
613 sessionId = customTabSessionId,
614 onNeedToRequestPermissions = { permissions ->
615 requestPermissions(permissions, REQUEST_CODE_APP_PERMISSIONS)
617 onShouldShowRequestPermissionRationale = {
618 shouldShowRequestPermissionRationale(
628 sitePermissionWifiIntegration.set(
629 feature = SitePermissionsWifiIntegration(
630 settings = context.settings(),
631 wifiConnectionMonitor = context.components.wifiConnectionMonitor
637 context.settings().setSitePermissionSettingListener(viewLifecycleOwner) {
638 // If the user connects to WIFI while on the BrowserFragment, this will update the
639 // SitePermissionsRules (specifically autoplay) accordingly
640 runIfFragmentIsAttached {
641 assignSitePermissionsRules()
644 assignSitePermissionsRules()
646 fullScreenFeature.set(
647 feature = FullScreenFeature(
648 requireComponents.core.store,
649 requireComponents.useCases.sessionUseCases,
658 expandToolbarOnNavigation(store)
660 store.flowScoped(viewLifecycleOwner) { flow ->
661 flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(customTabSessionId) }
662 .ifChanged { tab -> tab.content.pictureInPictureEnabled }
663 .collect { tab -> pipModeChanged(tab) }
666 view.swipeRefresh.isEnabled = shouldPullToRefreshBeEnabled()
668 if (view.swipeRefresh.isEnabled) {
669 val primaryTextColor =
670 ThemeManager.resolveAttribute(R.attr.primaryText, context)
671 view.swipeRefresh.setColorSchemeColors(primaryTextColor)
672 swipeRefreshFeature.set(
673 feature = SwipeRefreshFeature(
674 requireComponents.core.store,
675 context.components.useCases.sessionUseCases.reload,
684 webchannelIntegration.set(
685 feature = FxaWebChannelFeature(
688 requireComponents.core.engine,
689 requireComponents.core.store,
690 requireComponents.backgroundServices.accountManager,
691 requireComponents.backgroundServices.serverConfig,
692 setOf(FxaCapability.CHOOSE_WHAT_TO_SYNC)
698 initializeEngineView(toolbarHeight)
703 internal fun expandToolbarOnNavigation(store: BrowserStore) {
704 consumeFlow(store) { flow ->
706 state -> state.findCustomTabOrSelectedTab(customTabSessionId)
709 tab -> arrayOf(tab.content.url, tab.content.loadRequest)
712 browserToolbarView.expand()
718 * Preserves current state of the [DynamicDownloadDialog] to persist through tab changes and
719 * other fragments navigation.
721 private fun saveDownloadDialogState(
723 downloadState: DownloadState,
724 downloadJobStatus: DownloadState.Status
726 sessionId?.let { id ->
727 sharedViewModel.downloadDialogState[id] = Pair(
729 downloadJobStatus == DownloadState.Status.FAILED
735 * Re-initializes [DynamicDownloadDialog] if the user hasn't dismissed the dialog
736 * before navigating away from it's original tab.
737 * onTryAgain it will use [ContentAction.UpdateDownloadAction] to re-enqueue the former failed
738 * download, because [DownloadsFeature] clears any queued downloads onStop.
741 internal fun resumeDownloadDialogState(
748 val savedDownloadState =
749 sharedViewModel.downloadDialogState[sessionId]
751 if (savedDownloadState == null || sessionId == null) {
752 view.viewDynamicDownloadDialog.visibility = View.GONE
756 val onTryAgain: (String) -> Unit = {
757 savedDownloadState.first?.let { dlState ->
759 ContentAction.UpdateDownloadAction(
760 sessionId, dlState.copy(skipConfirmation = true)
766 val onCannotOpenFile = {
768 view = view.browserLayout,
769 duration = Snackbar.LENGTH_SHORT,
770 isDisplayedWithBrowserToolbar = true
772 .setText(context.getString(R.string.mozac_feature_downloads_could_not_open_file))
776 val onDismiss: () -> Unit =
777 { sharedViewModel.downloadDialogState.remove(sessionId) }
779 DynamicDownloadDialog(
780 container = view.browserLayout,
781 downloadState = savedDownloadState.first,
782 didFail = savedDownloadState.second,
783 tryAgain = onTryAgain,
784 onCannotOpenFile = onCannotOpenFile,
785 view = view.viewDynamicDownloadDialog,
786 toolbarHeight = toolbarHeight,
787 onDismiss = onDismiss
790 browserToolbarView.expand()
794 internal fun shouldPullToRefreshBeEnabled(): Boolean {
795 return FeatureFlags.pullToRefreshEnabled &&
796 requireContext().settings().isPullToRefreshEnabledInBrowser &&
797 !(requireActivity() as HomeActivity).isImmersive
800 private fun initializeEngineView(toolbarHeight: Int) {
801 val context = requireContext()
803 if (context.settings().isDynamicToolbarEnabled) {
804 engineView.setDynamicToolbarMaxHeight(toolbarHeight)
806 val behavior = when (context.settings().toolbarPosition) {
807 // Set engineView dynamic vertical clipping depending on the toolbar position.
808 ToolbarPosition.BOTTOM -> EngineViewBottomBehavior(context, null)
809 // Set scroll flags depending on if if the browser or the website is doing the scroll.
810 ToolbarPosition.TOP -> SwipeRefreshScrollingViewBehavior(
818 (swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams).behavior = behavior
820 // Ensure webpage's bottom elements are aligned to the very bottom of the engineView.
821 engineView.setDynamicToolbarMaxHeight(0)
823 // Effectively place the engineView on top of the toolbar if that is not dynamic.
824 if (context.settings().shouldUseBottomToolbar) {
825 val browserEngine = swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams
826 browserEngine.bottomMargin =
827 requireContext().resources.getDimensionPixelSize(R.dimen.browser_toolbar_height)
833 * Returns a list of context menu items [ContextMenuCandidate] for the context menu
835 protected abstract fun getContextMenuCandidates(
838 ): List<ContextMenuCandidate>
841 override fun onStart() {
843 sitePermissionWifiIntegration.get()?.maybeAddWifiConnectedListener()
847 internal fun observeRestoreComplete(store: BrowserStore, navController: NavController) {
848 val activity = activity as HomeActivity
849 consumeFlow(store) { flow ->
850 flow.map { state -> state.restoreComplete }
852 .collect { restored ->
854 // Once tab restoration is complete, if there are no tabs to show in the browser, go home
856 store.state.getNormalOrPrivateTabs(
857 activity.browsingModeManager.mode.isPrivate
859 if (tabs.isEmpty() || store.state.selectedTabId == null) {
860 navController.popBackStack(R.id.homeFragment, false)
868 internal fun observeTabSelection(store: BrowserStore) {
869 consumeFlow(store) { flow ->
877 handleTabSelected(it)
883 @Suppress("ComplexCondition")
884 internal fun observeTabSource(store: BrowserStore) {
885 consumeFlow(store) { flow ->
886 flow.mapNotNull { state ->
890 if (!onboarding.userHasBeenOnboarded() &&
891 it.content.loadRequest?.triggeredByRedirect != true &&
892 it.source !in intentSourcesList &&
893 it.content.url !in onboardingLinksList
901 private fun handleTabSelected(selectedTab: TabSessionState) {
902 if (!this.isRemoving) {
903 updateThemeForSession(selectedTab)
906 if (browserInitialized) {
908 fullScreenChanged(false)
909 browserToolbarView.expand()
911 val toolbarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height)
912 val context = requireContext()
913 resumeDownloadDialogState(selectedTab.id, context.components.core.store, view, context, toolbarHeight)
917 browserInitialized = initializeUI(view) != null
923 override fun onResume() {
925 val components = requireComponents
927 val preferredColorScheme = components.core.getPreferredColorScheme()
928 if (components.core.engine.settings.preferredColorScheme != preferredColorScheme) {
929 components.core.engine.settings.preferredColorScheme = preferredColorScheme
930 components.useCases.sessionUseCases.reload()
934 components.core.store.state.findTabOrCustomTabOrSelectedTab(customTabSessionId)?.let {
935 updateThemeForSession(it)
940 override fun onPause() {
942 if (findNavController().currentDestination?.id != R.id.searchDialogFragment) {
948 override fun onStop() {
952 requireComponents.core.store.state.findTabOrCustomTabOrSelectedTab(customTabSessionId)
954 // If we didn't enter PiP, exit full screen on stop
955 if (!session.content.pictureInPictureEnabled && fullScreenFeature.onBackPressed()) {
956 fullScreenChanged(false)
962 override fun onBackPressed(): Boolean {
963 return findInPageIntegration.onBackPressed() ||
964 fullScreenFeature.onBackPressed() ||
965 sessionFeature.onBackPressed() ||
966 removeSessionIfNeeded()
969 override fun onBackLongPressed(): Boolean {
970 findNavController().navigate(
971 NavGraphDirections.actionGlobalTabHistoryDialogFragment(
972 activeSessionId = customTabSessionId
979 * Saves the external app session ID to be restored later in [onViewStateRestored].
981 final override fun onSaveInstanceState(outState: Bundle) {
982 super.onSaveInstanceState(outState)
983 outState.putString(KEY_CUSTOM_TAB_SESSION_ID, customTabSessionId)
987 * Retrieves the external app session ID saved by [onSaveInstanceState].
989 final override fun onViewStateRestored(savedInstanceState: Bundle?) {
990 super.onViewStateRestored(savedInstanceState)
991 savedInstanceState?.getString(KEY_CUSTOM_TAB_SESSION_ID)?.let {
992 if (requireComponents.core.sessionManager.findSessionById(it)?.customTabConfig != null) {
993 customTabSessionId = it
999 * Forwards permission grant results to one of the features.
1001 final override fun onRequestPermissionsResult(
1003 permissions: Array<String>,
1004 grantResults: IntArray
1006 val feature: PermissionsFeature? = when (requestCode) {
1007 REQUEST_CODE_DOWNLOAD_PERMISSIONS -> downloadsFeature.get()
1008 REQUEST_CODE_PROMPT_PERMISSIONS -> promptsFeature.get()
1009 REQUEST_CODE_APP_PERMISSIONS -> sitePermissionsFeature.get()
1012 feature?.onPermissionsResult(permissions, grantResults)
1016 * Forwards activity results to the prompt feature.
1018 final override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
1019 promptsFeature.withFeature { it.onActivityResult(requestCode, resultCode, data) }
1023 * Removes the session if it was opened by an ACTION_VIEW intent
1024 * or if it has a parent session and no more history
1026 protected open fun removeSessionIfNeeded(): Boolean {
1027 getSessionById()?.let { session ->
1028 return if (session.source == SessionState.Source.ACTION_VIEW) {
1030 requireComponents.useCases.tabsUseCases.removeTab(session)
1033 if (session.hasParentSession) {
1034 // The removeTab use case does not currently select a parent session, so
1035 // we are using sessionManager.remove
1036 requireComponents.core.sessionManager.remove(
1038 selectParentIfExists = true
1041 // We want to return to home if this session didn't have a parent session to select.
1042 val goToOverview = !session.hasParentSession
1049 protected abstract fun navToQuickSettingsSheet(
1051 sitePermissions: SitePermissions?
1054 protected abstract fun navToTrackingProtectionPanel(session: Session)
1057 * Returns the layout [android.view.Gravity] for the quick settings and ETP dialog.
1059 protected fun getAppropriateLayoutGravity(): Int =
1060 requireComponents.settings.toolbarPosition.androidGravity
1063 * Updates the site permissions rules based on user settings.
1065 private fun assignSitePermissionsRules() {
1066 val rules = requireComponents.settings.getSitePermissionsCustomSettingsRules()
1068 sitePermissionsFeature.withFeature {
1069 it.sitePermissionsRules = rules
1074 * Displays the quick settings dialog,
1075 * which lets the user control tracking protection and site settings.
1077 private fun showQuickSettingsDialog() {
1078 val session = getSessionById() ?: return
1079 viewLifecycleOwner.lifecycleScope.launch(Main) {
1080 val sitePermissions: SitePermissions? = session.url.toUri().host?.let { host ->
1081 val storage = requireComponents.core.permissionStorage
1082 storage.findSitePermissionsBy(host)
1086 navToQuickSettingsSheet(session, sitePermissions)
1091 private fun showTrackingProtectionPanel() {
1092 val session = getSessionById() ?: return
1094 navToTrackingProtectionPanel(session)
1099 * Set the activity normal/private theme to match the current session.
1102 internal fun updateThemeForSession(session: SessionState) {
1103 val sessionMode = BrowsingMode.fromBoolean(session.content.private)
1104 (activity as HomeActivity).browsingModeManager.mode = sessionMode
1108 * Returns the current session.
1110 protected fun getSessionById(): Session? {
1111 val sessionManager = requireComponents.core.sessionManager
1112 val localCustomTabId = customTabSessionId
1113 return if (localCustomTabId != null) {
1114 sessionManager.findSessionById(localCustomTabId)
1116 sessionManager.selectedSession
1120 private suspend fun bookmarkTapped(sessionUrl: String, sessionTitle: String) = withContext(IO) {
1121 val bookmarksStorage = requireComponents.core.bookmarksStorage
1123 bookmarksStorage.getBookmarksWithUrl(sessionUrl).firstOrNull { it.url == sessionUrl }
1124 if (existing != null) {
1125 // Bookmark exists, go to edit fragment
1128 R.id.browserFragment,
1129 BrowserFragmentDirections.actionGlobalBookmarkEditFragment(existing.guid, true)
1133 // Save bookmark, then go to edit fragment
1134 val guid = bookmarksStorage.addItem(
1135 BookmarkRoot.Mobile.id,
1137 title = sessionTitle,
1142 requireComponents.analytics.metrics.track(Event.AddBookmark)
1146 view = view.browserLayout,
1147 duration = FenixSnackbar.LENGTH_LONG,
1148 isDisplayedWithBrowserToolbar = true
1150 .setText(getString(R.string.bookmark_saved_snackbar))
1151 .setAction(getString(R.string.edit_bookmark_snackbar_action)) {
1153 R.id.browserFragment,
1154 BrowserFragmentDirections.actionGlobalBookmarkEditFragment(
1166 override fun onHomePressed() = pipFeature?.onHomePressed() ?: false
1169 * Exit fullscreen mode when exiting PIP mode
1171 private fun pipModeChanged(session: SessionState) {
1172 if (!session.content.pictureInPictureEnabled && session.content.fullScreen) {
1174 fullScreenChanged(false)
1178 final override fun onPictureInPictureModeChanged(enabled: Boolean) {
1179 pipFeature?.onPictureInPictureModeChanged(enabled)
1182 private fun viewportFitChange(layoutInDisplayCutoutMode: Int) {
1183 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
1184 val layoutParams = activity?.window?.attributes
1185 layoutParams?.layoutInDisplayCutoutMode = layoutInDisplayCutoutMode
1186 activity?.window?.attributes = layoutParams
1191 internal fun fullScreenChanged(inFullScreen: Boolean) {
1193 // Close find in page bar if opened
1194 findInPageIntegration.onBackPressed()
1196 view = requireView().browserLayout,
1197 duration = Snackbar.LENGTH_SHORT,
1198 isDisplayedWithBrowserToolbar = false
1200 .setText(getString(R.string.full_screen_notification))
1202 activity?.enterToImmersiveMode()
1203 browserToolbarView.view.isVisible = false
1204 val browserEngine = swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams
1205 browserEngine.bottomMargin = 0
1207 engineView.setDynamicToolbarMaxHeight(0)
1208 browserToolbarView.expand()
1209 // Without this, fullscreen has a margin at the top.
1210 engineView.setVerticalClipping(0)
1212 activity?.exitImmersiveModeIfNeeded()
1213 (activity as? HomeActivity)?.let { activity ->
1214 activity.themeManager.applyStatusBarTheme(activity)
1216 if (webAppToolbarShouldBeVisible) {
1217 browserToolbarView.view.isVisible = true
1218 val toolbarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height)
1219 initializeEngineView(toolbarHeight)
1225 * Dereference these views when the fragment view is destroyed to prevent memory leaks
1227 override fun onDestroyView() {
1228 super.onDestroyView()
1230 // Diagnostic breadcrumb for "Display already aquired" crash:
1231 // https://github.com/mozilla-mobile/android-components/issues/7960
1233 message = "onDestroyView()"
1236 requireContext().accessibilityManager.removeAccessibilityStateChangeListener(this)
1237 _browserToolbarView = null
1238 _browserInteractor = null
1241 override fun onAttach(context: Context) {
1242 super.onAttach(context)
1244 // Diagnostic breadcrumb for "Display already aquired" crash:
1245 // https://github.com/mozilla-mobile/android-components/issues/7960
1247 message = "onAttach()"
1251 override fun onDetach() {
1254 // Diagnostic breadcrumb for "Display already aquired" crash:
1255 // https://github.com/mozilla-mobile/android-components/issues/7960
1257 message = "onDetach()"
1262 private const val KEY_CUSTOM_TAB_SESSION_ID = "custom_tab_session_id"
1263 private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1
1264 private const val REQUEST_CODE_PROMPT_PERMISSIONS = 2
1265 private const val REQUEST_CODE_APP_PERMISSIONS = 3
1267 val onboardingLinksList: List<String> = listOf(
1268 SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.PRIVATE_NOTICE),
1269 SupportUtils.getFirefoxAccountSumoUrl()
1272 val intentSourcesList: List<SessionState.Source> = listOf(
1273 SessionState.Source.ACTION_SEARCH,
1274 SessionState.Source.ACTION_SEND,
1275 SessionState.Source.ACTION_VIEW
1279 override fun onAccessibilityStateChanged(enabled: Boolean) {
1280 if (_browserToolbarView != null) {
1281 browserToolbarView.setScrollFlags(enabled)