BaseBrowserFragment.kt 29.1 KB
Newer Older
1
2
3
4
5
6
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.fenix.browser

7
import android.content.Context
8
import android.content.Intent
9
10
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
11
import android.os.Bundle
12
import android.view.Gravity
13
14
15
16
17
18
19
20
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.CallSuper
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.net.toUri
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
21
import androidx.navigation.NavDirections
22
23
24
25
26
27
import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_browser.*
import kotlinx.android.synthetic.main.fragment_browser.view.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
28
29
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
30
31
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
Severin Rudie's avatar
Severin Rudie committed
32
import mozilla.appservices.places.BookmarkRoot
33
import mozilla.components.browser.session.Session
34
import mozilla.components.browser.session.SessionManager
35
import mozilla.components.concept.engine.prompt.ShareData
Grisha Kruglov's avatar
Grisha Kruglov committed
36
import mozilla.components.feature.accounts.FxaCapability
Grisha Kruglov's avatar
Grisha Kruglov committed
37
import mozilla.components.feature.accounts.FxaWebChannelFeature
38
import mozilla.components.feature.app.links.AppLinksFeature
39
import mozilla.components.feature.contextmenu.ContextMenuCandidate
40
import mozilla.components.feature.contextmenu.ContextMenuFeature
41
import mozilla.components.feature.downloads.AbstractFetchDownloadService
42
43
import mozilla.components.feature.downloads.DownloadsFeature
import mozilla.components.feature.downloads.manager.FetchDownloadManager
44
import mozilla.components.feature.intent.ext.EXTRA_SESSION_ID
45
import mozilla.components.feature.prompts.PromptFeature
46
import mozilla.components.feature.prompts.share.ShareDelegate
Severin Rudie's avatar
Severin Rudie committed
47
import mozilla.components.feature.readerview.ReaderViewFeature
48
49
50
51
52
53
54
55
import mozilla.components.feature.session.FullScreenFeature
import mozilla.components.feature.session.SessionFeature
import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.session.SwipeRefreshFeature
import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.feature.sitepermissions.SitePermissionsFeature
import mozilla.components.feature.sitepermissions.SitePermissionsRules
import mozilla.components.support.base.feature.PermissionsFeature
56
import mozilla.components.support.base.feature.UserInteractionHandler
57
58
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.view.exitImmersiveModeIfNeeded
Grisha Kruglov's avatar
Grisha Kruglov committed
59
import org.mozilla.fenix.Experiments
60
61
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.HomeActivity
62
import org.mozilla.fenix.IntentReceiverActivity
63
import org.mozilla.fenix.NavGraphDirections
64
import org.mozilla.fenix.R
Severin Rudie's avatar
Severin Rudie committed
65
import org.mozilla.fenix.browser.readermode.DefaultReaderModeController
66
67
68
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.FindInPageIntegration
import org.mozilla.fenix.components.StoreProvider
69
import org.mozilla.fenix.components.metrics.Event
70
71
import org.mozilla.fenix.components.toolbar.BrowserFragmentState
import org.mozilla.fenix.components.toolbar.BrowserFragmentStore
Severin Rudie's avatar
Severin Rudie committed
72
import org.mozilla.fenix.components.toolbar.BrowserInteractor
73
import org.mozilla.fenix.components.toolbar.BrowserToolbarView
Tiger Oakes's avatar
Tiger Oakes committed
74
import org.mozilla.fenix.components.toolbar.BrowserToolbarViewInteractor
75
76
import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarController
import org.mozilla.fenix.components.toolbar.ToolbarIntegration
77
import org.mozilla.fenix.downloads.DownloadNotificationBottomSheetDialog
78
79
80
import org.mozilla.fenix.downloads.DownloadService
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.enterToImmersiveMode
81
import org.mozilla.fenix.ext.getRootView
Tiger Oakes's avatar
Tiger Oakes committed
82
import org.mozilla.fenix.ext.hideToolbar
83
import org.mozilla.fenix.ext.metrics
84
import org.mozilla.fenix.ext.nav
85
import org.mozilla.fenix.ext.requireComponents
86
import org.mozilla.fenix.ext.sessionsOfType
87
import org.mozilla.fenix.ext.settings
Grisha Kruglov's avatar
Grisha Kruglov committed
88
import org.mozilla.fenix.isInExperiment
89
import org.mozilla.fenix.settings.SupportUtils
90
import org.mozilla.fenix.theme.ThemeManager
91
92
93
94
95
96

