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

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

69
    internal lateinit var geckoSession: GeckoSession
Grisha Kruglov's avatar
Grisha Kruglov committed
70
    internal var currentUrl: String? = null
Tiger Oakes's avatar
Tiger Oakes committed
71
    internal var scrollY: Int = 0
72
73
74
75
76

    // This is set once the first content paint has occurred and can be used to
    // decide if it's safe to call capturePixels on the view.
    internal var firstContentfulPaint = false

77
    internal var job: Job = Job()
78
    private var lastSessionState: GeckoSession.SessionState? = null
79
    private var stateBeforeCrash: GeckoSession.SessionState? = null
80
    private var canGoBack: Boolean = false
81

82
83
84
    /**
     * See [EngineSession.settings]
     */
85
    override val settings: Settings = object : Settings() {
86
        override var requestInterceptor: RequestInterceptor? = null
Grisha Kruglov's avatar
Grisha Kruglov committed
87
        override var historyTrackingDelegate: HistoryTrackingDelegate? = null
88
        override var userAgentString: String?
89
90
            get() = geckoSession.settings.userAgentOverride
            set(value) { geckoSession.settings.userAgentOverride = value }
91
92
93
        override var suspendMediaWhenInactive: Boolean
            get() = geckoSession.settings.suspendMediaWhenInactive
            set(value) { geckoSession.settings.suspendMediaWhenInactive = value }
94
95
    }

96
97
    private var initialLoad = true

98
99
100
    override val coroutineContext: CoroutineContext
        get() = context + job

101
    init {
102
        createGeckoSession(shouldOpen = openGeckoSession)
103
104
105
106
107
    }

    /**
     * See [EngineSession.loadUrl]
     */
108
109
110
111
112
113
114
    override fun loadUrl(
        url: String,
        parent: EngineSession?,
        flags: LoadUrlFlags,
        additionalHeaders: Map<String, String>?
    ) {
        geckoSession.loadUri(url, (parent as? GeckoEngineSession)?.geckoSession, flags.value, additionalHeaders)
115
116
    }

117
118
119
    /**
     * See [EngineSession.loadData]
     */
120
121
122
123
124
    override fun loadData(data: String, mimeType: String, encoding: String) {
        when (encoding) {
            "base64" -> geckoSession.loadData(data.toByteArray(), mimeType)
            else -> geckoSession.loadString(data, mimeType)
        }
125
126
    }

127
128
129
130
131
132
133
    /**
     * See [EngineSession.stopLoading]
     */
    override fun stopLoading() {
        geckoSession.stop()
    }

134
135
136
137
138
139
140
141
142
143
144
145
    /**
     * See [EngineSession.reload]
     */
    override fun reload() {
        geckoSession.reload()
    }

    /**
     * See [EngineSession.goBack]
     */
    override fun goBack() {
        geckoSession.goBack()
146
147
148
        if (canGoBack) {
            notifyObservers { onNavigateBack() }
        }
149
150
151
152
153
154
155
156
157
158
159
    }
    /**
     * See [EngineSession.goForward]
     */
    override fun goForward() {
        geckoSession.goForward()
    }

    /**
     * See [EngineSession.saveState]
     */
160
161
    override fun saveState(): EngineSessionState {
        return GeckoEngineSessionState(lastSessionState)
162
163
164
165
166
    }

    /**
     * See [EngineSession.restoreState]
     */
167
    override fun restoreState(state: EngineSessionState): Boolean {
168
169
170
171
172
        if (state !is GeckoEngineSessionState) {
            throw IllegalStateException("Can only restore from GeckoEngineSessionState")
        }

        if (state.actualState == null) {
173
            return false
174
        }
175
176

        geckoSession.restoreState(state.actualState)
177
        return true
178
179
    }

