[fenix] For https://github.com/mozilla-mobile/fenix/issues/19876 - Part 1: Refactor...
[gecko.git] / mobile / android / fenix / app / src / main / java / org / mozilla / fenix / browser / BrowserFragment.kt
blob8e4d6c8a8957c09c1054e0aeafc616b1f063327c
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
45 /**
46  * Fragment used for browsing the web within the main app.
47  */
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
75                 )
76             )
77         }
79         val readerModeAction =
80             BrowserToolbar.ToggleButton(
81                 image = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_readermode)!!,
82                 imageSelected =
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),
86                 visible = {
87                     readerModeAvailable
88                 },
89                 selected = getCurrentTab()?.let {
90                         activity?.components?.core?.store?.state?.findTab(it.id)?.readerState?.active
91                     } ?: false,
92                 listener = browserToolbarInteractor::onReaderModePressed
93             )
95         browserToolbarView.view.addPageAction(readerModeAction)
97         thumbnailsFeature.set(
98             feature = BrowserThumbnails(context, view.engineView, components.core.store),
99             owner = this,
100             view = view
101         )
103         readerViewFeature.set(
104             feature = components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
105                 ReaderViewFeature(
106                     context,
107                     components.core.engine,
108                     components.core.store,
109                     view.readerViewControlsBar
110                 ) { available, active ->
111                     if (available) {
112                         components.analytics.metrics.track(Event.ReaderModeAvailable)
113                     }
115                     readerModeAvailable = available
116                     readerModeAction.setSelected(active)
117                     safeInvalidateBrowserToolbarView()
118                 }
119             },
120             owner = this,
121             view = view
122         )
124         windowFeature.set(
125             feature = WindowFeature(
126                 store = components.core.store,
127                 tabsUseCases = components.useCases.tabsUseCases
128             ),
129             owner = this,
130             view = view
131         )
133         if (context.settings().shouldShowOpenInAppCfr) {
134             openInAppOnboardingObserver.set(
135                 feature = OpenInAppOnboardingObserver(
136                     context = context,
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
144                 ),
145                 owner = this,
146                 view = view
147             )
148         }
149         if (context.settings().shouldShowTrackingProtectionCfr) {
150             trackingProtectionOverlayObserver.set(
151                 feature = TrackingProtectionOverlay(
152                     context = context,
153                     store = context.components.core.store,
154                     lifecycleOwner = viewLifecycleOwner,
155                     settings = context.settings(),
156                     metrics = context.components.analytics.metrics,
157                     getToolbar = { browserToolbarView.view }
158                 ),
159                 owner = this,
160                 view = view
161             )
162         }
163     }
165     override fun onStart() {
166         super.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(),
175                 settings = settings,
176                 webAppUseCases = context.components.useCases.webAppUseCases
177             ).also {
178                 it.start()
179             }
180         }
182         subscribeToTabCollections()
183     }
185     override fun onStop() {
186         super.onStop()
187         updateLastBrowseActivity()
188         pwaOnboardingObserver?.stop()
189     }
191     private fun subscribeToTabCollections() {
192         Observer<List<TabCollection>> {
193             requireComponents.core.tabCollectionStorage.cachedTabCollections = it
194         }.also { observer ->
195             requireComponents.core.tabCollectionStorage.getCollections()
196                 .observe(viewLifecycleOwner, observer)
197         }
198     }
200     override fun onResume() {
201         super.onResume()
202         requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this)
203     }
205     override fun onBackPressed(): Boolean {
206         return readerViewFeature.onBackPressed() || super.onBackPressed()
207     }
209     override fun navToQuickSettingsSheet(tab: SessionState, sitePermissions: SitePermissions?) {
210         val directions =
211             BrowserFragmentDirections.actionBrowserFragmentToQuickSettingsSheetDialogFragment(
212                 sessionId = tab.id,
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
220             )
221         nav(R.id.browserFragment, directions)
222     }
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
229             val directions =
230                 BrowserFragmentDirections.actionBrowserFragmentToTrackingProtectionPanelDialogFragment(
231                     sessionId = tab.id,
232                     url = tab.content.url,
233                     trackingProtectionEnabled = isEnabled,
234                     gravity = getAppropriateLayoutGravity()
235                 )
236             navController.navigateSafe(R.id.browserFragment, directions)
237         }
238     }
240     private val collectionStorageObserver = object : TabCollectionStorage.Observer {
241         override fun onCollectionCreated(title: String, sessions: List<TabSessionState>, id: Long?) {
242             showTabSavedToCollectionSnackbar(sessions.size, true)
243         }
245         override fun onTabsAdded(tabCollection: TabCollection, sessions: List<TabSessionState>) {
246             showTabSavedToCollectionSnackbar(sessions.size)
247         }
249         private fun showTabSavedToCollectionSnackbar(tabSize: Int, isNewCollection: Boolean = false) {
250             view?.let { view ->
251                 val messageStringRes = when {
252                     isNewCollection -> {
253                         R.string.create_collection_tabs_saved_new_collection
254                     }
255                     tabSize > 1 -> {
256                         R.string.create_collection_tabs_saved
257                     }
258                     else -> {
259                         R.string.create_collection_tab_saved
260                     }
261                 }
262                 FenixSnackbar.make(
263                     view = view.browserLayout,
264                     duration = Snackbar.LENGTH_SHORT,
265                     isDisplayedWithBrowserToolbar = true
266                 )
267                     .setText(view.context.getString(messageStringRes))
268                     .setAction(requireContext().getString(R.string.create_collection_view)) {
269                         findNavController().navigateBlockingForAsyncNavGraph(
270                             BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = false)
271                         )
272                     }
273                     .show()
274             }
275         }
276     }
278     override fun getContextMenuCandidates(
279         context: Context,
280         view: View
281     ): List<ContextMenuCandidate> {
282         val contextMenuCandidateAppLinksUseCases = AppLinksUseCases(
283             requireContext(),
284             { true }
285         )
287         return ContextMenuCandidate.defaultCandidates(
288             context,
289             context.components.useCases.tabsUseCases,
290             context.components.useCases.contextMenuUseCases,
291             view,
292             FenixSnackbarDelegate(view)
293         ) + ContextMenuCandidate.createOpenInExternalAppCandidate(requireContext(),
294             contextMenuCandidateAppLinksUseCases)
295     }
297     /**
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].
301      */
302     @VisibleForTesting
303     internal fun updateLastBrowseActivity() {
304         requireContext().settings().lastBrowseActivity = System.currentTimeMillis()
305     }