GeckoEngineSession.kt 33.7 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 mozilla.components.browser.engine.gecko

7
import android.annotation.SuppressLint
8
9
10
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
11
12
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
13
import kotlinx.coroutines.runBlocking
14
import mozilla.components.browser.engine.gecko.media.GeckoMediaDelegate
15
import mozilla.components.browser.engine.gecko.permission.GeckoPermissionRequest
16
import mozilla.components.browser.engine.gecko.prompt.GeckoPromptDelegate
17
import mozilla.components.browser.engine.gecko.window.GeckoWindowRequest
18
import mozilla.components.browser.errorpages.ErrorType
19
import mozilla.components.concept.engine.EngineSession
20
import mozilla.components.concept.engine.EngineSessionState
21
import mozilla.components.concept.engine.HitResult
22
import mozilla.components.concept.engine.Settings
23
import mozilla.components.concept.engine.content.blocking.Tracker
Grisha Kruglov's avatar
Grisha Kruglov committed
24
import mozilla.components.concept.engine.history.HistoryTrackingDelegate
25
import mozilla.components.concept.engine.manifest.WebAppManifestParser
26
import mozilla.components.concept.engine.request.RequestInterceptor
27
import mozilla.components.concept.engine.request.RequestInterceptor.InterceptionResponse
28
import mozilla.components.concept.engine.window.WindowRequest
29
30
import mozilla.components.concept.storage.PageVisit
import mozilla.components.concept.storage.RedirectSource
31
import mozilla.components.concept.storage.VisitType
32
import mozilla.components.support.ktx.android.util.Base64
33
34
import mozilla.components.support.ktx.kotlin.isEmail
import mozilla.components.support.ktx.kotlin.isGeoLocation
35
import mozilla.components.support.ktx.kotlin.isPhone
36
import org.json.JSONObject
37
import org.mozilla.geckoview.AllowOrDeny
38
import org.mozilla.geckoview.ContentBlocking
39
import org.mozilla.geckoview.GeckoResult
40
41
import org.mozilla.geckoview.GeckoRuntime
import org.mozilla.geckoview.GeckoSession
Renan Barros's avatar
Renan Barros committed
42
import org.mozilla.geckoview.GeckoSession.NavigationDelegate
43
import org.mozilla.geckoview.GeckoSessionSettings
44
import org.mozilla.geckoview.WebRequestError
45
import kotlin.coroutines.CoroutineContext
46
47
48
49

/**
 * Gecko-based EngineSession implementation.
 */