180
181
182
183
    /**
     * See [EngineSession.enableTrackingProtection]
     */
    override fun enableTrackingProtection(policy: TrackingProtectionPolicy) {
184
185
186
187
188
        val enabled = if (privateMode) {
            policy.useForPrivateSessions
        } else {
            policy.useForRegularSessions
        }
189
190
191
192
193
194
195
196
197
198
199
200
201
202
        /**
         * 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)

        geckoSession.settings.useTrackingProtection = shouldBlockContent
        if (!enabled) {
            disableTrackingProtectionOnGecko()
        }
203
        notifyAtLeastOneObserver { onTrackerBlockingEnabledChange(enabled) }
204
205
206
207
208
209
    }

    /**
     * See [EngineSession.disableTrackingProtection]
     */
    override fun disableTrackingProtection() {
210
        disableTrackingProtectionOnGecko()
211
212
213
        notifyObservers { onTrackerBlockingEnabledChange(false) }
    }

214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
    /**
     * 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)
            }
        }
    }

229
230
231
232
233
234
235
    // 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)
236
        runtime.settings.contentBlocking.setEnhancedTrackingProtectionLevel(ContentBlocking.EtpLevel.NONE)
237
238
    }

239
240
241
    /**
     * See [EngineSession.settings]
     */
242
    override fun toggleDesktopMode(enable: Boolean, reload: Boolean) {
243
        val currentMode = geckoSession.settings.userAgentMode
244
245
        val currentViewPortMode = geckoSession.settings.viewportMode

246
247
248
249
250
251
        val newMode = if (enable) {
            GeckoSessionSettings.USER_AGENT_MODE_DESKTOP
        } else {
            GeckoSessionSettings.USER_AGENT_MODE_MOBILE
        }

252
253
254
255
256
257
258
        val newViewportMode = if (enable) {
            GeckoSessionSettings.VIEWPORT_MODE_DESKTOP
        } else {
            GeckoSessionSettings.VIEWPORT_MODE_MOBILE
        }

        if (newMode != currentMode || newViewportMode != currentViewPortMode) {
259
            geckoSession.settings.userAgentMode = newMode
260
            geckoSession.settings.viewportMode = newViewportMode
261
            notifyObservers { onDesktopModeChange(enable) }
262
263
264
        }

        if (reload) {
265
            this.reload()
266
267
268
        }
    }

269
270
271
272
273
274
275
    /**
     * See [EngineSession.findAll]
     */
    override fun findAll(text: String) {
        notifyObservers { onFind(text) }
        geckoSession.finder.find(text, 0).then { result: GeckoSession.FinderResult? ->
            result?.let {
276
277
                val activeMatchOrdinal = if (it.current > 0) it.current - 1 else it.current
                notifyObservers { onFindResult(activeMatchOrdinal, it.total, true) }
278
279
280
281
282
283
284
285
            }
            GeckoResult<Void>()
        }
    }

    /**
     * See [EngineSession.findNext]
     */
286
    @SuppressLint("WrongConstant") // FinderFindFlags annotation doesn't include a 0 value.
287
288
289
290
    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 {
291
292
                val activeMatchOrdinal = if (it.current > 0) it.current - 1 else it.current
                notifyObservers { onFindResult(activeMatchOrdinal, it.total, true) }
293
294
295
296
297
298
299
300
301
302
303
304
            }
            GeckoResult<Void>()
        }
    }

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

305
306
307
308
309
310
311
    /**
     * See [EngineSession.exitFullScreenMode]
     */
    override fun exitFullScreenMode() {
        geckoSession.exitFullScreen()
    }