/**
 * Base fragment extended by [BrowserFragment].
 * This class only contains shared code focused on the main browsing content.
 * UI code specific to the app or to custom tabs can be found in the subclasses.
 */
97
@Suppress("TooManyFunctions", "LargeClass")
98
abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, SessionManager.Observer {
Severin Rudie's avatar
Severin Rudie committed
99
    protected lateinit var browserFragmentStore: BrowserFragmentStore
Tiger Oakes's avatar
Tiger Oakes committed
100
    protected lateinit var browserInteractor: BrowserToolbarViewInteractor
101
102
    protected lateinit var browserToolbarView: BrowserToolbarView

Severin Rudie's avatar
Severin Rudie committed
103
104
    protected val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewFeature>()

105
106
107
108
109
110
111
112
113
114
    private val sessionFeature = ViewBoundFeatureWrapper<SessionFeature>()
    private val contextMenuFeature = ViewBoundFeatureWrapper<ContextMenuFeature>()
    private val downloadsFeature = ViewBoundFeatureWrapper<DownloadsFeature>()
    private val appLinksFeature = ViewBoundFeatureWrapper<AppLinksFeature>()
    private val promptsFeature = ViewBoundFeatureWrapper<PromptFeature>()
    private val findInPageIntegration = ViewBoundFeatureWrapper<FindInPageIntegration>()
    private val toolbarIntegration = ViewBoundFeatureWrapper<ToolbarIntegration>()
    private val sitePermissionsFeature = ViewBoundFeatureWrapper<SitePermissionsFeature>()
    private val fullScreenFeature = ViewBoundFeatureWrapper<FullScreenFeature>()
    private val swipeRefreshFeature = ViewBoundFeatureWrapper<SwipeRefreshFeature>()
Grisha Kruglov's avatar
Grisha Kruglov committed
115
    private val webchannelIntegration = ViewBoundFeatureWrapper<FxaWebChannelFeature>()
116
117
118

    var customTabSessionId: String? = null

119
120
121
    private var browserInitialized: Boolean = false
    private var initUIJob: Job? = null

122
123
124
125
126
127
128
129
130
131
132
133
    @CallSuper
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        require(arguments != null)
        customTabSessionId = arguments?.getString(EXTRA_SESSION_ID)

        val view = inflater.inflate(R.layout.fragment_browser, container, false)

        val activity = activity as HomeActivity
134
        activity.themeManager.applyStatusBarTheme(activity)
135

Severin Rudie's avatar
Severin Rudie committed
136
        browserFragmentStore = StoreProvider.get(this) {
137
            BrowserFragmentStore(
Severin Rudie's avatar
Severin Rudie committed
138
                BrowserFragmentState()
139
140
141
142
143
144
            )
        }

        return view
    }

Tiger Oakes's avatar
Tiger Oakes committed
145
    final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
146
147
        browserInitialized = initializeUI(view) != null
    }
148

149
    @Suppress("ComplexMethod", "LongMethod")
