SessionState.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.browser.state.state

import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingStatus
import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_CATEGORY
import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_PACKAGE
import mozilla.components.support.utils.SafeIntent

/**
 * Interface for states that contain a [ContentState] and can be accessed via an [id].
 *
 * @property id the unique id of the session.
 * @property content the [ContentState] of this session.
 * @property trackingProtection the [TrackingProtectionState] of this session.
 * @property translationsState the [TranslationsState] of this session.
 * @property cookieBanner Indicates the state of cookie banner for this session.
 * @property engineState the [EngineState] of this session.
 * @property extensionState a map of extension id and web extension states
 * specific to this [SessionState].
 * @property mediaSessionState the [MediaSessionState] of this session.
 * @property contextId the session context ID of the session. The session context ID specifies the
 * contextual identity to use for the session's cookie store.
 * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Work_with_contextual_identities
 * @property restored Indicates if this session was restored from a hydrated state.
 * @property originalInput If the user entered a URL, this is the original user
 * input before any fixups were applied to it.
 */
interface SessionState {
    val id: String
    val content: ContentState
    val trackingProtection: TrackingProtectionState
    val translationsState: TranslationsState
    val cookieBanner: CookieBannerHandlingStatus
    val engineState: EngineState
    val extensionState: Map<String, WebExtensionState>
    val mediaSessionState: MediaSessionState?
    val contextId: String?
    val source: Source
    val restored: Boolean
    val originalInput: String?

    /**
     * Copy the class and override some parameters.
     */
    fun createCopy(
        id: String = this.id,
        content: ContentState = this.content,
        trackingProtection: TrackingProtectionState = this.trackingProtection,
        translationsState: TranslationsState = this.translationsState,
        engineState: EngineState = this.engineState,
        extensionState: Map<String, WebExtensionState> = this.extensionState,
        mediaSessionState: MediaSessionState? = this.mediaSessionState,
        contextId: String? = this.contextId,
        cookieBanner: CookieBannerHandlingStatus = this.cookieBanner,
    ): SessionState

    /**
     * Represents the origin of a session to describe how and why it was created.
     * @param id A unique identifier, exists for serialization purposes.
     */
    @Suppress("MagicNumber")
    sealed class Source(val id: Int) {
        companion object {
            /**
             * Initializes a [Source] of a correct type from its component properties.
             * Intended use is for restoring persisted state.
             */
            fun restore(sourceId: Int?, packageId: String?, packageCategory: Int?): Source {
                val caller = if (packageId != null) {
                    ExternalPackage(packageId, PackageCategory.fromInt(packageCategory))
                } else {
                    null
                }
                return when (sourceId) {
                    1 -> External.ActionSend(caller)
                    2 -> External.ActionView(caller)
                    3 -> External.ActionSearch(caller)
                    4 -> External.CustomTab(caller)
                    5 -> Internal.HomeScreen
                    6 -> Internal.Menu
                    7 -> Internal.NewTab
                    8 -> Internal.None
                    9 -> Internal.TextSelection
                    10 -> Internal.UserEntered
                    11 -> Internal.CustomTab
                    // Silently handle abnormalities (like invalid or null sourceId).
                    else -> Internal.None
                }
            }
        }

        /**
         * Describes sessions of external origins, i.e. from outside of the application.
         */
        sealed class External(id: Int, open val caller: ExternalPackage?) : Source(id) {
            /**
             * Created to handle an ACTION_SEND (share) intent.
             */
            data class ActionSend(override val caller: ExternalPackage?) : External(1, caller)

            /**
             * Created to handle an ACTION_VIEW intent.
             */
            data class ActionView(override val caller: ExternalPackage?) : External(2, caller)

            /**
             * Created to handle an ACTION_SEARCH and ACTION_WEB_SEARCH intent.
             */
            data class ActionSearch(override val caller: ExternalPackage?) : External(3, caller)

            /**
             * Created to handle a CustomTabs intent of external origin.
             */
            data class CustomTab(override val caller: ExternalPackage?) : External(4, caller)
        }

        /**
         * Describes sessions of internal origin, i.e. from within of the application.
         */
        sealed class Internal(id: Int) : Source(id) {
            /**
             * User interacted with the home screen.
             */
            object HomeScreen : Internal(5)

            /**
             * User interacted with a menu.
             */
            object Menu : Internal(6)

            /**
             * User opened a new tab.
             */
            object NewTab : Internal(7)

            /**
             * Default value and for testing purposes.
             */
            object None : Internal(8)

            /**
             * Default value and for testing purposes.
             */
            object TextSelection : Internal(9)

            /**
             * User entered a URL or search term.
             */
            object UserEntered : Internal(10)

            /**
             * Created to handle a CustomTabs intent of internal origin.
             */
            object CustomTab : Internal(11)
        }
    }
}

/**
 * Describes a category of an external package.
 */
@Suppress("MagicNumber")
enum class PackageCategory(val id: Int) {
    UNKNOWN(-1),
    GAME(0),
    AUDIO(1),
    VIDEO(2),
    IMAGE(3),
    SOCIAL(4),
    NEWS(5),
    MAPS(6),
    PRODUCTIVITY(7),
    ;

    companion object {
        /**
         * Maps an int category (as it can be obtained from a package manager) to our internal representation.
         */
        fun fromInt(id: Int?): PackageCategory = values().find { category -> category.id == id } ?: UNKNOWN
    }
}

/**
 * Describes an external package.
 * @param packageId An Android package id.
 * @param category A [PackageCategory] as defined by the application.
 */
data class ExternalPackage(val packageId: String, val category: PackageCategory)

/**
 * Produces an [ExternalPackage] based on extras present in this intent.
 */
fun SafeIntent.externalPackage(): ExternalPackage? {
    val referrerPackage = this.getStringExtra(EXTRA_ACTIVITY_REFERRER_PACKAGE)
    val referrerCategory = this.getIntExtra(EXTRA_ACTIVITY_REFERRER_CATEGORY, -1)
    return if (referrerPackage != null) {
        ExternalPackage(referrerPackage, PackageCategory.fromInt(referrerCategory))
    } else {
        null
    }
}