312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
    /**
     * See [EngineSession.recoverFromCrash]
     */
    @Synchronized
    override fun recoverFromCrash(): Boolean {
        val state = stateBeforeCrash

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

328
329
330
331
332
333
334
    /**
     * See [EngineSession.markActiveForWebExtensions].
     */
    override fun markActiveForWebExtensions(active: Boolean) {
        runtime.webExtensionController.setTabActive(geckoSession, active)
    }

335
336
337
338
339
    /**
     * See [EngineSession.close].
     */
    override fun close() {
        super.close()
340
        job.cancel()
341
        geckoSession.close()
342
        firstContentfulPaint = false
343
344
    }

345
346
347
    /**
     * NavigationDelegate implementation for forwarding callbacks to observers of the session.
     */
348
    @Suppress("ComplexMethod")
349
    private fun createNavigationDelegate() = object : GeckoSession.NavigationDelegate {
350
351
352
353
354
        override fun onLocationChange(session: GeckoSession, url: String?) {
            if (url == null) {
                return // ¯\_(ツ)_/¯
            }

355
356
357
358
359
            // Ignore initial load of about:blank (see https://github.com/mozilla-mobile/android-components/issues/403)
            if (initialLoad && url == ABOUT_BLANK) {
                return
            }
            initialLoad = false
360
361
362
363
364
            isIgnoredForTrackingProtection { ignored ->
                notifyObservers {
                    onExcludedOnTrackingProtectionChange(ignored)
                }
            }
365
366
367
368
            notifyObservers { onLocationChange(url) }
        }

        override fun onLoadRequest(
369
            session: GeckoSession,
370
371
            request: NavigationDelegate.LoadRequest
        ): GeckoResult<AllowOrDeny> {
372
            if (request.target == NavigationDelegate.TARGET_WINDOW_NEW) {
373
                return GeckoResult.fromValue(AllowOrDeny.ALLOW)
374
375
            }

376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
            val interceptor = settings.requestInterceptor
            val response = if (
                interceptor != null && (!request.isDirectNavigation || interceptor.interceptsAppInitiatedRequests())
            ) {
                val engineSession = this@GeckoEngineSession
                val isSameDomain = engineSession.currentUrl?.tryGetHostFromUrl() == request.uri.tryGetHostFromUrl()
                interceptor.onLoadRequest(
                    engineSession,
                    request.uri,
                    request.hasUserGesture,
                    isSameDomain
                )?.apply {
                    when (this) {
                        is InterceptionResponse.Content -> loadData(data, mimeType, encoding)
                        is InterceptionResponse.Url -> loadUrl(url)
                        is InterceptionResponse.AppIntent -> {
                            notifyObservers {
                                onLaunchIntentRequest(url = url, appIntent = appIntent)
                            }
395
396
                        }
                    }
397
                }
398
399
            } else {
                null
400
            }
401
402
403
404
405

            return if (response != null) {
                GeckoResult.fromValue(AllowOrDeny.DENY)
            } else {
                notifyObservers {
406
                    onLoadRequest(
407
                        url = request.uri,
408
                        triggeredByRedirect = request.isRedirect,
409
                        triggeredByWebContent = request.hasUserGesture
410
                    )
411
412
                }

413
                GeckoResult.fromValue(AllowOrDeny.ALLOW)
414
            }
415
416
        }

417
        override fun onCanGoForward(session: GeckoSession, canGoForward: Boolean) {
418
419
420
            notifyObservers { onNavigationStateChange(canGoForward = canGoForward) }
        }

421
        override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) {
422
            notifyObservers { onNavigationStateChange(canGoBack = canGoBack) }
423
            this@GeckoEngineSession.canGoBack = canGoBack
424
425
426
        }

        override fun onNewSession(
427
428
            session: GeckoSession,
            uri: String
429
430
431
432
        ): GeckoResult<GeckoSession> {
            val newEngineSession = GeckoEngineSession(runtime, privateMode, defaultSettings, openGeckoSession = false)
            notifyObservers {
                MainScope().launch {
433
                    onWindowRequest(GeckoWindowRequest(uri, newEngineSession))
434
435
436
437
                }
            }
            return GeckoResult.fromValue(newEngineSession.geckoSession)
        }
438
439

        override fun onLoadError(
440
            session: GeckoSession,
441
            uri: String?,
442
            error: WebRequestError
443
        ): GeckoResult<String> {
444
            val uriToLoad = settings.requestInterceptor?.onErrorRequest(
445
                this@GeckoEngineSession,
446
                geckoErrorToErrorType(error.code),
447
                uri
448
449
450
451
452
            )?.run {
                when (this) {
                    is RequestInterceptor.ErrorResponse.Content -> Base64.encodeToUriString(data)
                    is RequestInterceptor.ErrorResponse.Uri -> this.uri
                }
453
            }
454
            return GeckoResult.fromValue(uriToLoad)
455
        }
456
457
458
    }

    /**
459
460
     * ProgressDelegate implementation for forwarding callbacks to observers of the session.
     */