150
151
    @CallSuper
    protected open fun initializeUI(view: View): Session? {
152
153
        val context = requireContext()
        val sessionManager = context.components.core.sessionManager
154
        val store = context.components.core.store
155

156
        return getSessionById()?.also { session ->
157

158
159
160
161
            // We need to show the snackbar while the browsing data is deleting(if "Delete
            // browsing data on quit" is activated). After the deletion is over, the snackbar
            // is dismissed.
            val snackbar: FenixSnackbar? = requireActivity().getRootView()?.let { v ->
162
                FenixSnackbar.makeWithToolbarPadding(v)
163
164
165
                    .setText(v.context.getString(R.string.deleting_browsing_data_in_progress))
            }

166
            val browserToolbarController = DefaultBrowserToolbarController(
Severin Rudie's avatar
Severin Rudie committed
167
168
169
170
171
172
173
                store = browserFragmentStore,
                activity = requireActivity(),
                snackbar = snackbar,
                navController = findNavController(),
                readerModeController = DefaultReaderModeController(readerViewFeature),
                browsingModeManager = (activity as HomeActivity).browsingModeManager,
                sessionManager = requireComponents.core.sessionManager,
174
                findInPageLauncher = { findInPageIntegration.withFeature { it.launch() } },
175
                browserLayout = view.browserLayout,
176
                engineView = engineView,
177
178
                swipeRefresh = swipeRefresh,
                adjustBackgroundAndNavigate = ::adjustBackgroundAndNavigate,
179
                customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) },
180
181
                getSupportUrl = {
                    SupportUtils.getSumoURLForTopic(
182
                        context,
183
184
185
                        SupportUtils.SumoTopic.HELP
                    )
                },
186
187
188
                openInFenixIntent = Intent(context, IntentReceiverActivity::class.java).apply {
                    action = Intent.ACTION_VIEW
                    flags = Intent.FLAG_ACTIVITY_NEW_TASK
189
                },
Severin Rudie's avatar
Severin Rudie committed
190
                bookmarkTapped = { lifecycleScope.launch { bookmarkTapped(it) } },
191
192
                scope = lifecycleScope,
                tabCollectionStorage = requireComponents.core.tabCollectionStorage
193
194
            )

Severin Rudie's avatar
Severin Rudie committed
195
196
197
            browserInteractor = BrowserInteractor(
                browserToolbarController = browserToolbarController
            )
198
199
200

            browserToolbarView = BrowserToolbarView(
                container = view.browserLayout,
201
                shouldUseBottomToolbar = context.settings().shouldUseBottomToolbar,
202
                interactor = browserInteractor,
203
                customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }
204
205
206
207
208
209
210
211
212
213
            )

            toolbarIntegration.set(
                feature = browserToolbarView.toolbarIntegration,
                owner = this,
                view = view
            )

            findInPageIntegration.set(
                feature = FindInPageIntegration(
214
                    store = store,
215
                    sessionId = customTabSessionId,
216
                    stub = view.stubFindInPage,
217
                    engineView = view.engineView,
218
                    toolbar = browserToolbarView.view
219
220
221
222
223
                ),
                owner = this,
                view = view
            )

224
            browserToolbarView.view.display.setOnSiteSecurityClickedListener {
225
226
227
                showQuickSettingsDialog()
            }

228
            browserToolbarView.view.display.setOnTrackingProtectionClickedListener {
229
                context.metrics.track(Event.TrackingProtectionIconPressed)
230
231
232
                showTrackingProtectionPanel()
            }

233
234
            contextMenuFeature.set(
                feature = ContextMenuFeature(
235
                    fragmentManager = parentFragmentManager,
236
                    store = store,
237
                    candidates = getContextMenuCandidates(context, view),
238
                    engineView = view.engineView,
239
240
                    useCases = context.components.useCases.contextMenuUseCases,
                    customTabId = customTabSessionId
241
                ),
242
243
244
245
                owner = this,
                view = view
            )

246
247
248
249
250
251
252
            val downloadFeature = DownloadsFeature(
                context.applicationContext,
                store = store,
                useCases = context.components.useCases.downloadUseCases,
                fragmentManager = childFragmentManager,
                customTabId = customTabSessionId,
                downloadManager = FetchDownloadManager(
253
                    context.applicationContext,
254
255
256
257
258
                    DownloadService::class
                ),
                promptsStyling = DownloadsFeature.PromptsStyling(
                    gravity = Gravity.BOTTOM,
                    shouldWidthMatchParent = true,
259
260
261
262
263
264
265
266
                    positiveButtonBackgroundColor = ThemeManager.resolveAttribute(
                        R.attr.accent,
                        context
                    ),
                    positiveButtonTextColor = ThemeManager.resolveAttribute(
                        R.attr.contrastText,
                        context
                    ),
267
268
269
270
271
272
273
                    positiveButtonRadius = (resources.getDimensionPixelSize(R.dimen.tab_corner_radius)).toFloat()
                ),
                onNeedToRequestPermissions = { permissions ->
                    requestPermissions(permissions, REQUEST_CODE_DOWNLOAD_PERMISSIONS)
                }
            )

