EngineSession.kt

/* 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.concept.engine

import android.content.Intent
import androidx.annotation.CallSuper
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.CookiePolicy.ACCEPT_ALL
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy.CookiePolicy.ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS
import mozilla.components.concept.engine.content.blocking.Tracker
import mozilla.components.concept.engine.history.HistoryItem
import mozilla.components.concept.engine.manifest.WebAppManifest
import mozilla.components.concept.engine.media.RecordingDevice
import mozilla.components.concept.engine.mediasession.MediaSession
import mozilla.components.concept.engine.permission.PermissionRequest
import mozilla.components.concept.engine.prompt.PromptRequest
import mozilla.components.concept.engine.translate.TranslationEngineState
import mozilla.components.concept.engine.translate.TranslationError
import mozilla.components.concept.engine.translate.TranslationOperation
import mozilla.components.concept.engine.translate.TranslationOptions
import mozilla.components.concept.engine.window.WindowRequest
import mozilla.components.concept.fetch.Response
import mozilla.components.support.base.observer.Observable
import mozilla.components.support.base.observer.ObserverRegistry
import org.json.JSONObject

/**
 * Class representing a single engine session.
 *
 * In browsers usually a session corresponds to a tab.
 */
@Suppress("TooManyFunctions")
abstract class EngineSession(
    private val delegate: Observable<Observer> = ObserverRegistry(),
) : Observable<EngineSession.Observer> by delegate, DataCleanable {
    /**
     * Interface to be implemented by classes that want to observe this engine session.
     */
    interface Observer {
        /**
         * Event to indicate the scroll position of the content has changed.
         *
         * @param scrollX The new horizontal scroll position in pixels.
         * @param scrollY The new vertical scroll position in pixels.
         */
        fun onScrollChange(scrollX: Int, scrollY: Int) = Unit

        fun onLocationChange(url: String, hasUserGesture: Boolean) = Unit
        fun onTitleChange(title: String) = Unit

        /**
         * Event to indicate a preview image URL was discovered in the content after the content loaded.
         *
         * @param previewImageUrl The preview image URL sent from the content.
         */
        fun onPreviewImageChange(previewImageUrl: String) = Unit

        fun onProgress(progress: Int) = Unit
        fun onLoadingStateChange(loading: Boolean) = Unit
        fun onNavigationStateChange(canGoBack: Boolean? = null, canGoForward: Boolean? = null) = Unit
        fun onSecurityChange(secure: Boolean, host: String? = null, issuer: String? = null) = Unit
        fun onTrackerBlockingEnabledChange(enabled: Boolean) = Unit

        /**
         * Event to indicate a new [CookieBannerHandlingStatus] is available.
         */
        fun onCookieBannerChange(status: CookieBannerHandlingStatus) = Unit
        fun onTrackerBlocked(tracker: Tracker) = Unit
        fun onTrackerLoaded(tracker: Tracker) = Unit
        fun onNavigateBack() = Unit

        /**
         * Event to indicate a product URL is currently open.
         */
        fun onProductUrlChange(isProductUrl: Boolean) = Unit

        /**
         * Event to indicate that a page change is occurring, which will invalidate the page's
         * translations state.
         */
        fun onTranslatePageChange() = Unit

        /**
         * Event to indicate that a url was loaded to this session.
         */
        fun onLoadUrl() = Unit

        /**
         * Event to indicate that the session was requested to navigate to a specified index.
         */
        fun onGotoHistoryIndex() = Unit

        /**
         * Event to indicate that the session was requested to render data.
         */
        fun onLoadData() = Unit

        /**
         * Event to indicate that the session was requested to navigate forward in history
         */
        fun onNavigateForward() = Unit

        /**
         * Event to indicate whether or not this [EngineSession] should be [excluded] from tracking protection.
         */
        fun onExcludedOnTrackingProtectionChange(excluded: Boolean) = Unit

        /**
         * Event to indicate that this session has had it's first engine contentful paint of page content.
         */
        fun onFirstContentfulPaint() = Unit

        /**
         * Event to indicate that this session has had it's paint status reset.
         */
        fun onPaintStatusReset() = Unit
        fun onLongPress(hitResult: HitResult) = Unit
        fun onDesktopModeChange(enabled: Boolean) = Unit
        fun onFind(text: String) = Unit
        fun onFindResult(activeMatchOrdinal: Int, numberOfMatches: Int, isDoneCounting: Boolean) = Unit
        fun onFullScreenChange(enabled: Boolean) = Unit

        /**
         * @param layoutInDisplayCutoutMode value of defined in https://developer.android.com/reference/android/view/WindowManager.LayoutParams#layoutInDisplayCutoutMode
         */
        fun onMetaViewportFitChanged(layoutInDisplayCutoutMode: Int) = Unit
        fun onAppPermissionRequest(permissionRequest: PermissionRequest) = permissionRequest.reject()
        fun onContentPermissionRequest(permissionRequest: PermissionRequest) = permissionRequest.reject()
        fun onCancelContentPermissionRequest(permissionRequest: PermissionRequest) = Unit
        fun onPromptRequest(promptRequest: PromptRequest) = Unit

        /**
         * The engine has requested a prompt be dismissed.
         */
        fun onPromptDismissed(promptRequest: PromptRequest) = Unit

        /**
         * The engine has requested a prompt update.
         */
        fun onPromptUpdate(previousPromptRequestUid: String, promptRequest: PromptRequest) = Unit

        /**
         * User cancelled a repost prompt. Page will not be reloaded.
         */
        fun onRepostPromptCancelled() = Unit

        /**
         * User cancelled a beforeunload prompt. Navigating to another page is cancelled.
         */
        fun onBeforeUnloadPromptDenied() = Unit

        /**
         * The engine received a request to open or close a window.
         *
         * @param windowRequest the request to describing the required window action.
         */
        fun onWindowRequest(windowRequest: WindowRequest) = Unit

        /**
         * Based on the webpage current state the toolbar should be expanded to it's full height
         * previously specified in [EngineView.setDynamicToolbarMaxHeight].
         */
        fun onShowDynamicToolbar() = Unit

        /**
         * Notify that the given media session has become active.
         *
         * @param mediaSessionController The associated [MediaSession.Controller].
         */
        fun onMediaActivated(mediaSessionController: MediaSession.Controller) = Unit

        /**
         * Notify that the given media session has become inactive.
         * Inactive media sessions can not be controlled.
         */
        fun onMediaDeactivated() = Unit

        /**
         * Notify on updated metadata.
         *
         * @param metadata The updated [MediaSession.Metadata].
         */
        fun onMediaMetadataChanged(metadata: MediaSession.Metadata) = Unit

        /**
         * Notify on updated supported features.
         *
         * @param features A combination of [MediaSession.Feature].
         */
        fun onMediaFeatureChanged(features: MediaSession.Feature) = Unit

        /**
         * Notify that playback has changed for the given media session.
         *
         * @param playbackState The updated [MediaSession.PlaybackState].
         */
        fun onMediaPlaybackStateChanged(playbackState: MediaSession.PlaybackState) = Unit

        /**
         * Notify on updated position state.
         *
         * @param positionState The updated [MediaSession.PositionState].
         */
        fun onMediaPositionStateChanged(positionState: MediaSession.PositionState) = Unit

        /**
         * Notify changed audio mute state.
         *
         * @param muted True if audio of this media session is muted.
         */
        fun onMediaMuteChanged(muted: Boolean) = Unit

        /**
         * Notify on changed fullscreen state.
         *
         * @param fullscreen True when this media session in in fullscreen mode.
         * @param elementMetadata An instance of [MediaSession.ElementMetadata], if enabled.
         */
        fun onMediaFullscreenChanged(
            fullscreen: Boolean,
            elementMetadata: MediaSession.ElementMetadata?,
        ) = Unit

        fun onWebAppManifestLoaded(manifest: WebAppManifest) = Unit
        fun onCrash() = Unit
        fun onProcessKilled() = Unit
        fun onRecordingStateChanged(devices: List<RecordingDevice>) = Unit

        /**
         * Event to indicate that a new saved [EngineSessionState] is available.
         */
        fun onStateUpdated(state: EngineSessionState) = Unit

        /**
         * The engine received a request to load a request.
         *
         * @param url The string url that was requested.
         * @param triggeredByRedirect True if and only if the request was triggered by an HTTP redirect.
         * @param triggeredByWebContent True if and only if the request was triggered from within
         * web content (as opposed to via the browser chrome).
         *
         * Unlike the name LoadRequest.isRedirect may imply this flag is not about http redirects.
         * The flag is "True if and only if the request was triggered by an HTTP redirect."
         * See: https://bugzilla.mozilla.org/show_bug.cgi?id=1545170
         */
        fun onLoadRequest(
            url: String,
            triggeredByRedirect: Boolean,
            triggeredByWebContent: Boolean,
        ) = Unit

        /**
         * The engine received a request to launch a app intent.
         *
         * @param url The string url that was requested.
         * @param appIntent The Android Intent that was requested.
         * web content (as opposed to via the browser chrome).
         */
        fun onLaunchIntentRequest(
            url: String,
            appIntent: Intent?,
        ) = Unit

        /**
         * The engine received a request to download a file.
         *
         * @param url The string url that was requested.
         * @param fileName The file name.
         * @param contentLength The size of the file to be downloaded.
         * @param contentType The type of content to be downloaded.
         * @param cookie The cookie related to request.
         * @param userAgent The user agent of the engine.
         * @param skipConfirmation Whether or not the confirmation dialog should be shown before the download begins.
         * @param openInApp Whether or not the associated resource should be opened in a third party
         * app after processed successfully.
         * @param isPrivate Indicates if the download was requested from a private session.
         * @param response A response object associated with this request, when provided can be
         * used instead of performing a manual a download.
         */
        fun onExternalResource(
            url: String,
            fileName: String? = null,
            contentLength: Long? = null,
            contentType: String? = null,
            cookie: String? = null,
            userAgent: String? = null,
            isPrivate: Boolean = false,
            skipConfirmation: Boolean = false,
            openInApp: Boolean = false,
            response: Response? = null,
        ) = Unit

        /**
         * Event to indicate that this session has changed its history state.
         *
         * @param historyList The list of items in the session history.
         * @param currentIndex Index of the current page in the history list.
         */
        fun onHistoryStateChanged(historyList: List<HistoryItem>, currentIndex: Int) = Unit

        /**
         * Event to indicate that an exception was thrown while generating a PDF.
         *
         * @param throwable The throwable from the exception.
         */
        fun onSaveToPdfException(throwable: Throwable) = Unit

        /**
         * Event to indicate that printing finished.
         */
        fun onPrintFinish() = Unit

        /**
         * Event to indicate that an exception was thrown while preparing to print or save as pdf.
         *
         * @param isPrint true for a true print error or false for a Save as PDF error.
         * @param throwable The exception throwable. Usually a GeckoPrintException.
         */
        fun onPrintException(isPrint: Boolean, throwable: Throwable) = Unit

        /**
         * Event to indicate that the PDF was successfully generated.
         */
        fun onSaveToPdfComplete() = Unit

        /**
         * Event to indicate that this session needs to be checked for form data.
         *
         * @param containsFormData Indicates if the session has form data.
         */
        fun onCheckForFormData(containsFormData: Boolean, adjustPriority: Boolean = true) = Unit

        /**
         * Event to indicate that an exception was thrown while checking for form data.
         *
         * @param throwable The throwable from the exception.
         */
        fun onCheckForFormDataException(throwable: Throwable) = Unit

        /**
         * Event to indicate that the translations engine expects that the user will likely
         * request page translation.
         *
         * The usual use case is to show a prominent translations UI entrypoint on the toolbar.
         */
        fun onTranslateExpected() = Unit

        /**
         * Event to indicate that the translations engine suggests notifying the user that
         * translations are available or else offering to translate.
         *
         * The usual use case is to show a popup or UI notification that translations are available.
         */
        fun onTranslateOffer() = Unit

        /**
         * Event to indicate the translations state. Translations state change
         * occurs generally during navigation and after translation operations are requested.
         *
         * @param state The translations state.
         */
        fun onTranslateStateChange(state: TranslationEngineState) = Unit

        /**
         * Event to indicate that the translation operation completed successfully.
         *
         * @param operation The operation that the translation engine completed.
         */
        fun onTranslateComplete(operation: TranslationOperation) = Unit

        /**
         * Event to indicate that the translation operation was unsuccessful.
         *
         * @param operation The operation that the translation engine attempted.
         * @param translationError The exception that occurred during the operation.
         */
        fun onTranslateException(
            operation: TranslationOperation,
            translationError: TranslationError,
        ) = Unit
    }

    /**
     * Provides access to the settings of this engine session.
     */
    abstract val settings: Settings

    /**
     * Represents a safe browsing policy, which is indicates with type of site should be alerted
     * to user as possible harmful.
     */
    @Suppress("MagicNumber")
    enum class SafeBrowsingPolicy(val id: Int) {
        NONE(0),

        /**
         * Blocks malware sites.
         */
        MALWARE(1 shl 10),

        /**
         * Blocks unwanted sites.
         */
        UNWANTED(1 shl 11),

        /**
         * Blocks harmful sites.
         */
        HARMFUL(1 shl 12),

        /**
         * Blocks phishing sites.
         */
        PHISHING(1 shl 13),

        /**
         * Blocks all unsafe sites.
         */
        RECOMMENDED(MALWARE.id + UNWANTED.id + HARMFUL.id + PHISHING.id),
    }

    /**
     * Represents a tracking protection policy, which is a combination of
     * tracker categories that should be blocked. Unless otherwise specified,
     * a [TrackingProtectionPolicy] is applicable to all session types (see
     * [TrackingProtectionPolicyForSessionTypes]).
     */
    open class TrackingProtectionPolicy internal constructor(
        val trackingCategories: Array<TrackingCategory> = arrayOf(TrackingCategory.RECOMMENDED),
        val useForPrivateSessions: Boolean = true,
        val useForRegularSessions: Boolean = true,
        val cookiePolicy: CookiePolicy = ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS,
        val cookiePolicyPrivateMode: CookiePolicy = cookiePolicy,
        val strictSocialTrackingProtection: Boolean? = null,
        val cookiePurging: Boolean = false,
    ) {

        /**
         * Indicates how cookies should behave for a given [TrackingProtectionPolicy].
         * The ids of each cookiePolicy is aligned with the GeckoView @CookieBehavior constants.
         */
        @Suppress("MagicNumber")
        enum class CookiePolicy(val id: Int) {
            /**
             * Accept first-party and third-party cookies and site data.
             */
            ACCEPT_ALL(0),

            /**
             * Accept only first-party cookies and site data to block cookies which are
             * not associated with the domain of the visited site.
             */
            ACCEPT_ONLY_FIRST_PARTY(1),

            /**
             * Do not store any cookies and site data.
             */
            ACCEPT_NONE(2),

            /**
             * Accept first-party and third-party cookies and site data only from
             * sites previously visited in a first-party context.
             */
            ACCEPT_VISITED(3),

            /**
             * Accept only first-party and non-tracking third-party cookies and site data
             * to block cookies which are not associated with the domain of the visited
             * site set by known trackers.
             */
            ACCEPT_NON_TRACKERS(4),

            /**
             * Enable dynamic first party isolation (dFPI); this will block third-party tracking
             * cookies in accordance with the ETP level and isolate non-tracking third-party
             * cookies.
             */
            ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS(5),
        }

        @Suppress("MagicNumber")
        enum class TrackingCategory(val id: Int) {

            NONE(0),

            /**
             * Blocks advertisement trackers from the ads-track-digest256 list.
             */
            AD(1 shl 1),

            /**
             * Blocks analytics trackers from the analytics-track-digest256 list.
             */
            ANALYTICS(1 shl 2),

            /**
             * Blocks social trackers from the social-track-digest256 list.
             */
            SOCIAL(1 shl 3),

            /**
             * Blocks content trackers from the content-track-digest256 list.
             * May cause issues with some web sites.
             */
            CONTENT(1 shl 4),

            // This policy is just to align categories with GeckoView
            TEST(1 shl 5),

            /**
             * Blocks cryptocurrency miners.
             */
            CRYPTOMINING(1 shl 6),

            /**
             * Blocks fingerprinting trackers.
             */
            FINGERPRINTING(1 shl 7),

            /**
             * Blocks social trackers from the social-tracking-protection-digest256 list.
             */
            MOZILLA_SOCIAL(1 shl 8),

            /**
             * Blocks email trackers.
             */
            EMAIL(1 shl 9),

            /**
             * Blocks content like scripts and sub-resources.
             */
            SCRIPTS_AND_SUB_RESOURCES(1 shl 31),

            RECOMMENDED(
                AD.id + ANALYTICS.id + SOCIAL.id + TEST.id + MOZILLA_SOCIAL.id +
                    CRYPTOMINING.id + FINGERPRINTING.id,
            ),

            /**
             * Combining the [RECOMMENDED] categories plus [SCRIPTS_AND_SUB_RESOURCES] & getAntiTracking[EMAIL].
             */
            STRICT(RECOMMENDED.id + SCRIPTS_AND_SUB_RESOURCES.id + EMAIL.id),
        }

        companion object {
            fun none() = TrackingProtectionPolicy(
                trackingCategories = arrayOf(TrackingCategory.NONE),
                cookiePolicy = ACCEPT_ALL,
            )

            /**
             * Strict policy.
             * Combining the [TrackingCategory.STRICT] plus a cookiePolicy of [ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS].
             * This is the strictest setting and may cause issues on some web sites.
             */
            fun strict() = TrackingProtectionPolicyForSessionTypes(
                trackingCategory = arrayOf(TrackingCategory.STRICT),
                cookiePolicy = ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS,
                strictSocialTrackingProtection = true,
                cookiePurging = true,
            )

            /**
             * Recommended policy.
             * Combining the [TrackingCategory.RECOMMENDED] plus a [CookiePolicy]
             * of [ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS].
             * This is the recommended setting.
             */
            fun recommended() = TrackingProtectionPolicyForSessionTypes(
                trackingCategory = arrayOf(TrackingCategory.RECOMMENDED),
                cookiePolicy = ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS,
                strictSocialTrackingProtection = false,
                cookiePurging = true,
            )

            /**
             *  Creates a custom [TrackingProtectionPolicyForSessionTypes] using the provide values .
             *  @param trackingCategories a list of tracking categories to apply.
             *  @param cookiePolicy indicates how cookies should behave for this policy.
             *  @param cookiePolicyPrivateMode indicates how cookies should behave in private mode for this policy,
             *  default to [cookiePolicy] if not set.
             *  @param strictSocialTrackingProtection indicate  if content should be blocked from the
             *  social-tracking-protection-digest256 list, when given a null value,
             *  it is only applied when the [EngineSession.TrackingProtectionPolicy.TrackingCategory.STRICT]
             *  is set.
             *  @param cookiePurging Whether or not to automatically purge tracking cookies. This will
             *  purge cookies from tracking sites that do not have recent user interaction provided.
             */
            fun select(
                trackingCategories: Array<TrackingCategory> = arrayOf(TrackingCategory.RECOMMENDED),
                cookiePolicy: CookiePolicy = ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS,
                cookiePolicyPrivateMode: CookiePolicy = cookiePolicy,
                strictSocialTrackingProtection: Boolean? = null,
                cookiePurging: Boolean = false,
            ) = TrackingProtectionPolicyForSessionTypes(
                trackingCategory = trackingCategories,
                cookiePolicy = cookiePolicy,
                cookiePolicyPrivateMode = cookiePolicyPrivateMode,
                strictSocialTrackingProtection = strictSocialTrackingProtection,
                cookiePurging = cookiePurging,
            )
        }

        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (other !is TrackingProtectionPolicy) return false
            if (hashCode() != other.hashCode()) return false
            if (useForPrivateSessions != other.useForPrivateSessions) return false
            if (useForRegularSessions != other.useForRegularSessions) return false
            if (cookiePurging != other.cookiePurging) return false
            if (cookiePolicyPrivateMode != other.cookiePolicyPrivateMode) return false
            if (strictSocialTrackingProtection != other.strictSocialTrackingProtection) return false
            return true
        }

        override fun hashCode() = trackingCategories.sumOf { it.id } + cookiePolicy.id

        fun contains(category: TrackingCategory) =
            (trackingCategories.sumOf { it.id } and category.id) != 0
    }

    /**
     * Represents settings options for cookie banner handling.
     */
    @Suppress("MagicNumber")
    enum class CookieBannerHandlingMode(val mode: Int) {
        /**
         * The feature is turned off and cookie banners are not handled
         */
        DISABLED(0),

        /**
         * Reject cookies if possible
         */
        REJECT_ALL(1),

        /**
         * Reject cookies if possible. If rejecting is not possible, accept cookies
         */
        REJECT_OR_ACCEPT_ALL(2),
    }

    /**
     * Represents a status for cookie banner handling.
     */
    enum class CookieBannerHandlingStatus {
        /**
         * Indicates a cookie banner was detected.
         */
        DETECTED,

        /**
         * Indicates a cookie banner was handled.
         */
        HANDLED,

        /**
         * Indicates a cookie banner has not been detected yet.
         */
        NO_DETECTED,
    }

    /**
     * Subtype of [TrackingProtectionPolicy] to control the type of session this policy
     * should be applied to. By default, a policy will be applied to all sessions.
     *  @param trackingCategory a list of tracking categories to apply.
     *  @param cookiePolicy indicates how cookies should behave for this policy.
     *  @param cookiePolicyPrivateMode indicates how cookies should behave in private mode for this policy,
     *  default to [cookiePolicy] if not set.
     *  @param strictSocialTrackingProtection indicate  if content should be blocked from the
     *  social-tracking-protection-digest256 list, when given a null value,
     *  it is only applied when the [EngineSession.TrackingProtectionPolicy.TrackingCategory.STRICT]
     *  is set.
     *  @param cookiePurging Whether or not to automatically purge tracking cookies. This will
     *  purge cookies from tracking sites that do not have recent user interaction provided.
     */
    class TrackingProtectionPolicyForSessionTypes internal constructor(
        trackingCategory: Array<TrackingCategory> = arrayOf(TrackingCategory.RECOMMENDED),
        cookiePolicy: CookiePolicy = ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS,
        cookiePolicyPrivateMode: CookiePolicy = cookiePolicy,
        strictSocialTrackingProtection: Boolean? = null,
        cookiePurging: Boolean = false,
    ) : TrackingProtectionPolicy(
        trackingCategories = trackingCategory,
        cookiePolicy = cookiePolicy,
        cookiePolicyPrivateMode = cookiePolicyPrivateMode,
        strictSocialTrackingProtection = strictSocialTrackingProtection,
        cookiePurging = cookiePurging,
    ) {
        /**
         * Marks this policy to be used for private sessions only.
         */
        fun forPrivateSessionsOnly() = TrackingProtectionPolicy(
            trackingCategories = trackingCategories,
            useForPrivateSessions = true,
            useForRegularSessions = false,
            cookiePolicy = cookiePolicy,
            cookiePolicyPrivateMode = cookiePolicyPrivateMode,
            strictSocialTrackingProtection = false,
            cookiePurging = cookiePurging,
        )

        /**
         * Marks this policy to be used for regular (non-private) sessions only.
         */
        fun forRegularSessionsOnly() = TrackingProtectionPolicy(
            trackingCategories = trackingCategories,
            useForPrivateSessions = false,
            useForRegularSessions = true,
            cookiePolicy = cookiePolicy,
            cookiePolicyPrivateMode = cookiePolicyPrivateMode,
            strictSocialTrackingProtection = strictSocialTrackingProtection,
            cookiePurging = cookiePurging,
        )
    }

    /**
     * Describes a combination of flags provided to the engine when loading a URL.
     */
    class LoadUrlFlags internal constructor(val value: Int) {
        companion object {
            const val NONE: Int = 0
            const val BYPASS_CACHE: Int = 1 shl 0
            const val BYPASS_PROXY: Int = 1 shl 1
            const val EXTERNAL: Int = 1 shl 2
            const val ALLOW_POPUPS: Int = 1 shl 3
            const val BYPASS_CLASSIFIER: Int = 1 shl 4
            const val LOAD_FLAGS_FORCE_ALLOW_DATA_URI: Int = 1 shl 5
            const val LOAD_FLAGS_REPLACE_HISTORY: Int = 1 shl 6
            const val LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE: Int = 1 shl 7
            const val ALLOW_ADDITIONAL_HEADERS: Int = 1 shl 15
            const val ALLOW_JAVASCRIPT_URL: Int = 1 shl 16
            internal const val ALL = BYPASS_CACHE + BYPASS_PROXY + EXTERNAL + ALLOW_POPUPS +
                BYPASS_CLASSIFIER + LOAD_FLAGS_FORCE_ALLOW_DATA_URI + LOAD_FLAGS_REPLACE_HISTORY +
                LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE + ALLOW_ADDITIONAL_HEADERS + ALLOW_JAVASCRIPT_URL

            fun all() = LoadUrlFlags(ALL)
            fun none() = LoadUrlFlags(NONE)
            fun external() = LoadUrlFlags(EXTERNAL)
            fun select(vararg types: Int) = LoadUrlFlags(types.sum())
        }

        fun contains(flag: Int) = (value and flag) != 0

        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (other !is LoadUrlFlags) return false
            if (value != other.value) return false
            return true
        }

        override fun hashCode() = value
    }

    /**
     * Represents a session priority, which signals to the engine that it should give
     * a different prioritization to a given session.
     */
    @Suppress("MagicNumber")
    enum class SessionPriority(val id: Int) {
        /**
         * Signals to the engine that this session has a default priority.
         */
        DEFAULT(0),

        /**
         * Signals to the engine that this session is important, and the Engine should keep
         * the session alive for as long as possible.
         */
        HIGH(1),
    }

    /**
     * Loads the given URL.
     *
     * @param url the url to load.
     * @param parent the parent (referring) [EngineSession] i.e. the session that
     * triggered creating this one.
     * @param flags the [LoadUrlFlags] to use when loading the provided url.
     * @param additionalHeaders the extra headers to use when loading the provided url.
     * @param originalInput If the user entered a URL, this is the original
     * user input before any fixups were applied to it.
     * @param textDirectiveUserActivation whether loading allows the scroll by text fragmentation.
     */
    abstract fun loadUrl(
        url: String,
        parent: EngineSession? = null,
        flags: LoadUrlFlags = LoadUrlFlags.none(),
        additionalHeaders: Map<String, String>? = null,
        originalInput: String? = null,
        textDirectiveUserActivation: Boolean = false,
    )

    /**
     * Loads the data with the given mimeType.
     * Example:
     * ```
     * engineSession.loadData("<html><body>Example HTML content here</body></html>", "text/html")
     * ```
     *
     * If the data is base64 encoded, you can override the default encoding (UTF-8) with 'base64'.
     * Example:
     * ```
     * engineSession.loadData("ahr0cdovl21vemlsbgeub3jn==", "text/plain", "base64")
     * ```
     *
     * @param data The data that should be rendering.
     * @param mimeType the data type needed by the engine to know how to render it.
     * @param encoding specifies whether the data is base64 encoded; use 'base64' else defaults to "UTF-8".
     */
    abstract fun loadData(data: String, mimeType: String = "text/html", encoding: String = "UTF-8")

    /**
     * Requests the [EngineSession] to download the current session's contents as a PDF.
     *
     * A typical implementation would have the same flow that feeds into [EngineSession.Observer.onExternalResource].
     */
    abstract fun requestPdfToDownload()

    /**
     * Requests the [EngineSession] to print the current session's contents.
     *
     * This will open the Android Print Spooler.
     */
    abstract fun requestPrintContent()

    /**
     * Stops loading the current session.
     */
    abstract fun stopLoading()

    /**
     * Reloads the current URL.
     *
     * @param flags the [LoadUrlFlags] to use when reloading the current url.
     */
    abstract fun reload(flags: LoadUrlFlags = LoadUrlFlags.none())

    /**
     * Navigates back in the history of this session.
     *
     * @param userInteraction informs the engine whether the action was user invoked.
     */
    abstract fun goBack(userInteraction: Boolean = true)

    /**
     * Navigates forward in the history of this session.
     *
     * @param userInteraction informs the engine whether the action was user invoked.
     */
    abstract fun goForward(userInteraction: Boolean = true)

    /**
     * Navigates to the specified index in the [HistoryState] of this session. The current index of
     * this session's [HistoryState] will be updated but the items within it will be unchanged.
     * Invalid index values are ignored.
     *
     * @param index the index of the session's [HistoryState] to navigate to
     */
    abstract fun goToHistoryIndex(index: Int)

    /**
     * Restore a saved state; only data that is saved (history, scroll position, zoom, and form data)
     * will be restored.
     *
     * @param state A saved session state.
     * @return true if the engine session has successfully been restored with the provided state,
     * false otherwise.
     */
    abstract fun restoreState(state: EngineSessionState): Boolean

    /**
     * Updates the tracking protection [policy] for this engine session.
     * If you want to disable tracking protection use [TrackingProtectionPolicy.none].
     *
     * @param policy the tracking protection policy to use, defaults to blocking all trackers.
     */
    abstract fun updateTrackingProtection(policy: TrackingProtectionPolicy = TrackingProtectionPolicy.strict())

    /**
     * Enables/disables Desktop Mode with an optional ability to reload the session right after.
     */
    abstract fun toggleDesktopMode(enable: Boolean, reload: Boolean = false)

    /**
     * Checks if there is a rule for handling a cookie banner for the current website in the session.
     *
     * @param onSuccess callback invoked if the engine API returned a valid response. Please note
     * that the response can be null - which can indicate a bug, a miscommunication
     * or other unexpected failure.
     * @param onError callback invoked if there was an error getting the response.
     */
    abstract fun hasCookieBannerRuleForSession(onResult: (Boolean) -> Unit, onException: (Throwable) -> Unit)

    /**
     * Checks if the current session is using a PDF viewer.
     *
     * @param onSuccess callback invoked if the engine API returned a valid response. Please note
     * that the response can be null - which can indicate a bug, a miscommunication
     * or other unexpected failure.
     * @param onError callback invoked if there was an error getting the response.
     */
    abstract fun checkForPdfViewer(onResult: (Boolean) -> Unit, onException: (Throwable) -> Unit)

    /**
     * Gets the web compat info.
     *
     * @param onResult callback invoked if the engine API returned a valid response.
     * @param onException callback invoked if there was an error getting the response.
     */
    abstract fun getWebCompatInfo(onResult: (JSONObject) -> Unit, onException: (Throwable) -> Unit)

    /**
     * Requests the [EngineSession] to translate the current session's contents.
     *
     * @param fromLanguage The BCP 47 language tag that the page should be translated from.
     * @param toLanguage The BCP 47 language tag that the page should be translated to.
     * @param options Options for how the translation should be processed.
     */
    abstract fun requestTranslate(
        fromLanguage: String,
        toLanguage: String,
        options: TranslationOptions?,
    )

    /**
     * Requests the [EngineSession] to restore the current session's contents.
     * Will be a no-op on the Gecko side if the page is not translated.
     */
    abstract fun requestTranslationRestore()

    /**
     * Requests the [EngineSession] retrieve the current site's never translate preference.
     */
    abstract fun getNeverTranslateSiteSetting(
        onResult: (Boolean) -> Unit,
        onException: (Throwable) -> Unit,
    )

    /**
     * Requests the [EngineSession] to set the current site's never translate preference.
     *
     * @param setting True if the site should never be translated. False if the site should be
     * translated.
     */
    abstract fun setNeverTranslateSiteSetting(
        setting: Boolean,
        onResult: () -> Unit,
        onException: (Throwable) -> Unit,
    )

    /**
     * Finds and highlights all occurrences of the provided String and highlights them asynchronously.
     *
     * @param text the String to search for
     */
    abstract fun findAll(text: String)

    /**
     * Finds and highlights the next or previous match found by [findAll].
     *
     * @param forward true if the next match should be highlighted, false for
     * the previous match.
     */
    abstract fun findNext(forward: Boolean)

    /**
     * Clears the highlighted results of previous calls to [findAll] / [findNext].
     */
    abstract fun clearFindMatches()

    /**
     * Exits fullscreen mode if currently in it that state.
     */
    abstract fun exitFullScreenMode()

    /**
     * Marks this session active/inactive for web extensions to support
     * tabs.query({active: true}).
     *
     * @param active whether this session should be marked as active or inactive.
     */
    open fun markActiveForWebExtensions(active: Boolean) = Unit

    /**
     * Updates the priority for this session.
     *
     * @param priority the new priority for this session.
     */
    open fun updateSessionPriority(priority: SessionPriority) = Unit

    /**
     * Checks this session for existing user form data.
     */
    open fun checkForFormData(adjustPriority: Boolean = true) = Unit

    /**
     * Purges the history for the session (back and forward history).
     */
    abstract fun purgeHistory()

    /**
     * Close the session. This may free underlying objects. Call this when you are finished using
     * this session.
     */
    @CallSuper
    open fun close() = delegate.unregisterObservers()

    /**
     * Returns the list of URL schemes that are blocked from loading.
     */
    open fun getBlockedSchemes(): List<String> = emptyList()

    /**
     * Set the display member in Web App Manifest for this session.
     *
     * @param displayMode the display mode value for this session.
     */
    open fun setDisplayMode(displayMode: WebAppManifest.DisplayMode) = Unit
}