461
    private fun createProgressDelegate() = object : GeckoSession.ProgressDelegate {
462
        override fun onProgressChange(session: GeckoSession, progress: Int) {
463
464
465
            notifyObservers { onProgress(progress) }
        }

466
        override fun onSecurityChange(
467
468
            session: GeckoSession,
            securityInfo: GeckoSession.ProgressDelegate.SecurityInformation
469
        ) {
470
            // Ignore initial load of about:blank (see https://github.com/mozilla-mobile/android-components/issues/403)
471
            if (initialLoad && securityInfo.origin?.startsWith(MOZ_NULL_PRINCIPAL) == true) {
472
473
474
                return
            }

475
            notifyObservers {
476
                // TODO provide full certificate info: https://github.com/mozilla-mobile/android-components/issues/5557
Kate Glazko's avatar
Kate Glazko committed
477
                onSecurityChange(securityInfo.isSecure, securityInfo.host, securityInfo.getIssuerName())
478
479
480
            }
        }

481
482
        override fun onPageStart(session: GeckoSession, url: String) {
            currentUrl = url
Grisha Kruglov's avatar
Grisha Kruglov committed
483

484
485
486
487
488
489
            notifyObservers {
                onProgress(PROGRESS_START)
                onLoadingStateChange(true)
            }
        }

490
        override fun onPageStop(session: GeckoSession, success: Boolean) {
491
492
493
            // 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.
494
495
496
            notifyObservers {
                onProgress(PROGRESS_STOP)
                onLoadingStateChange(false)
497
498
            }
        }
499
500
501
502

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

505
506
    @Suppress("ComplexMethod")
    internal fun createHistoryDelegate() = object : GeckoSession.HistoryDelegate {
507
        @SuppressWarnings("ReturnCount")
508
509
510
511
512
513
        override fun onVisited(
            session: GeckoSession,
            url: String,
            lastVisitedURL: String?,
            flags: Int
        ): GeckoResult<Boolean>? {
Grisha Kruglov's avatar
Grisha Kruglov committed
514
515
516
517
            // Don't track:
            // - private visits
            // - error pages
            // - non-top level visits (i.e. iframes).
518
519
520
521
522
523
            if (privateMode ||
                (flags and GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL) == 0 ||
                (flags and GeckoSession.HistoryDelegate.VISIT_UNRECOVERABLE_ERROR) != 0) {
                return GeckoResult.fromValue(false)
            }

524
525
526
527
528
            val isReload = lastVisitedURL?.let { it == url } ?: false

            val visitType = if (isReload) {
                VisitType.RELOAD
            } else {
529
530
531
532
533
534
535
536
537
538
539
                // 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) {
540
                    VisitType.REDIRECT_PERMANENT
541
                } else if (flags and GeckoSession.HistoryDelegate.VISIT_REDIRECT_TEMPORARY != 0) {
542
543
544
545
546
                    VisitType.REDIRECT_TEMPORARY
                } else {
                    VisitType.LINK
                }
            }
547
548
549
550
551
552
553
            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
            }
554

555
556
            val delegate = settings.historyTrackingDelegate ?: return GeckoResult.fromValue(false)

557
558
559
560
561
            // Check if the delegate wants this type of url.
            if (!delegate.shouldStoreUri(url)) {
                return GeckoResult.fromValue(false)
            }

562
            return launchGeckoResult {
563
                delegate.onVisited(url, PageVisit(visitType, redirectSource))
564
                true
565
566
567
568
569
570
571
572
573
574
575
576
577
            }
        }

        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)