50
@Suppress("TooManyFunctions", "LargeClass")
51
class GeckoEngineSession(
52
    private val runtime: GeckoRuntime,
Grisha Kruglov's avatar
Grisha Kruglov committed
53
    private val privateMode: Boolean = false,
54
    private val defaultSettings: Settings? = null,
55
56
57
58
59
60
    private val geckoSessionProvider: () -> GeckoSession = {
        val settings = GeckoSessionSettings.Builder()
            .usePrivateMode(privateMode)
            .build()
        GeckoSession(settings)
    },
61
62
    private val context: CoroutineContext = Dispatchers.IO,
    openGeckoSession: Boolean = true
63
) : CoroutineScope, EngineSession() {
64

65
    internal lateinit var geckoSession: GeckoSession
Grisha Kruglov's avatar
Grisha Kruglov committed
66
    internal var currentUrl: String? = null
67
    internal var scrollY: Int = 0
68
    internal var job: Job = Job()
69
70
    private var lastSessionState: GeckoSession.SessionState? = null
    private var stateBeforeCrash: GeckoSession.SessionState? = null
71
72
73
74

    /**
     * See [EngineSession.settings]
     */
75
    override val settings: Settings = object : Settings() {
76
        override var requestInterceptor: RequestInterceptor? = null
Grisha Kruglov's avatar
Grisha Kruglov committed
77
        override var historyTrackingDelegate: HistoryTrackingDelegate? = null
78
        override var userAgentString: String?
79
80
            get() = geckoSession.settings.userAgentOverride
            set(value) { geckoSession.settings.userAgentOverride = value }
81
82
83
        override var suspendMediaWhenInactive: Boolean
            get() = geckoSession.settings.suspendMediaWhenInactive
            set(value) { geckoSession.settings.suspendMediaWhenInactive = value }
84
85
    }

86
87
    private var initialLoad = true

88
89
    private var requestFromWebContent = false

90
91
92
    override val coroutineContext: CoroutineContext
        get() = context + job

93
    init {
94
        createGeckoSession(shouldOpen = openGeckoSession)
95
96
97
98
99
    }

    /**
     * See [EngineSession.loadUrl]
     */
100
    override fun loadUrl(url: String, parent: EngineSession?, flags: LoadUrlFlags) {
101
        requestFromWebContent = false
102
        geckoSession.loadUri(url, (parent as? GeckoEngineSession)?.geckoSession, flags.value)
103
104
    }

105
106
107
    /**
     * See [EngineSession.loadData]
     */
108
    override fun loadData(data: String, mimeType: String, encoding: String) {
109
        requestFromWebContent = false
110
111
112
113
        when (encoding) {
            "base64" -> geckoSession.loadData(data.toByteArray(), mimeType)
            else -> geckoSession.loadString(data, mimeType)
        }
114
115
    }

116
117
118
119
120
121
122
    /**
     * See [EngineSession.stopLoading]
     */
    override fun stopLoading() {
        geckoSession.stop()
    }

123
124
125
126
    /**
     * See [EngineSession.reload]
     */
    override fun reload() {
127
        requestFromWebContent = false
128
129
130
131
132
133
134
        geckoSession.reload()
    }

    /**
     * See [EngineSession.goBack]
     */
    override fun goBack() {
135
        requestFromWebContent = false
136
137
138
139
140
141
142
        geckoSession.goBack()
    }

    /**
     * See [EngineSession.goForward]
     */
    override fun goForward() {
143
        requestFromWebContent = false
144
145
146
147
148
149
        geckoSession.goForward()
    }

    /**
     * See [EngineSession.saveState]
     */
150
151
    override fun saveState(): EngineSessionState {
        return GeckoEngineSessionState(lastSessionState)
152
153
154
155
156
    }

    /**
     * See [EngineSession.restoreState]
     */
157
158
159
    override fun restoreState(state: EngineSessionState) {
        if (state !is GeckoEngineSessionState) {
            throw IllegalStateException("Can only restore from GeckoEngineSessionState")
160
        }
161
162
163
164
165
166

        if (state.actualState == null) {
            return
        }

        geckoSession.restoreState(state.actualState)
167
168
    }

169
170
171
172
    /**
     * See [EngineSession.enableTrackingProtection]
     */
    override fun enableTrackingProtection(policy: TrackingProtectionPolicy) {
173
174
175
176
177
        val enabled = if (privateMode) {
            policy.useForPrivateSessions
        } else {
            policy.useForRegularSessions
        }
178
179
180
181
182
183
184
185
186
187
        /**
         * As described on https://bugzilla.mozilla.org/show_bug.cgi?id=1579264,useTrackingProtection
         * is a misleading setting. When is set to true is blocking content (scripts/sub-resources).
         * Instead of just turn on/off tracking protection. Until, this issue is fixed consumers need
         * a way to indicate, if they want to block content or not, this is why we use
         * [TrackingProtectionPolicy.TrackingCategory.SCRIPTS_AND_SUB_RESOURCES].
         */
        val shouldBlockContent =
            policy.contains(TrackingProtectionPolicy.TrackingCategory.SCRIPTS_AND_SUB_RESOURCES)

188
        geckoSession.settings.useTrackingProtection = shouldBlockContent
189
190
191
        if (!enabled) {
            disableTrackingProtectionOnGecko()
        }
192
        notifyObservers { onTrackerBlockingEnabledChange(enabled) }
193
194
195
196
197
198
    }

    /**
     * See [EngineSession.disableTrackingProtection]
     */
    override fun disableTrackingProtection() {
199
        disableTrackingProtectionOnGecko()
200
201
202
        notifyObservers { onTrackerBlockingEnabledChange(false) }
    }

203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
    /**
     * Indicates if this [EngineSession] should be ignored the tracking protection policies.
     * @param onResult A callback to inform if this [EngineSession] is in
     * the exception list, true if it is in, otherwise false.
     */
    internal fun isIgnoredForTrackingProtection(onResult: (Boolean) -> Unit) {
        runtime.contentBlockingController.checkException(geckoSession).accept {
            if (it != null) {
                onResult(it)
            } else {
                onResult(false)
            }
        }
    }

218
219
220
221
222
223
224
    // To fully disable tracking protection we need to change the different tracking protection
    // variables to none.
    private fun disableTrackingProtectionOnGecko() {
        geckoSession.settings.useTrackingProtection = false
        runtime.settings.contentBlocking.setAntiTracking(ContentBlocking.AntiTracking.NONE)
        runtime.settings.contentBlocking.cookieBehavior = ContentBlocking.CookieBehavior.ACCEPT_ALL
        runtime.settings.contentBlocking.setStrictSocialTrackingProtection(false)
225
        runtime.settings.contentBlocking.setEnhancedTrackingProtectionLevel(ContentBlocking.EtpLevel.NONE)
226
227
    }

228
229
230
    /**
     * See [EngineSession.settings]
     */
231
    override fun toggleDesktopMode(enable: Boolean, reload: Boolean) {
232
        val currentMode = geckoSession.settings.userAgentMode
233
234
        val currentViewPortMode = geckoSession.settings.viewportMode

235
236
237
238
239
240
        val newMode = if (enable) {
            GeckoSessionSettings.USER_AGENT_MODE_DESKTOP
        } else {
            GeckoSessionSettings.USER_AGENT_MODE_MOBILE
        }

241
242
243
244
245
246
247
        val newViewportMode = if (enable) {
            GeckoSessionSettings.VIEWPORT_MODE_DESKTOP
        } else {
            GeckoSessionSettings.VIEWPORT_MODE_MOBILE
        }

        if (newMode != currentMode || newViewportMode != currentViewPortMode) {
248
            geckoSession.settings.userAgentMode = newMode
249
            geckoSession.settings.viewportMode = newViewportMode
250
251
252
253
            notifyObservers { onDesktopModeChange(enable) }
        }

        if (reload) {
254
            this.reload()
255
        }
256
257
    }

258
259
260
261
262
263
264
    /**
     * See [EngineSession.findAll]
     */
    override fun findAll(text: String) {
        notifyObservers { onFind(text) }
        geckoSession.finder.find(text, 0).then { result: GeckoSession.FinderResult? ->
            result?.let {
265
266
                val activeMatchOrdinal = if (it.current > 0) it.current - 1 else it.current
                notifyObservers { onFindResult(activeMatchOrdinal, it.total, true) }
267
268
269
270
271
272
273
274
            }
            GeckoResult<Void>()
        }
    }

    /**
     * See [EngineSession.findNext]
     */
275
    @SuppressLint("WrongConstant") // FinderFindFlags annotation doesn't include a 0 value.
276
277
278
279
    override fun findNext(forward: Boolean) {
        val findFlags = if (forward) 0 else GeckoSession.FINDER_FIND_BACKWARDS
        geckoSession.finder.find(null, findFlags).then { result: GeckoSession.FinderResult? ->
            result?.let {
280
281
                val activeMatchOrdinal = if (it.current > 0) it.current - 1 else it.current
                notifyObservers { onFindResult(activeMatchOrdinal, it.total, true) }
282
283
284
285
286
287
288
289
290
291
292
293
            }
            GeckoResult<Void>()
        }
    }

    /**
     * See [EngineSession.clearFindMatches]
     */
    override fun clearFindMatches() {
        geckoSession.finder.clear()
    }

294
295
296
297
298
299
300
    /**
     * See [EngineSession.exitFullScreenMode]
     */
    override fun exitFullScreenMode() {
        geckoSession.exitFullScreen()
    }

301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
    /**
     * See [EngineSession.recoverFromCrash]
     */
    @Synchronized
    override fun recoverFromCrash(): Boolean {
        val state = stateBeforeCrash

        return if (state != null) {
            geckoSession.restoreState(state)
            stateBeforeCrash = null
            true
        } else {
            false
        }
    }

317
318
319
320
321
    /**
     * See [EngineSession.close].
     */
    override fun close() {
        super.close()
322
        job.cancel()
323
324
325
        geckoSession.close()
    }

326
327
328
    /**
     * NavigationDelegate implementation for forwarding callbacks to observers of the session.
     */
329
    @Suppress("ComplexMethod")
330
    private fun createNavigationDelegate() = object : GeckoSession.NavigationDelegate {
331
332
333
334
335
        override fun onLocationChange(session: GeckoSession, url: String?) {
            if (url == null) {
                return // ¯\_(ツ)_/¯
            }

336
337
338
339
340
            // Ignore initial load of about:blank (see https://github.com/mozilla-mobile/android-components/issues/403)
            if (initialLoad && url == ABOUT_BLANK) {
                return
            }
            initialLoad = false
341
342
343
344
345
            isIgnoredForTrackingProtection { ignored ->
                notifyObservers {
                    onExcludedOnTrackingProtectionChange(ignored)
                }
            }
346
347
348
349
            notifyObservers { onLocationChange(url) }
        }

        override fun onLoadRequest(
350
            session: GeckoSession,
351
352
            request: NavigationDelegate.LoadRequest
        ): GeckoResult<AllowOrDeny> {
353
            if (request.target == GeckoSession.NavigationDelegate.TARGET_WINDOW_NEW) {
354
                return GeckoResult.fromValue(AllowOrDeny.ALLOW)
355
356
            }

357
358
            val response = settings.requestInterceptor?.onLoadRequest(
                this@GeckoEngineSession,
359
                request.uri
360
            )?.apply {
361
362
363
364
                when (this) {
                    is InterceptionResponse.Content -> loadData(data, mimeType, encoding)
                    is InterceptionResponse.Url -> loadUrl(url)
                }
365
            }
366
367
368
369
370

            return if (response != null) {
                GeckoResult.fromValue(AllowOrDeny.DENY)
            } else {
                notifyObservers {
371
                    // Unlike the name LoadRequest.isRedirect may imply this flag is not about http redirects. The flag
372
                    // is "True if and only if the request was triggered by an HTTP redirect."
373
                    // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1545170
374
                    onLoadRequest(
375
                        url = request.uri,
376
377
378
                        triggeredByRedirect = request.isRedirect,
                        triggeredByWebContent = requestFromWebContent
                    )
379
380
381
382
                }

                GeckoResult.fromValue(AllowOrDeny.ALLOW)
            }
383
384
        }

385
        override fun onCanGoForward(session: GeckoSession, canGoForward: Boolean) {
386
387
388
            notifyObservers { onNavigationStateChange(canGoForward = canGoForward) }
        }

389
        override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) {
390
391
392
393
            notifyObservers { onNavigationStateChange(canGoBack = canGoBack) }
        }

        override fun onNewSession(
394
395
            session: GeckoSession,
            uri: String
396
397
398
399
        ): GeckoResult<GeckoSession> {
            val newEngineSession = GeckoEngineSession(runtime, privateMode, defaultSettings, openGeckoSession = false)
            notifyObservers {
                MainScope().launch {
400
                    onWindowRequest(GeckoWindowRequest(uri, newEngineSession))
401
402
403
404
                }
            }
            return GeckoResult.fromValue(newEngineSession.geckoSession)
        }
405
406

        override fun onLoadError(
407
            session: GeckoSession,
408
            uri: String?,
409
            error: WebRequestError
410
411
412
        ): GeckoResult<String> {
            settings.requestInterceptor?.onErrorRequest(
                this@GeckoEngineSession,
413
                geckoErrorToErrorType(error.code),
414
                uri
415
            )?.apply {
416
                return GeckoResult.fromValue(Base64.encodeToUriString(data))
417
            }
418
419
            return GeckoResult.fromValue(null)
        }
420
421
422
    }

    /**
423
424
     * ProgressDelegate implementation for forwarding callbacks to observers of the session.
     */