274
            downloadFeature.onDownloadStopped = { download, _, downloadJobStatus ->
275
276
                // If the download is just paused, don't show any in-app notification
                if (downloadJobStatus == AbstractFetchDownloadService.DownloadJobStatus.COMPLETED ||
277
278
                    downloadJobStatus == AbstractFetchDownloadService.DownloadJobStatus.FAILED
                ) {
279
280
281
282
                    val dialog = DownloadNotificationBottomSheetDialog(
                        context = context,
                        didFail = downloadJobStatus == AbstractFetchDownloadService.DownloadJobStatus.FAILED,
                        download = download,
283
284
                        tryAgain = downloadFeature::tryAgain,
                        onCannotOpenFile = {
285
286
287
                            FenixSnackbar.makeWithToolbarPadding(view, Snackbar.LENGTH_SHORT)
                                .setText(context.getString(R.string.mozac_feature_downloads_could_not_open_file))
                                .show()
288
                        }
289
290
291
                    )
                    dialog.show()
                }
292
293
294
295
            }

            downloadsFeature.set(
                downloadFeature,
296
297
298
299
300
301
                owner = this,
                view = view
            )

            appLinksFeature.set(
                feature = AppLinksFeature(
302
                    context,
303
304
                    sessionManager = sessionManager,
                    sessionId = customTabSessionId,
305
                    fragmentManager = parentFragmentManager,
306
                    launchInApp = { context.settings().openLinksInExternalApp }
307
                ),
308
309
310
311
312
313
314
                owner = this,
                view = view
            )

            promptsFeature.set(
                feature = PromptFeature(
                    fragment = this,
315
                    store = store,
316
                    customTabId = customTabSessionId,
317
                    fragmentManager = parentFragmentManager,
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
                    shareDelegate = object : ShareDelegate {
                        override fun showShareSheet(
                            context: Context,
                            shareData: ShareData,
                            onDismiss: () -> Unit,
                            onSuccess: () -> Unit
                        ) {
                            val directions = NavGraphDirections.actionGlobalShareFragment(
                                data = arrayOf(shareData),
                                showPage = true,
                                sessionId = getSessionById()?.id
                            )
                            findNavController().navigate(directions)
                        }
                    },
333
334
335
336
337
338
339
340
341
342
343
344
                    onNeedToRequestPermissions = { permissions ->
                        requestPermissions(permissions, REQUEST_CODE_PROMPT_PERMISSIONS)
                    }),
                owner = this,
                view = view
            )

            sessionFeature.set(
                feature = SessionFeature(
                    sessionManager,
                    SessionUseCases(sessionManager),
                    view.engineView,
345
346
347
348
349
                    customTabSessionId
                ),
                owner = this,
                view = view
            )
350

351
            val accentHighContrastColor =
352
                ThemeManager.resolveAttribute(R.attr.accentHighContrast, context)
353
354
355

            sitePermissionsFeature.set(
                feature = SitePermissionsFeature(
356
                    context = context,
357
                    sessionManager = sessionManager,
358
                    fragmentManager = parentFragmentManager,
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
                    promptsStyling = SitePermissionsFeature.PromptsStyling(
                        gravity = getAppropriateLayoutGravity(),
                        shouldWidthMatchParent = true,
                        positiveButtonBackgroundColor = accentHighContrastColor,
                        positiveButtonTextColor = R.color.photonWhite
                    ),
                    sessionId = customTabSessionId
                ) { permissions ->
                    requestPermissions(permissions, REQUEST_CODE_APP_PERMISSIONS)
                },
                owner = this,
                view = view
            )

            fullScreenFeature.set(
                feature = FullScreenFeature(
                    sessionManager,
                    SessionUseCases(sessionManager),
                    customTabSessionId
                ) { inFullScreen ->
                    if (inFullScreen) {
                        FenixSnackbar.make(view.rootView, Snackbar.LENGTH_SHORT)
                            .setText(getString(R.string.full_screen_notification))
                            .show()
                        activity?.enterToImmersiveMode()
384
                        browserToolbarView.view.visibility = View.GONE
385
386
                    } else {
                        activity?.exitImmersiveModeIfNeeded()
387
388
                        (activity as? HomeActivity)?.let { activity ->
                            activity.themeManager.applyStatusBarTheme(activity)
389
                        }
390
                        browserToolbarView.view.visibility = View.VISIBLE
391
                    }
392
                    updateLayoutMargins(inFullScreen)
393
394
395
396
397
                },
                owner = this,
                view = view
            )