578
579
580
            return launchGeckoResult {
                val visits = delegate.getVisited(urls.toList())
                visits.toBooleanArray()
581
582
583
584
            }
        }
    }

Grisha Kruglov's avatar
Grisha Kruglov committed
585
    @Suppress("ComplexMethod")
586
    internal fun createContentDelegate() = object : GeckoSession.ContentDelegate {
587
        override fun onFirstComposite(session: GeckoSession) = Unit
588

589
590
591
592
        override fun onFirstContentfulPaint(session: GeckoSession) {
            firstContentfulPaint = true
        }

593
594
595
596
        override fun onContextMenu(
            session: GeckoSession,
            screenX: Int,
            screenY: Int,
597
            element: GeckoSession.ContentDelegate.ContextElement
598
        ) {
599
            val hitResult = handleLongClick(element.srcUri, element.type, element.linkUri, element.title)
600
601
602
603
            hitResult?.let {
                notifyObservers { onLongPress(it) }
            }
        }
604

605
        override fun onCrash(session: GeckoSession) {
606
607
            stateBeforeCrash = lastSessionState

608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
            recoverGeckoSession()

            notifyObservers { onCrash() }
        }

        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

633
            geckoSession.close()
634
            createGeckoSession()
635
        }
636

637
638
639
        override fun onFullScreen(session: GeckoSession, fullScreen: Boolean) {
            notifyObservers { onFullScreenChange(fullScreen) }
        }
640
641
642
643

        override fun onExternalResponse(session: GeckoSession, response: GeckoSession.WebResponseInfo) {
            notifyObservers {
                onExternalResource(
Renan Barros's avatar
Renan Barros committed
644
645
646
                        url = response.uri,
                        contentLength = response.contentLength,
                        contentType = response.contentType,
647
                        fileName = response.filename)
648
649
650
            }
        }

651
652
653
654
655
656
657
658
659
        override fun onCloseRequest(session: GeckoSession) {
            notifyObservers {
                onWindowRequest(GeckoWindowRequest(
                        engineSession = this@GeckoEngineSession,
                        type = WindowRequest.Type.CLOSE
                    )
                )
            }
        }
660

661
        override fun onTitleChange(session: GeckoSession, title: String?) {
662
663
664
            if (!privateMode) {
                currentUrl?.let { url ->
                    settings.historyTrackingDelegate?.let { delegate ->
665
666
667
668
669
                        // NB: There's no guarantee that the title change will be processed by the
                        // delegate before the session is closed (and the corresponding coroutine
                        // job is cancelled). Observers will always be notified of the title
                        // change though.
                        launch(coroutineContext) {
670
                            delegate.onTitleChanged(url, title ?: "")
671
                        }
672
673
                    }
                }
Grisha Kruglov's avatar
Grisha Kruglov committed
674
            }
675
            notifyObservers { onTitleChange(title ?: "") }
676
        }
677
678

        override fun onFocusRequest(session: GeckoSession) = Unit
679
680

        override fun onWebAppManifest(session: GeckoSession, manifest: JSONObject) {
681
682
683
684
            val parsed = WebAppManifestParser().parse(manifest)
            if (parsed is WebAppManifestParser.Result.Success) {
                notifyObservers { onWebAppManifestLoaded(parsed.manifest) }
            }
685
        }
686
687
688
689
690
691
692
693
694
695
696
697

        override fun onMetaViewportFitChange(session: GeckoSession, viewportFit: String) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                val layoutInDisplayCutoutMode = when (viewportFit) {
                    "cover" -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
                    "contain" -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
                    else -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
                }

                notifyObservers { onMetaViewportFitChanged(layoutInDisplayCutoutMode) }
            }
        }