425
    private fun createProgressDelegate() = object : GeckoSession.ProgressDelegate {
426
        override fun onProgressChange(session: GeckoSession, progress: Int) {
427
428
429
            notifyObservers { onProgress(progress) }
        }

430
        override fun onSecurityChange(
431
432
            session: GeckoSession,
            securityInfo: GeckoSession.ProgressDelegate.SecurityInformation
433
        ) {
434
            // Ignore initial load of about:blank (see https://github.com/mozilla-mobile/android-components/issues/403)
435
            if (initialLoad && securityInfo.origin?.startsWith(MOZ_NULL_PRINCIPAL) == true) {
436
437
438
                return
            }

439
            notifyObservers {
Grisha Kruglov's avatar
Grisha Kruglov committed
440
                onSecurityChange(securityInfo.isSecure, securityInfo.host, securityInfo.issuerOrganization)
441
442
443
            }
        }

444
445
        override fun onPageStart(session: GeckoSession, url: String) {
            currentUrl = url
Grisha Kruglov's avatar
Grisha Kruglov committed
446

447
448
449
450
451
452
            notifyObservers {
                onProgress(PROGRESS_START)
                onLoadingStateChange(true)
            }
        }

453
        override fun onPageStop(session: GeckoSession, success: Boolean) {
454
455
456
457
            // by the time we reach here, any new request will come from web content.
            // If it comes from the chrome, loadUrl(url) or loadData(string) will set it to
            // false.
            requestFromWebContent = true
458
459
460
            notifyObservers {
                onProgress(PROGRESS_STOP)
                onLoadingStateChange(false)
461
462
            }
        }
463
464
465
466

        override fun onSessionStateChange(session: GeckoSession, sessionState: GeckoSession.SessionState) {
            lastSessionState = sessionState
        }
467
468
    }

