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.os.StrictMode
9 import android.view.View
10 import android.view.ViewGroup
11 import androidx.annotation.VisibleForTesting
12 import androidx.appcompat.content.res.AppCompatResources
13 import androidx.lifecycle.Observer
14 import androidx.navigation.fragment.findNavController
15 import com.google.android.material.snackbar.Snackbar
16 import kotlinx.android.synthetic.main.fragment_browser.*
17 import kotlinx.android.synthetic.main.fragment_browser.view.*
18 import kotlinx.coroutines.ExperimentalCoroutinesApi
19 import mozilla.components.browser.state.selector.findTab
20 import mozilla.components.browser.state.state.TabSessionState
21 import mozilla.components.browser.state.state.SessionState
22 import mozilla.components.browser.thumbnails.BrowserThumbnails
23 import mozilla.components.browser.toolbar.BrowserToolbar
24 import mozilla.components.feature.app.links.AppLinksUseCases
25 import mozilla.components.feature.contextmenu.ContextMenuCandidate
26 import mozilla.components.feature.readerview.ReaderViewFeature
27 import mozilla.components.feature.sitepermissions.SitePermissions
28 import mozilla.components.feature.tab.collections.TabCollection
29 import mozilla.components.feature.tabs.WindowFeature
30 import mozilla.components.support.base.feature.UserInteractionHandler
31 import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
32 import org.mozilla.fenix.R
33 import org.mozilla.fenix.components.FenixSnackbar
34 import org.mozilla.fenix.components.TabCollectionStorage
35 import org.mozilla.fenix.components.metrics.Event
36 import org.mozilla.fenix.ext.components
37 import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
38 import org.mozilla.fenix.ext.nav
39 import org.mozilla.fenix.ext.navigateSafe
40 import org.mozilla.fenix.ext.requireComponents
41 import org.mozilla.fenix.ext.settings
42 import org.mozilla.fenix.shortcut.PwaOnboardingObserver
43 import org.mozilla.fenix.trackingprotection.TrackingProtectionOverlay
46 * Fragment used for browsing the web within the main app.
48 @ExperimentalCoroutinesApi
49 @Suppress("TooManyFunctions", "LargeClass")
50 class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
52 private val windowFeature = ViewBoundFeatureWrapper<WindowFeature>()
53 private val openInAppOnboardingObserver = ViewBoundFeatureWrapper<OpenInAppOnboardingObserver>()
54 private val trackingProtectionOverlayObserver = ViewBoundFeatureWrapper<TrackingProtectionOverlay>()
56 private var readerModeAvailable = false
57 private var pwaOnboardingObserver: PwaOnboardingObserver? = null
59 @Suppress("LongMethod")
60 override fun initializeUI(view: View, tab: SessionState) {
61 super.initializeUI(view, tab)
63 val context = requireContext()
64 val components = context.components
66 if (context.settings().isSwipeToolbarToSwitchTabsEnabled) {
67 gestureLayout.addGestureListener(
68 ToolbarGestureHandler(
69 activity = requireActivity(),
70 contentLayout = browserLayout,
71 tabPreview = tabPreview,
72 toolbarLayout = browserToolbarView.view,
73 store = components.core.store,
74 selectTabUseCase = components.useCases.tabsUseCases.selectTab
79 val readerModeAction =
80 BrowserToolbar.ToggleButton(
81 image = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_readermode)!!,
83 AppCompatResources.getDrawable(requireContext(), R.drawable.ic_readermode_selected)!!,
84 contentDescription = requireContext().getString(R.string.browser_menu_read),
85 contentDescriptionSelected = requireContext().getString(R.string.browser_menu_read_close),
89 selected = getCurrentTab()?.let {
90 activity?.components?.core?.store?.state?.findTab(it.id)?.readerState?.active
92 listener = browserToolbarInteractor::onReaderModePressed
95 browserToolbarView.view.addPageAction(readerModeAction)
97 thumbnailsFeature.set(
98 feature = BrowserThumbnails(context, view.engineView, components.core.store),
103 readerViewFeature.set(
104 feature = components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
107 components.core.engine,
108 components.core.store,
109 view.readerViewControlsBar
110 ) { available, active ->
112 components.analytics.metrics.track(Event.ReaderModeAvailable)
115 readerModeAvailable = available
116 readerModeAction.setSelected(active)
117 safeInvalidateBrowserToolbarView()
125 feature = WindowFeature(
126 store = components.core.store,
127 tabsUseCases = components.useCases.tabsUseCases
133 if (context.settings().shouldShowOpenInAppCfr) {
134 openInAppOnboardingObserver.set(
135 feature = OpenInAppOnboardingObserver(
137 store = context.components.core.store,
138 lifecycleOwner = this,
139 navController = findNavController(),
140 settings = context.settings(),
141 appLinksUseCases = context.components.useCases.appLinksUseCases,
142 container = browserLayout as ViewGroup,
143 shouldScrollWithTopToolbar = !context.settings().shouldUseBottomToolbar
149 if (context.settings().shouldShowTrackingProtectionCfr) {
150 trackingProtectionOverlayObserver.set(
151 feature = TrackingProtectionOverlay(
153 store = context.components.core.store,
154 lifecycleOwner = viewLifecycleOwner,
155 settings = context.settings(),
156 metrics = context.components.analytics.metrics,
157 getToolbar = { browserToolbarView.view }
165 override fun onStart() {
167 val context = requireContext()
168 val settings = context.settings()
170 if (!settings.userKnowsAboutPwas) {
171 pwaOnboardingObserver = PwaOnboardingObserver(
172 store = context.components.core.store,
173 lifecycleOwner = this,
174 navController = findNavController(),
176 webAppUseCases = context.components.useCases.webAppUseCases
182 subscribeToTabCollections()
185 override fun onStop() {
187 updateLastBrowseActivity()
188 pwaOnboardingObserver?.stop()
191 private fun subscribeToTabCollections() {
192 Observer<List<TabCollection>> {
193 requireComponents.core.tabCollectionStorage.cachedTabCollections = it
195 requireComponents.core.tabCollectionStorage.getCollections()
196 .observe(viewLifecycleOwner, observer)
200 override fun onResume() {
202 requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this)
205 override fun onBackPressed(): Boolean {
206 return readerViewFeature.onBackPressed() || super.onBackPressed()
209 override fun navToQuickSettingsSheet(tab: SessionState, sitePermissions: SitePermissions?) {
211 BrowserFragmentDirections.actionBrowserFragmentToQuickSettingsSheetDialogFragment(
213 url = tab.content.url,
214 title = tab.content.title,
215 isSecured = tab.content.securityInfo.secure,
216 sitePermissions = sitePermissions,
217 gravity = getAppropriateLayoutGravity(),
218 certificateName = tab.content.securityInfo.issuer,
219 permissionHighlights = tab.content.permissionHighlights
221 nav(R.id.browserFragment, directions)
224 override fun navToTrackingProtectionPanel(tab: SessionState) {
225 val navController = findNavController()
227 requireComponents.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->
228 val isEnabled = tab.trackingProtection.enabled && !contains
230 BrowserFragmentDirections.actionBrowserFragmentToTrackingProtectionPanelDialogFragment(
232 url = tab.content.url,
233 trackingProtectionEnabled = isEnabled,
234 gravity = getAppropriateLayoutGravity()
236 navController.navigateSafe(R.id.browserFragment, directions)
240 private val collectionStorageObserver = object : TabCollectionStorage.Observer {
241 override fun onCollectionCreated(title: String, sessions: List<TabSessionState>, id: Long?) {
242 showTabSavedToCollectionSnackbar(sessions.size, true)
245 override fun onTabsAdded(tabCollection: TabCollection, sessions: List<TabSessionState>) {
246 showTabSavedToCollectionSnackbar(sessions.size)
249 private fun showTabSavedToCollectionSnackbar(tabSize: Int, isNewCollection: Boolean = false) {
251 val messageStringRes = when {
253 R.string.create_collection_tabs_saved_new_collection
256 R.string.create_collection_tabs_saved
259 R.string.create_collection_tab_saved
263 view = view.browserLayout,
264 duration = Snackbar.LENGTH_SHORT,
265 isDisplayedWithBrowserToolbar = true
267 .setText(view.context.getString(messageStringRes))
268 .setAction(requireContext().getString(R.string.create_collection_view)) {
269 findNavController().navigateBlockingForAsyncNavGraph(
270 BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = false)
278 override fun getContextMenuCandidates(
281 ): List<ContextMenuCandidate> {
282 val contextMenuCandidateAppLinksUseCases = AppLinksUseCases(
287 return ContextMenuCandidate.defaultCandidates(
289 context.components.useCases.tabsUseCases,
290 context.components.useCases.contextMenuUseCases,
292 FenixSnackbarDelegate(view)
293 ) + ContextMenuCandidate.createOpenInExternalAppCandidate(requireContext(),
294 contextMenuCandidateAppLinksUseCases)
298 * Updates the last time the user was active on the [BrowserFragment].
299 * This is useful to determine if the user has to start on the [HomeFragment]
300 * or it should go directly to the [BrowserFragment].
303 internal fun updateLastBrowseActivity() {
304 requireContext().settings().lastBrowseActivity = System.currentTimeMillis()