398
399
400
401
402
403
            session.register(observer = object : Session.Observer {
                override fun onLoadRequest(
                    session: Session,
                    url: String,
                    triggeredByRedirect: Boolean,
                    triggeredByWebContent: Boolean
404
                ) {
405
406
407
408
                    browserToolbarView.expand()
                }
            }, owner = viewLifecycleOwner)

409
410
411
412
413
414
            sessionManager.register(observer = object : SessionManager.Observer {
                override fun onSessionSelected(session: Session) {
                    browserToolbarView.expand()
                }
            }, owner = viewLifecycleOwner)

415
416
            @Suppress("ConstantConditionIf")
            if (FeatureFlags.pullToRefreshEnabled) {
417
                val primaryTextColor =
418
                    ThemeManager.resolveAttribute(R.attr.primaryText, context)
419
420
421
                view.swipeRefresh.setColorSchemeColors(primaryTextColor)
                swipeRefreshFeature.set(
                    feature = SwipeRefreshFeature(
422
423
                        sessionManager,
                        context.components.useCases.sessionUseCases.reload,
424
425
426
427
428
429
430
431
432
433
434
                        view.swipeRefresh,
                        customTabSessionId
                    ),
                    owner = this,
                    view = view
                )
            } else {
                // Disable pull to refresh
                view.swipeRefresh.setOnChildScrollUpCallback { _, _ -> true }
            }

Grisha Kruglov's avatar
Grisha Kruglov committed
435
436
437
438
439
440
441
            if (!requireContext().isInExperiment(Experiments.asFeatureWebChannelsDisabled)) {
                webchannelIntegration.set(
                    feature = FxaWebChannelFeature(
                        requireContext(),
                        customTabSessionId,
                        requireComponents.core.engine,
                        requireComponents.core.sessionManager,
Grisha Kruglov's avatar
Grisha Kruglov committed
442
443
                        requireComponents.backgroundServices.accountManager,
                        setOf(FxaCapability.CHOOSE_WHAT_TO_SYNC)
Grisha Kruglov's avatar
Grisha Kruglov committed
444
445
446
447
448
449
                    ),
                    owner = this,
                    view = view
                )
            }

450
            (activity as HomeActivity).updateThemeForSession(session)
451
452
453
        }
    }

454
455
456
457
458
459
460
461
    /**
     * Returns a list of context menu items [ContextMenuCandidate] for the context menu
     */
    protected abstract fun getContextMenuCandidates(
        context: Context,
        view: View
    ): List<ContextMenuCandidate>

462
463
    private fun adjustBackgroundAndNavigate(directions: NavDirections) {
        context?.let {
464
465
466
            swipeRefresh?.background = ColorDrawable(Color.TRANSPARENT)
            engineView?.asView()?.visibility = View.GONE
            findNavController().nav(R.id.browserFragment, directions)
467
468
469
        }
    }

470
471
    @CallSuper
    override fun onSessionSelected(session: Session) {
472
        (activity as HomeActivity).updateThemeForSession(session)
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
        if (!browserInitialized) {
            // Initializing a new coroutineScope to avoid ConcurrentModificationException in ObserverRegistry
            // This will be removed when ObserverRegistry is deprecated by browser-state.
            initUIJob = MainScope().launch {
                view?.let {
                    browserInitialized = initializeUI(it) != null
                }
            }
        }
    }

    @CallSuper
    override fun onStart() {
        super.onStart()
        requireComponents.core.sessionManager.register(this, this, autoPause = true)
    }