469
470
    @Suppress("ComplexMethod")
    internal fun createHistoryDelegate() = object : GeckoSession.HistoryDelegate {
471
        @SuppressWarnings("ReturnCount")
472
473
474
475
476
477
        override fun onVisited(
            session: GeckoSession,
            url: String,
            lastVisitedURL: String?,
            flags: Int
        ): GeckoResult<Boolean>? {
Grisha Kruglov's avatar
Grisha Kruglov committed
478
479
480
481
            // Don't track:
            // - private visits
            // - error pages
            // - non-top level visits (i.e. iframes).
482
483
484
485
486
487
            if (privateMode ||
                (flags and GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL) == 0 ||
                (flags and GeckoSession.HistoryDelegate.VISIT_UNRECOVERABLE_ERROR) != 0) {
                return GeckoResult.fromValue(false)
            }

488
489
490
491
492
            val isReload = lastVisitedURL?.let { it == url } ?: false

            val visitType = if (isReload) {
                VisitType.RELOAD
            } else {
493
494
495
496
497
498
499
500
501
502
503
                // Note the difference between `VISIT_REDIRECT_PERMANENT`,
                // `VISIT_REDIRECT_TEMPORARY`, `VISIT_REDIRECT_SOURCE`, and
                // `VISIT_REDIRECT_SOURCE_PERMANENT`.
                //
                // The former two indicate if the visited page is the *target*
                // of a redirect; that is, another page redirected to it.
                //
                // The latter two indicate if the visited page is the *source*
                // of a redirect: it's redirecting to another page, because the
                // server returned an HTTP 3xy status code.
                if (flags and GeckoSession.HistoryDelegate.VISIT_REDIRECT_PERMANENT != 0) {
504
                    VisitType.REDIRECT_PERMANENT
505
                } else if (flags and GeckoSession.HistoryDelegate.VISIT_REDIRECT_TEMPORARY != 0) {
506
507
508
509
510
                    VisitType.REDIRECT_TEMPORARY
                } else {
                    VisitType.LINK
                }
            }
511
512
513
514
515
516
517
            val redirectSource = when {
                flags and GeckoSession.HistoryDelegate.VISIT_REDIRECT_SOURCE_PERMANENT != 0 ->
                    RedirectSource.PERMANENT
                flags and GeckoSession.HistoryDelegate.VISIT_REDIRECT_SOURCE != 0 ->
                    RedirectSource.TEMPORARY
                else -> RedirectSource.NOT_A_SOURCE
            }
518

519
520
            val delegate = settings.historyTrackingDelegate ?: return GeckoResult.fromValue(false)

521
522
523
524
525
            // Check if the delegate wants this type of url.
            if (!delegate.shouldStoreUri(url)) {
                return GeckoResult.fromValue(false)
            }

526
            return launchGeckoResult {
527
                delegate.onVisited(url, PageVisit(visitType, redirectSource))
528
                true
529
530
531
532
533
534
535
536
537
538
539
540
541
            }
        }

        override fun getVisited(
            session: GeckoSession,
            urls: Array<out String>
        ): GeckoResult<BooleanArray>? {
            if (privateMode) {
                return GeckoResult.fromValue(null)
            }

            val delegate = settings.historyTrackingDelegate ?: return GeckoResult.fromValue(null)

542
543
544
            return launchGeckoResult {
                val visits = delegate.getVisited(urls.toList())
                visits.toBooleanArray()
545
546
547
548
            }
        }
    }