698
699
    }

700
701
    private fun createContentBlockingDelegate() = object : ContentBlocking.Delegate {
        override fun onContentBlocked(session: GeckoSession, event: ContentBlocking.BlockEvent) {
702
703
704
            notifyObservers {
                onTrackerBlocked(event.toTracker())
            }
705
        }
706
707
708
709
710
711

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

714
    private fun ContentBlocking.BlockEvent.toTracker(): Tracker {
715
        val blockedContentCategories = mutableListOf<TrackingProtectionPolicy.TrackingCategory>()
716

717
718
        if (antiTrackingCategory.contains(ContentBlocking.AntiTracking.AD)) {
            blockedContentCategories.add(TrackingProtectionPolicy.TrackingCategory.AD)
719
720
        }

721
722
        if (antiTrackingCategory.contains(ContentBlocking.AntiTracking.ANALYTIC)) {
            blockedContentCategories.add(TrackingProtectionPolicy.TrackingCategory.ANALYTICS)
723
724
        }

725
726
        if (antiTrackingCategory.contains(ContentBlocking.AntiTracking.SOCIAL)) {
            blockedContentCategories.add(TrackingProtectionPolicy.TrackingCategory.SOCIAL)
727
728
        }

729
730
        if (antiTrackingCategory.contains(ContentBlocking.AntiTracking.FINGERPRINTING)) {
            blockedContentCategories.add(TrackingProtectionPolicy.TrackingCategory.FINGERPRINTING)
731
732
        }

733
734
        if (antiTrackingCategory.contains(ContentBlocking.AntiTracking.CRYPTOMINING)) {
            blockedContentCategories.add(TrackingProtectionPolicy.TrackingCategory.CRYPTOMINING)
735
        }
736
737
738

        if (antiTrackingCategory.contains(ContentBlocking.AntiTracking.CONTENT)) {
            blockedContentCategories.add(TrackingProtectionPolicy.TrackingCategory.CONTENT)
739
        }
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775

        if (antiTrackingCategory.contains(ContentBlocking.AntiTracking.TEST)) {
            blockedContentCategories.add(TrackingProtectionPolicy.TrackingCategory.TEST)
        }

        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)
        }

        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)
        }

        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
776
777
    }

Kate Glazko's avatar
Kate Glazko committed
778
779
780
781
    internal fun GeckoSession.ProgressDelegate.SecurityInformation.getIssuerName(): String? {
        return certificate?.issuerDN?.name?.substringAfterLast("O=")?.substringBeforeLast(",C=")
    }

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

786
787
    private fun createPermissionDelegate() = object : GeckoSession.PermissionDelegate {
        override fun onContentPermissionRequest(
788
            session: GeckoSession,
789
790
791
792
793
794
795
796
797
            uri: String?,
            type: Int,
            callback: GeckoSession.PermissionDelegate.Callback
        ) {
            val request = GeckoPermissionRequest.Content(uri ?: "", type, callback)
            notifyObservers { onContentPermissionRequest(request) }
        }

        override fun onMediaPermissionRequest(
798
799
            session: GeckoSession,
            uri: String,
800
801
802
803
804
            video: Array<out GeckoSession.PermissionDelegate.MediaSource>?,
            audio: Array<out GeckoSession.PermissionDelegate.MediaSource>?,
            callback: GeckoSession.PermissionDelegate.MediaCallback
        ) {
            val request = GeckoPermissionRequest.Media(
805
                    uri,
806
807
808
809
810
811
812
                    video?.toList() ?: emptyList(),
                    audio?.toList() ?: emptyList(),
                    callback)
            notifyObservers { onContentPermissionRequest(request) }
        }

        override fun onAndroidPermissionsRequest(
813
            session: GeckoSession,
814
815
816
817
818
819
820
821
822
823
            permissions: Array<out String>?,
            callback: GeckoSession.PermissionDelegate.Callback
        ) {
            val request = GeckoPermissionRequest.App(
                    permissions?.toList() ?: emptyList(),
                    callback)
            notifyObservers { onAppPermissionRequest(request) }
        }
    }