490
491
492
493
494
495
496
497
498
499
    @CallSuper
    override fun onResume() {
        super.onResume()
        val components = requireComponents

        val preferredColorScheme = components.core.getPreferredColorScheme()
        if (components.core.engine.settings.preferredColorScheme != preferredColorScheme) {
            components.core.engine.settings.preferredColorScheme = preferredColorScheme
            components.useCases.sessionUseCases.reload()
        }
Tiger Oakes's avatar
Tiger Oakes committed
500
        hideToolbar()
501
502
503
504

        assignSitePermissionsRules()
    }

505
    @CallSuper
506
507
508
509
510
    final override fun onPause() {
        super.onPause()
        fullScreenFeature.onBackPressed()
    }

511
512
513
514
515
516
    @CallSuper
    override fun onStop() {
        super.onStop()
        initUIJob?.cancel()
    }

517
518
519
    @CallSuper
    override fun onBackPressed(): Boolean {
        return findInPageIntegration.onBackPressed() ||
520
521
522
            fullScreenFeature.onBackPressed() ||
            sessionFeature.onBackPressed() ||
            removeSessionIfNeeded()
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
    }

    /**
     * Saves the external app session ID to be restored later in [onViewStateRestored].
     */
    final override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putString(KEY_CUSTOM_TAB_SESSION_ID, customTabSessionId)
    }

    /**
     * Retrieves the external app session ID saved by [onSaveInstanceState].
     */
    final override fun onViewStateRestored(savedInstanceState: Bundle?) {
        super.onViewStateRestored(savedInstanceState)
        savedInstanceState?.getString(KEY_CUSTOM_TAB_SESSION_ID)?.let {
            if (requireComponents.core.sessionManager.findSessionById(it)?.customTabConfig != null) {
                customTabSessionId = it
            }
        }
    }

    /**
     * Forwards permission grant results to one of the features.
     */
    final override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults: IntArray
    ) {
        val feature: PermissionsFeature? = when (requestCode) {
            REQUEST_CODE_DOWNLOAD_PERMISSIONS -> downloadsFeature.get()
            REQUEST_CODE_PROMPT_PERMISSIONS -> promptsFeature.get()
            REQUEST_CODE_APP_PERMISSIONS -> sitePermissionsFeature.get()
            else -> null
        }
        feature?.onPermissionsResult(permissions, grantResults)
    }

    /**
     * Forwards activity results to the prompt feature.
     */
    final override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        promptsFeature.withFeature { it.onActivityResult(requestCode, resultCode, data) }
    }

    /**
570
571
     * Removes the session if it was opened by an ACTION_VIEW intent
     * or if it has no more history
572
573
574
     */
    protected open fun removeSessionIfNeeded(): Boolean {
        getSessionById()?.let { session ->
575
            val sessionManager = requireComponents.core.sessionManager
576
            if (session.source == Session.Source.ACTION_VIEW) {
577
578
                sessionManager.remove(session)
            } else {
579
580
                val isLastSession =
                    sessionManager.sessionsOfType(private = session.private).count() == 1
581
582
583
                sessionManager.remove(session, session.hasParentSession)
                val goToOverview = isLastSession || !session.hasParentSession
                return !goToOverview
584
            }
585
586
587
588
        }
        return false
    }

589
590
591
592
    protected abstract fun navToQuickSettingsSheet(
        session: Session,
        sitePermissions: SitePermissions?
    )
593

594
595
    protected abstract fun navToTrackingProtectionPanel(session: Session)

596
597
598
    /**
     * Returns the top and bottom margins.
     */
599
600
601
602
603
604
605
    private fun getEngineMargins(): Pair<Int, Int> =
        if (context?.settings()?.shouldUseBottomToolbar == true) {
            val toolbarSize = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height)
            0 to toolbarSize
        } else {
            0 to 0
        }
606
607

    /**
608
     * Returns the layout [android.view.Gravity] for the quick settings and ETP dialog.
609
     */