Grisha Kruglov's avatar
Grisha Kruglov committed
549
    @Suppress("ComplexMethod")
550
    internal fun createContentDelegate() = object : GeckoSession.ContentDelegate {
551
        override fun onFirstComposite(session: GeckoSession) = Unit
552

553
554
555
556
        override fun onContextMenu(
            session: GeckoSession,
            screenX: Int,
            screenY: Int,
557
            element: GeckoSession.ContentDelegate.ContextElement
558
        ) {
559
            val hitResult = handleLongClick(element.srcUri, element.type, element.linkUri, element.title)
560
561
562
563
564
            hitResult?.let {
                notifyObservers { onLongPress(it) }
            }
        }

565
        override fun onCrash(session: GeckoSession) {
566
567
            stateBeforeCrash = lastSessionState

568
            recoverGeckoSession()
569

570
            notifyObservers { onCrash() }
571
        }
572

573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
        override fun onKill(session: GeckoSession) {
            // The content process of this session got killed (resources reclaimed by Android).
            // Let's recover and restore the last known state.

            val state = lastSessionState

            recoverGeckoSession()

            state?.let { geckoSession.restoreState(it) }

            notifyObservers { onProcessKilled() }
        }

        private fun recoverGeckoSession() {
            // Recover the GeckoSession after the process getting killed or crashing. We create a
            // new underlying GeckoSession.
            // Eventually we may be able to re-use the same GeckoSession by re-opening it. However
            // that seems to have caused issues:
            // https://github.com/mozilla-mobile/android-components/issues/3640

            geckoSession.close()
            createGeckoSession()
        }

597
598
599
        override fun onFullScreen(session: GeckoSession, fullScreen: Boolean) {
            notifyObservers { onFullScreenChange(fullScreen) }
        }
600
601
602
603

        override fun onExternalResponse(session: GeckoSession, response: GeckoSession.WebResponseInfo) {
            notifyObservers {
                onExternalResource(
Renan Barros's avatar
Renan Barros committed
604
605
606
                        url = response.uri,
                        contentLength = response.contentLength,
                        contentType = response.contentType,
607
                        fileName = response.filename)
608
609
610
            }
        }

611
612
613
614
615
616
617
618
619
        override fun onCloseRequest(session: GeckoSession) {
            notifyObservers {
                onWindowRequest(GeckoWindowRequest(
                        engineSession = this@GeckoEngineSession,
                        type = WindowRequest.Type.CLOSE
                    )
                )
            }
        }
620

621
        override fun onTitleChange(session: GeckoSession, title: String?) {
622
623
624
625
            if (!privateMode) {
                currentUrl?.let { url ->
                    settings.historyTrackingDelegate?.let { delegate ->
                        runBlocking {
626
                            delegate.onTitleChanged(url, title ?: "")
627
                        }
628
629
                    }
                }
Grisha Kruglov's avatar
Grisha Kruglov committed
630
            }
631
            notifyObservers { onTitleChange(title ?: "") }
632
633
634
        }

        override fun onFocusRequest(session: GeckoSession) = Unit
635
636
637
638
639
640
641

        override fun onWebAppManifest(session: GeckoSession, manifest: JSONObject) {
            val parsed = WebAppManifestParser().parse(manifest)
            if (parsed is WebAppManifestParser.Result.Success) {
                notifyObservers { onWebAppManifestLoaded(parsed.manifest) }
            }
        }
642
643
    }

