TabSessionState.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
import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingStatus
import mozilla.components.concept.engine.EngineSessionState
import mozilla.components.concept.engine.manifest.WebAppManifest
import mozilla.components.concept.storage.HistoryMetadataKey
import java.util.UUID

/**
 * Value type that represents the state of a tab (private or normal).
 *
 * @property id the ID of this tab and session.
 * @property content the [ContentState] of this tab.
 * @property trackingProtection the [TrackingProtectionState] of this tab.
 * @property translationsState the [TranslationsState] of this tab.
 * @property cookieBanner the [CookieBannerHandlingStatus] of this tab.
 * @property parentId the parent ID of this tab or null if this tab has no
 * parent. The parent tab is usually the tab that initiated opening this
 * tab (e.g. the user clicked a link with target="_blank" or selected
 * "open in new tab" or a "window.open" was triggered).
 * @property extensionState a map of web extension ids to extensions,
 * that contains the overridden values for this tab.
 * @property readerState the [ReaderState] of this tab.
 * @property contextId the session context ID of this tab.
 * @property lastAccess The last time this tab was selected (requires LastAccessMiddleware).
 * @property createdAt Timestamp of this tab's creation.
 * @property lastMediaAccessState - [LastMediaAccessState] detailing the tab state when media started playing.
 * Requires [LastMediaAccessMiddleware] to update the value when playback starts.
 * @property restored Indicates if this page was restored from a persisted state.
 * @property originalInput If the user entered a URL, this is the original user
 * input before any fixups were applied to it.
 */
data class TabSessionState(
    override val id: String = UUID.randomUUID().toString(),
    override val content: ContentState,
    override val trackingProtection: TrackingProtectionState = TrackingProtectionState(),
    override val translationsState: TranslationsState = TranslationsState(),
    override val cookieBanner: CookieBannerHandlingStatus = CookieBannerHandlingStatus.NO_DETECTED,
    override val engineState: EngineState = EngineState(),
    override val extensionState: Map<String, WebExtensionState> = emptyMap(),
    override val mediaSessionState: MediaSessionState? = null,
    override val contextId: String? = null,
    override val source: SessionState.Source = SessionState.Source.Internal.None,
    override val restored: Boolean = false,
    override val originalInput: String? = null,
    val parentId: String? = null,
    val lastAccess: Long = 0L,
    val createdAt: Long = System.currentTimeMillis(),
    val lastMediaAccessState: LastMediaAccessState = LastMediaAccessState(),
    val readerState: ReaderState = ReaderState(),
    val historyMetadata: HistoryMetadataKey? = null,
) : SessionState {

    override fun createCopy(
        id: String,
        content: ContentState,
        trackingProtection: TrackingProtectionState,
        translationsState: TranslationsState,
        engineState: EngineState,
        extensionState: Map<String, WebExtensionState>,
        mediaSessionState: MediaSessionState?,
        contextId: String?,
        cookieBanner: CookieBannerHandlingStatus,
    ): SessionState = copy(
        id = id,
        content = content,
        trackingProtection = trackingProtection,
        translationsState = translationsState,
        engineState = engineState,
        extensionState = extensionState,
        mediaSessionState = mediaSessionState,
        contextId = contextId,
        cookieBanner = cookieBanner,
    )
}

/**
 * Convenient function for creating a tab.
 */
fun createTab(
    url: String,
    private: Boolean = false,
    id: String = UUID.randomUUID().toString(),
    parent: TabSessionState? = null,
    parentId: String? = null,
    extensions: Map<String, WebExtensionState> = emptyMap(),
    readerState: ReaderState = ReaderState(),
    title: String = "",
    contextId: String? = null,
    lastAccess: Long = 0L,
    createdAt: Long = System.currentTimeMillis(),
    lastMediaAccessState: LastMediaAccessState = LastMediaAccessState(),
    source: SessionState.Source = SessionState.Source.Internal.None,
    restored: Boolean = false,
    isProductUrl: Boolean = false,
    engineSession: EngineSession? = null,
    engineSessionState: EngineSessionState? = null,
    crashed: Boolean = false,
    mediaSessionState: MediaSessionState? = null,
    historyMetadata: HistoryMetadataKey? = null,
    webAppManifest: WebAppManifest? = null,
    searchTerms: String = "",
    initialLoadFlags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(),
    initialAdditionalHeaders: Map<String, String>? = null,
    desktopMode: Boolean = false,
    previewImageUrl: String? = null,
    hasFormData: Boolean = false,
    originalInput: String? = null,
    initialTextDirectiveUserActivation: Boolean = false,
): TabSessionState {
    return TabSessionState(
        id = id,
        content = ContentState(
            url,
            private,
            title = title,
            webAppManifest = webAppManifest,
            searchTerms = searchTerms,
            desktopMode = desktopMode,
            previewImageUrl = previewImageUrl,
            hasFormData = hasFormData,
            isProductUrl = isProductUrl,
        ),
        parentId = parentId ?: parent?.id,
        extensionState = extensions,
        readerState = readerState,
        contextId = contextId,
        lastAccess = lastAccess,
        createdAt = createdAt,
        lastMediaAccessState = lastMediaAccessState,
        source = source,
        restored = restored,
        engineState = EngineState(
            engineSession = engineSession,
            engineSessionState = engineSessionState,
            crashed = crashed,
            initialLoadFlags = initialLoadFlags,
            initialAdditionalHeaders = initialAdditionalHeaders,
            initialTextDirectiveUserActivation = initialTextDirectiveUserActivation,
        ),
        mediaSessionState = mediaSessionState,
        historyMetadata = historyMetadata,
        originalInput = originalInput,
    )
}

/**
 * Indicates if the specified tab should be considered "inactive"
 */
fun TabSessionState.isActive(maxActiveTime: Long): Boolean {
    val lastActiveTime = maxOf(lastAccess, createdAt)
    val now = System.currentTimeMillis()
    return (now - lastActiveTime <= maxActiveTime)
}