610
611
    protected fun getAppropriateLayoutGravity(): Int =
        if (context?.settings()?.shouldUseBottomToolbar == true) Gravity.BOTTOM else Gravity.TOP
612

613
614
615
    protected fun updateLayoutMargins(inFullScreen: Boolean) {
        view?.swipeRefresh?.apply {
            val (topMargin, bottomMargin) = if (inFullScreen) 0 to 0 else getEngineMargins()
616
617
618
619
620
621
            (layoutParams as CoordinatorLayout.LayoutParams).setMargins(
                0,
                topMargin,
                0,
                bottomMargin
            )
622
623
624
        }
    }

625
626
627
628
    /**
     * Updates the site permissions rules based on user settings.
     */
    private fun assignSitePermissionsRules() {
629
        val settings = requireContext().settings()
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652

        val rules: SitePermissionsRules = settings.getSitePermissionsCustomSettingsRules()

        sitePermissionsFeature.withFeature {
            it.sitePermissionsRules = rules
        }
    }

    /**
     * Displays the quick settings dialog,
     * which lets the user control tracking protection and site settings.
     */
    private fun showQuickSettingsDialog() {
        val session = getSessionById() ?: return
        lifecycleScope.launch(Main) {
            val sitePermissions: SitePermissions? = withContext(IO) {
                session.url.toUri().host?.let { host ->
                    val storage = requireContext().components.core.permissionStorage
                    storage.findSitePermissionsBy(host)
                }
            }

            view?.let {
Tiger Oakes's avatar
Tiger Oakes committed
653
                navToQuickSettingsSheet(session, sitePermissions)
654
655
656
657
            }
        }
    }

658
659
660
661
662
663
664
    private fun showTrackingProtectionPanel() {
        val session = getSessionById() ?: return
        view?.let {
            navToTrackingProtectionPanel(session)
        }
    }

665
666
667
668
669
    /**
     * Returns the current session.
     */
    protected fun getSessionById(): Session? {
        val sessionManager = context?.components?.core?.sessionManager ?: return null
670
671
672
        val localCustomTabId = customTabSessionId
        return if (localCustomTabId != null) {
            sessionManager.findSessionById(localCustomTabId)
673
674
675
676
677
        } else {
            sessionManager.selectedSession
        }
    }

Severin Rudie's avatar
Severin Rudie committed
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
    private suspend fun bookmarkTapped(session: Session) = withContext(IO) {
        val bookmarksStorage = requireComponents.core.bookmarksStorage
        val existing =
            bookmarksStorage.getBookmarksWithUrl(session.url).firstOrNull { it.url == session.url }
        if (existing != null) {
            // Bookmark exists, go to edit fragment
            withContext(Main) {
                nav(
                    R.id.browserFragment,
                    BrowserFragmentDirections.actionBrowserFragmentToBookmarkEditFragment(existing.guid)
                )
            }
        } else {
            // Save bookmark, then go to edit fragment
            val guid = bookmarksStorage.addItem(
                BookmarkRoot.Mobile.id,
                url = session.url,
                title = session.title,
                position = null
            )

            withContext(Main) {
                requireComponents.analytics.metrics.track(Event.AddBookmark)

                view?.let { view ->
703
704
705
706
707
708
709
710
711
712
                    FenixSnackbar.makeWithToolbarPadding(view)
                        .setText(getString(R.string.bookmark_saved_snackbar))
                        .setAction(getString(R.string.edit_bookmark_snackbar_action)) {
                            nav(
                                R.id.browserFragment,
                                BrowserFragmentDirections.actionBrowserFragmentToBookmarkEditFragment(
                                    guid
                                ))
                        }
                        .show()
Severin Rudie's avatar
Severin Rudie committed
713
714
715
716
717
                }
            }
        }
    }

718
719
720
721
722
723
724
    companion object {
        private const val KEY_CUSTOM_TAB_SESSION_ID = "custom_tab_session_id"
        private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1
        private const val REQUEST_CODE_PROMPT_PERMISSIONS = 2
        private const val REQUEST_CODE_APP_PERMISSIONS = 3
    }
}