644
645
    private fun createContentBlockingDelegate() = object : ContentBlocking.Delegate {
        override fun onContentBlocked(session: GeckoSession, event: ContentBlocking.BlockEvent) {
646
647
648
            notifyObservers {
                onTrackerBlocked(event.toTracker())
            }
649
        }
650
651
652
653
654
655

        override fun onContentLoaded(session: GeckoSession, event: ContentBlocking.BlockEvent) {
            notifyObservers {
                onTrackerLoaded(event.toTracker())
            }
        }
656
657
    }

658
    private fun ContentBlocking.BlockEvent.toTracker(): Tracker {
659
        val blockedContentCategories = mutableListOf<TrackingProtectionPolicy.TrackingCategory>()
660

661
        if (antiTrackingCategory.contains(ContentBlocking.AntiTracking.AD)) {
662
            blockedContentCategories.add(TrackingProtectionPolicy.TrackingCategory.AD)
663
664
        }

665
        if (antiTrackingCategory.contains(ContentBlocking.AntiTracking.ANALYTIC)) {
666
            blockedContentCategories.add(TrackingProtectionPolicy.TrackingCategory.ANALYTICS)
667
668
        }

669
        if (antiTrackingCategory.contains(ContentBlocking.AntiTracking.SOCIAL)) {
670
            blockedContentCategories.add(TrackingProtectionPolicy.TrackingCategory.SOCIAL)
671
672
        }

673
        if (antiTrackingCategory.contains(ContentBlocking.AntiTracking.FINGERPRINTING)) {
674
            blockedContentCategories.add(TrackingProtectionPolicy.TrackingCategory.FINGERPRINTING)
675
676
        }

677
        if (antiTrackingCategory.contains(ContentBlocking.AntiTracking.CRYPTOMINING)) {
678
            blockedContentCategories.add(TrackingProtectionPolicy.TrackingCategory.CRYPTOMINING)
679
680
        }

681
        if (antiTrackingCategory.contains(ContentBlocking.AntiTracking.CONTENT)) {
682
            blockedContentCategories.add(TrackingProtectionPolicy.TrackingCategory.CONTENT)
683
684
        }

685
        if (antiTrackingCategory.contains(ContentBlocking.AntiTracking.TEST)) {
686
            blockedContentCategories.add(TrackingProtectionPolicy.TrackingCategory.TEST)
687
688
        }

689
690
691
692
693
694
695
696
697
698
699
700
        return Tracker(
            url = uri,
            trackingCategories = blockedContentCategories,
            cookiePolicies = getCookiePolicies()
        )
    }

    private fun ContentBlocking.BlockEvent.getCookiePolicies(): List<TrackingProtectionPolicy.CookiePolicy> {
        val cookiesPolicies = mutableListOf<TrackingProtectionPolicy.CookiePolicy>()

        if (cookieBehaviorCategory == ContentBlocking.CookieBehavior.ACCEPT_ALL) {
            cookiesPolicies.add(TrackingProtectionPolicy.CookiePolicy.ACCEPT_ALL)
701
        }
702
703
704
705
706
707
708

        if (cookieBehaviorCategory.contains(ContentBlocking.CookieBehavior.ACCEPT_FIRST_PARTY)) {
            cookiesPolicies.add(TrackingProtectionPolicy.CookiePolicy.ACCEPT_ONLY_FIRST_PARTY)
        }

        if (cookieBehaviorCategory.contains(ContentBlocking.CookieBehavior.ACCEPT_NONE)) {
            cookiesPolicies.add(TrackingProtectionPolicy.CookiePolicy.ACCEPT_NONE)
709
        }
710
711
712
713
714
715
716
717
718
719

        if (cookieBehaviorCategory.contains(ContentBlocking.CookieBehavior.ACCEPT_NON_TRACKERS)) {
            cookiesPolicies.add(TrackingProtectionPolicy.CookiePolicy.ACCEPT_NON_TRACKERS)
        }

        if (cookieBehaviorCategory.contains(ContentBlocking.CookieBehavior.ACCEPT_VISITED)) {
            cookiesPolicies.add(TrackingProtectionPolicy.CookiePolicy.ACCEPT_VISITED)
        }

        return cookiesPolicies
720
721
722
723
724
725
    }

    private operator fun Int.contains(mask: Int): Boolean {
        return (this and mask) != 0
    }