Tiger Oakes's avatar
Tiger Oakes committed
824
825
826
827
828
829
    private fun createScrollDelegate() = object : GeckoSession.ScrollDelegate {
        override fun onScrollChanged(session: GeckoSession, scrollX: Int, scrollY: Int) {
            this@GeckoEngineSession.scrollY = scrollY
        }
    }

830
    @Suppress("ComplexMethod")
831
    fun handleLongClick(elementSrc: String?, elementType: Int, uri: String? = null, title: String? = null): HitResult? {
832
        return when (elementType) {
833
            GeckoSession.ContentDelegate.ContextElement.TYPE_AUDIO ->
834
                elementSrc?.let {
835
                    HitResult.AUDIO(it, title)
836
                }
837
            GeckoSession.ContentDelegate.ContextElement.TYPE_VIDEO ->
838
                elementSrc?.let {
839
                    HitResult.VIDEO(it, title)
840
                }
841
            GeckoSession.ContentDelegate.ContextElement.TYPE_IMAGE -> {
842
843
844
845
                when {
                    elementSrc != null && uri != null ->
                        HitResult.IMAGE_SRC(elementSrc, uri)
                    elementSrc != null ->
846
                        HitResult.IMAGE(elementSrc, title)
847
848
849
                    else -> HitResult.UNKNOWN("")
                }
            }
850
            GeckoSession.ContentDelegate.ContextElement.TYPE_NONE -> {
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
                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("")
        }
    }

866
    private fun createGeckoSession(shouldOpen: Boolean = true) {
867
        this.geckoSession = geckoSessionProvider()
868

869
870
871
        defaultSettings?.trackingProtectionPolicy?.let { enableTrackingProtection(it) }
        defaultSettings?.requestInterceptor?.let { settings.requestInterceptor = it }
        defaultSettings?.historyTrackingDelegate?.let { settings.historyTrackingDelegate = it }
872
873
874
        defaultSettings?.testingModeEnabled?.let { geckoSession.settings.fullAccessibilityTree = it }
        defaultSettings?.userAgentString?.let { geckoSession.settings.userAgentOverride = it }
        defaultSettings?.suspendMediaWhenInactive?.let { geckoSession.settings.suspendMediaWhenInactive = it }
875

876
877
878
        if (shouldOpen) {
            geckoSession.open(runtime)
        }
879
880
881
882

        geckoSession.navigationDelegate = createNavigationDelegate()
        geckoSession.progressDelegate = createProgressDelegate()
        geckoSession.contentDelegate = createContentDelegate()
883
        geckoSession.contentBlockingDelegate = createContentBlockingDelegate()
884
        geckoSession.permissionDelegate = createPermissionDelegate()
885
        geckoSession.promptDelegate = GeckoPromptDelegate(this)
886
        geckoSession.historyDelegate = createHistoryDelegate()
887
        geckoSession.mediaDelegate = GeckoMediaDelegate(this)
Tiger Oakes's avatar
Tiger Oakes committed
888
        geckoSession.scrollDelegate = createScrollDelegate()
889
890
    }

891
892
893
    companion object {
        internal const val PROGRESS_START = 25
        internal const val PROGRESS_STOP = 100
894
895
        internal const val MOZ_NULL_PRINCIPAL = "moz-nullprincipal:"
        internal const val ABOUT_BLANK = "about:blank"
896
897
898
899
900

        /**
         * Provides an ErrorType corresponding to the error code provided.
         */
        @Suppress("ComplexMethod")
901
        internal fun geckoErrorToErrorType(errorCode: Int) =
902
            when (errorCode) {
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
                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
929
930
                else -> ErrorType.UNKNOWN
            }
931
932
    }
}