726
727
    private fun createPermissionDelegate() = object : GeckoSession.PermissionDelegate {
        override fun onContentPermissionRequest(
728
            session: GeckoSession,
729
730
731
732
733
734
735
736
737
            uri: String?,
            type: Int,
            callback: GeckoSession.PermissionDelegate.Callback
        ) {
            val request = GeckoPermissionRequest.Content(uri ?: "", type, callback)
            notifyObservers { onContentPermissionRequest(request) }
        }

        override fun onMediaPermissionRequest(
738
739
            session: GeckoSession,
            uri: String,
740
741
742
743
744
            video: Array<out GeckoSession.PermissionDelegate.MediaSource>?,
            audio: Array<out GeckoSession.PermissionDelegate.MediaSource>?,
            callback: GeckoSession.PermissionDelegate.MediaCallback
        ) {
            val request = GeckoPermissionRequest.Media(
745
                    uri,
746
747
748
749
750
751
752
                    video?.toList() ?: emptyList(),
                    audio?.toList() ?: emptyList(),
                    callback)
            notifyObservers { onContentPermissionRequest(request) }
        }

        override fun onAndroidPermissionsRequest(
753
            session: GeckoSession,
754
755
756
757
758
759
760
761
762
763
            permissions: Array<out String>?,
            callback: GeckoSession.PermissionDelegate.Callback
        ) {
            val request = GeckoPermissionRequest.App(
                    permissions?.toList() ?: emptyList(),
                    callback)
            notifyObservers { onAppPermissionRequest(request) }
        }
    }

764
765
766
767
768
769
    private fun createScrollDelegate() = object : GeckoSession.ScrollDelegate {
        override fun onScrollChanged(session: GeckoSession, scrollX: Int, scrollY: Int) {
            this@GeckoEngineSession.scrollY = scrollY
        }
    }

770
    @Suppress("ComplexMethod")
771
    fun handleLongClick(elementSrc: String?, elementType: Int, uri: String? = null, title: String? = null): HitResult? {
772
        return when (elementType) {
773
            GeckoSession.ContentDelegate.ContextElement.TYPE_AUDIO ->
774
775
776
                elementSrc?.let {
                    HitResult.AUDIO(it)
                }
777
            GeckoSession.ContentDelegate.ContextElement.TYPE_VIDEO ->
778
779
780
                elementSrc?.let {
                    HitResult.VIDEO(it)
                }
781
            GeckoSession.ContentDelegate.ContextElement.TYPE_IMAGE -> {
782
783
784
785
                when {
                    elementSrc != null && uri != null ->
                        HitResult.IMAGE_SRC(elementSrc, uri)
                    elementSrc != null ->
786
                        HitResult.IMAGE(elementSrc, title)
787
788
789
                    else -> HitResult.UNKNOWN("")
                }
            }
790
            GeckoSession.ContentDelegate.ContextElement.TYPE_NONE -> {
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
                elementSrc?.let {
                    when {
                        it.isPhone() -> HitResult.PHONE(it)
                        it.isEmail() -> HitResult.EMAIL(it)
                        it.isGeoLocation() -> HitResult.GEO(it)
                        else -> HitResult.UNKNOWN(it)
                    }
                } ?: uri?.let {
                    HitResult.UNKNOWN(it)
                }
            }
            else -> HitResult.UNKNOWN("")
        }
    }

806
    private fun createGeckoSession(shouldOpen: Boolean = true) {
807
        this.geckoSession = geckoSessionProvider()
808

809
810
811
        defaultSettings?.trackingProtectionPolicy?.let { enableTrackingProtection(it) }
        defaultSettings?.requestInterceptor?.let { settings.requestInterceptor = it }
        defaultSettings?.historyTrackingDelegate?.let { settings.historyTrackingDelegate = it }
812
813
814
        defaultSettings?.testingModeEnabled?.let { geckoSession.settings.fullAccessibilityTree = it }
        defaultSettings?.userAgentString?.let { geckoSession.settings.userAgentOverride = it }
        defaultSettings?.suspendMediaWhenInactive?.let { geckoSession.settings.suspendMediaWhenInactive = it }
815

816
817
818
        if (shouldOpen) {
            geckoSession.open(runtime)
        }
819
820
821
822

        geckoSession.navigationDelegate = createNavigationDelegate()
        geckoSession.progressDelegate = createProgressDelegate()
        geckoSession.contentDelegate = createContentDelegate()
823
        geckoSession.contentBlockingDelegate = createContentBlockingDelegate()
824
        geckoSession.permissionDelegate = createPermissionDelegate()
825
        geckoSession.promptDelegate = GeckoPromptDelegate(this)
826
        geckoSession.historyDelegate = createHistoryDelegate()
827
        geckoSession.mediaDelegate = GeckoMediaDelegate(this)
828
        geckoSession.scrollDelegate = createScrollDelegate()
829
830
    }

831
832
833
    companion object {
        internal const val PROGRESS_START = 25
        internal const val PROGRESS_STOP = 100
834
835
        internal const val MOZ_NULL_PRINCIPAL = "moz-nullprincipal:"
        internal const val ABOUT_BLANK = "about:blank"
836
837
838
839
840

        /**
         * Provides an ErrorType corresponding to the error code provided.
         */
        @Suppress("ComplexMethod")
841
        internal fun geckoErrorToErrorType(errorCode: Int) =
842
            when (errorCode) {
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
                WebRequestError.ERROR_UNKNOWN -> ErrorType.UNKNOWN
                WebRequestError.ERROR_SECURITY_SSL -> ErrorType.ERROR_SECURITY_SSL
                WebRequestError.ERROR_SECURITY_BAD_CERT -> ErrorType.ERROR_SECURITY_BAD_CERT
                WebRequestError.ERROR_NET_INTERRUPT -> ErrorType.ERROR_NET_INTERRUPT
                WebRequestError.ERROR_NET_TIMEOUT -> ErrorType.ERROR_NET_TIMEOUT
                WebRequestError.ERROR_CONNECTION_REFUSED -> ErrorType.ERROR_CONNECTION_REFUSED
                WebRequestError.ERROR_UNKNOWN_SOCKET_TYPE -> ErrorType.ERROR_UNKNOWN_SOCKET_TYPE
                WebRequestError.ERROR_REDIRECT_LOOP -> ErrorType.ERROR_REDIRECT_LOOP
                WebRequestError.ERROR_OFFLINE -> ErrorType.ERROR_OFFLINE
                WebRequestError.ERROR_PORT_BLOCKED -> ErrorType.ERROR_PORT_BLOCKED
                WebRequestError.ERROR_NET_RESET -> ErrorType.ERROR_NET_RESET
                WebRequestError.ERROR_UNSAFE_CONTENT_TYPE -> ErrorType.ERROR_UNSAFE_CONTENT_TYPE
                WebRequestError.ERROR_CORRUPTED_CONTENT -> ErrorType.ERROR_CORRUPTED_CONTENT
                WebRequestError.ERROR_CONTENT_CRASHED -> ErrorType.ERROR_CONTENT_CRASHED
                WebRequestError.ERROR_INVALID_CONTENT_ENCODING -> ErrorType.ERROR_INVALID_CONTENT_ENCODING
                WebRequestError.ERROR_UNKNOWN_HOST -> ErrorType.ERROR_UNKNOWN_HOST
                WebRequestError.ERROR_MALFORMED_URI -> ErrorType.ERROR_MALFORMED_URI
                WebRequestError.ERROR_UNKNOWN_PROTOCOL -> ErrorType.ERROR_UNKNOWN_PROTOCOL
                WebRequestError.ERROR_FILE_NOT_FOUND -> ErrorType.ERROR_FILE_NOT_FOUND
                WebRequestError.ERROR_FILE_ACCESS_DENIED -> ErrorType.ERROR_FILE_ACCESS_DENIED
                WebRequestError.ERROR_PROXY_CONNECTION_REFUSED -> ErrorType.ERROR_PROXY_CONNECTION_REFUSED
                WebRequestError.ERROR_UNKNOWN_PROXY_HOST -> ErrorType.ERROR_UNKNOWN_PROXY_HOST
                WebRequestError.ERROR_SAFEBROWSING_MALWARE_URI -> ErrorType.ERROR_SAFEBROWSING_MALWARE_URI
                WebRequestError.ERROR_SAFEBROWSING_UNWANTED_URI -> ErrorType.ERROR_SAFEBROWSING_UNWANTED_URI
                WebRequestError.ERROR_SAFEBROWSING_HARMFUL_URI -> ErrorType.ERROR_SAFEBROWSING_HARMFUL_URI
                WebRequestError.ERROR_SAFEBROWSING_PHISHING_URI -> ErrorType.ERROR_SAFEBROWSING_PHISHING_URI
869
870
                else -> ErrorType.UNKNOWN
            }
871
872
    }
}