EngineObserver.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.engine

import android.content.Intent
import android.os.Environment
import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.CookieBannerAction
import mozilla.components.browser.state.action.CrashAction
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.action.MediaSessionAction
import mozilla.components.browser.state.action.ReaderAction
import mozilla.components.browser.state.action.TrackingProtectionAction
import mozilla.components.browser.state.action.TranslationsAction
import mozilla.components.browser.state.selector.findTabOrCustomTab
import mozilla.components.browser.state.state.AppIntentState
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.LoadRequestState
import mozilla.components.browser.state.state.SecurityInfoState
import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.browser.state.state.content.DownloadState.Status.INITIATED
import mozilla.components.browser.state.state.content.FindResultState
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineSessionState
import mozilla.components.concept.engine.HitResult
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.window.WindowRequest
import mozilla.components.concept.fetch.Response
import mozilla.components.lib.state.Store

/**
 * [EngineSession.Observer] implementation responsible to update the state of a [Session] from the events coming out of
 * an [EngineSession].
 */
@Suppress("TooManyFunctions", "LargeClass")
internal class EngineObserver(
    private val tabId: String,
    private val store: Store<BrowserState, BrowserAction>,
) : EngineSession.Observer {

    override fun onScrollChange(scrollX: Int, scrollY: Int) {
        store.dispatch(ReaderAction.UpdateReaderScrollYAction(tabId, scrollY))
    }

    override fun onNavigateBack() {
        store.dispatch(ContentAction.UpdateSearchTermsAction(tabId, ""))
    }

    override fun onNavigateForward() {
        store.dispatch(ContentAction.UpdateSearchTermsAction(tabId, ""))
    }

    override fun onGotoHistoryIndex() {
        store.dispatch(ContentAction.UpdateSearchTermsAction(tabId, ""))
    }

    override fun onLoadData() {
        store.dispatch(ContentAction.UpdateSearchTermsAction(tabId, ""))
    }

    override fun onLoadUrl() {
        if (store.state.findTabOrCustomTab(tabId)?.content?.isSearch == true) {
            store.dispatch(ContentAction.UpdateIsSearchAction(tabId, false))
        } else {
            store.dispatch(ContentAction.UpdateSearchTermsAction(tabId, ""))
        }
    }

    override fun onFirstContentfulPaint() {
        store.dispatch(ContentAction.UpdateFirstContentfulPaintStateAction(tabId, true))
    }

    override fun onPaintStatusReset() {
        store.dispatch(ContentAction.UpdateFirstContentfulPaintStateAction(tabId, false))
    }

    override fun onLocationChange(url: String, hasUserGesture: Boolean) {
        store.dispatch(ContentAction.UpdateUrlAction(tabId, url, hasUserGesture))
    }

    @Suppress("DEPRECATION") // Session observable is deprecated
    override fun onLoadRequest(
        url: String,
        triggeredByRedirect: Boolean,
        triggeredByWebContent: Boolean,
    ) {
        if (triggeredByWebContent) {
            store.dispatch(ContentAction.UpdateSearchTermsAction(tabId, ""))
        }

        val loadRequest = LoadRequestState(url, triggeredByRedirect, triggeredByWebContent)
        store.dispatch(ContentAction.UpdateLoadRequestAction(tabId, loadRequest))
    }

    override fun onLaunchIntentRequest(url: String, appIntent: Intent?) {
        store.dispatch(ContentAction.UpdateAppIntentAction(tabId, AppIntentState(url, appIntent)))
    }

    override fun onTitleChange(title: String) {
        store.dispatch(ContentAction.UpdateTitleAction(tabId, title))
    }

    override fun onPreviewImageChange(previewImageUrl: String) {
        store.dispatch(ContentAction.UpdatePreviewImageAction(tabId, previewImageUrl))
    }

    override fun onProgress(progress: Int) {
        store.dispatch(ContentAction.UpdateProgressAction(tabId, progress))
    }

    override fun onLoadingStateChange(loading: Boolean) {
        store.dispatch(ContentAction.UpdateLoadingStateAction(tabId, loading))

        if (loading) {
            store.dispatch(ContentAction.ClearFindResultsAction(tabId))
            store.dispatch(ContentAction.UpdateRefreshCanceledStateAction(tabId, false))
            store.dispatch(TrackingProtectionAction.ClearTrackersAction(tabId))
        }
    }

    override fun onNavigationStateChange(canGoBack: Boolean?, canGoForward: Boolean?) {
        canGoBack?.let {
            store.dispatch(ContentAction.UpdateBackNavigationStateAction(tabId, canGoBack))
        }
        canGoForward?.let {
            store.dispatch(ContentAction.UpdateForwardNavigationStateAction(tabId, canGoForward))
        }
    }

    override fun onSecurityChange(secure: Boolean, host: String?, issuer: String?) {
        store.dispatch(
            ContentAction.UpdateSecurityInfoAction(
                tabId,
                SecurityInfoState(secure, host ?: "", issuer ?: ""),
            ),
        )
    }

    override fun onTrackerBlocked(tracker: Tracker) {
        store.dispatch(TrackingProtectionAction.TrackerBlockedAction(tabId, tracker))
    }

    override fun onTrackerLoaded(tracker: Tracker) {
        store.dispatch(TrackingProtectionAction.TrackerLoadedAction(tabId, tracker))
    }

    override fun onExcludedOnTrackingProtectionChange(excluded: Boolean) {
        store.dispatch(TrackingProtectionAction.ToggleExclusionListAction(tabId, excluded))
    }

    override fun onTrackerBlockingEnabledChange(enabled: Boolean) {
        store.dispatch(TrackingProtectionAction.ToggleAction(tabId, enabled))
    }

    override fun onCookieBannerChange(status: EngineSession.CookieBannerHandlingStatus) {
        store.dispatch(CookieBannerAction.UpdateStatusAction(tabId, status))
    }

    override fun onProductUrlChange(isProductUrl: Boolean) {
        store.dispatch(ContentAction.UpdateProductUrlStateAction(tabId, isProductUrl))
    }

    override fun onTranslatePageChange() {
        store.dispatch(TranslationsAction.SetTranslateProcessingAction(tabId, isProcessing = false))
    }

    override fun onLongPress(hitResult: HitResult) {
        store.dispatch(
            ContentAction.UpdateHitResultAction(tabId, hitResult),
        )
    }

    override fun onFind(text: String) {
        store.dispatch(ContentAction.ClearFindResultsAction(tabId))
    }

    override fun onFindResult(activeMatchOrdinal: Int, numberOfMatches: Int, isDoneCounting: Boolean) {
        store.dispatch(
            ContentAction.AddFindResultAction(
                tabId,
                FindResultState(
                    activeMatchOrdinal,
                    numberOfMatches,
                    isDoneCounting,
                ),
            ),
        )
    }

    override fun onExternalResource(
        url: String,
        fileName: String?,
        contentLength: Long?,
        contentType: String?,
        cookie: String?,
        userAgent: String?,
        isPrivate: Boolean,
        skipConfirmation: Boolean,
        openInApp: Boolean,
        response: Response?,
    ) {
        // We want to avoid negative contentLength values
        // For more info see https://bugzilla.mozilla.org/show_bug.cgi?id=1632594
        val fileSize = if (contentLength != null && contentLength < 0) null else contentLength
        val download = DownloadState(
            url,
            fileName,
            contentType,
            fileSize,
            0,
            INITIATED,
            userAgent,
            Environment.DIRECTORY_DOWNLOADS,
            private = isPrivate,
            skipConfirmation = skipConfirmation,
            openInApp = openInApp,
            response = response,
        )

        store.dispatch(
            ContentAction.UpdateDownloadAction(
                tabId,
                download,
            ),
        )
    }

    override fun onDesktopModeChange(enabled: Boolean) {
        store.dispatch(
            ContentAction.UpdateTabDesktopMode(
                tabId,
                enabled,
            ),
        )
    }

    override fun onFullScreenChange(enabled: Boolean) {
        store.dispatch(
            ContentAction.FullScreenChangedAction(
                tabId,
                enabled,
            ),
        )
    }

    override fun onMetaViewportFitChanged(layoutInDisplayCutoutMode: Int) {
        store.dispatch(
            ContentAction.ViewportFitChangedAction(
                tabId,
                layoutInDisplayCutoutMode,
            ),
        )
    }

    override fun onContentPermissionRequest(permissionRequest: PermissionRequest) {
        store.dispatch(
            ContentAction.UpdatePermissionsRequest(
                tabId,
                permissionRequest,
            ),
        )
    }

    override fun onCancelContentPermissionRequest(permissionRequest: PermissionRequest) {
        store.dispatch(
            ContentAction.ConsumePermissionsRequest(
                tabId,
                permissionRequest,
            ),
        )
    }

    override fun onAppPermissionRequest(permissionRequest: PermissionRequest) {
        store.dispatch(
            ContentAction.UpdateAppPermissionsRequest(
                tabId,
                permissionRequest,
            ),
        )
    }

    override fun onPromptRequest(promptRequest: PromptRequest) {
        store.dispatch(
            ContentAction.UpdatePromptRequestAction(
                tabId,
                promptRequest,
            ),
        )
    }

    override fun onPromptDismissed(promptRequest: PromptRequest) {
        store.dispatch(
            ContentAction.ConsumePromptRequestAction(tabId, promptRequest),
        )
    }

    override fun onPromptUpdate(previousPromptRequestUid: String, promptRequest: PromptRequest) {
        store.dispatch(
            ContentAction.ReplacePromptRequestAction(tabId, previousPromptRequestUid, promptRequest),
        )
    }

    override fun onRepostPromptCancelled() {
        store.dispatch(ContentAction.UpdateRefreshCanceledStateAction(tabId, true))
    }

    override fun onBeforeUnloadPromptDenied() {
        store.dispatch(ContentAction.UpdateRefreshCanceledStateAction(tabId, true))
    }

    override fun onWindowRequest(windowRequest: WindowRequest) {
        store.dispatch(
            ContentAction.UpdateWindowRequestAction(
                tabId,
                windowRequest,
            ),
        )
    }

    override fun onShowDynamicToolbar() {
        store.dispatch(
            ContentAction.UpdateExpandedToolbarStateAction(tabId, true),
        )
    }

    override fun onMediaActivated(mediaSessionController: MediaSession.Controller) {
        store.dispatch(
            MediaSessionAction.ActivatedMediaSessionAction(
                tabId,
                mediaSessionController,
            ),
        )
    }

    override fun onMediaDeactivated() {
        store.dispatch(MediaSessionAction.DeactivatedMediaSessionAction(tabId))
    }

    override fun onMediaMetadataChanged(metadata: MediaSession.Metadata) {
        store.dispatch(MediaSessionAction.UpdateMediaMetadataAction(tabId, metadata))
    }

    override fun onMediaPlaybackStateChanged(playbackState: MediaSession.PlaybackState) {
        store.dispatch(
            MediaSessionAction.UpdateMediaPlaybackStateAction(
                tabId,
                playbackState,
            ),
        )
    }

    override fun onMediaFeatureChanged(features: MediaSession.Feature) {
        store.dispatch(
            MediaSessionAction.UpdateMediaFeatureAction(
                tabId,
                features,
            ),
        )
    }

    override fun onMediaPositionStateChanged(positionState: MediaSession.PositionState) {
        store.dispatch(
            MediaSessionAction.UpdateMediaPositionStateAction(
                tabId,
                positionState,
            ),
        )
    }

    override fun onMediaMuteChanged(muted: Boolean) {
        store.dispatch(
            MediaSessionAction.UpdateMediaMutedAction(
                tabId,
                muted,
            ),
        )
    }

    override fun onMediaFullscreenChanged(
        fullscreen: Boolean,
        elementMetadata: MediaSession.ElementMetadata?,
    ) {
        store.dispatch(
            MediaSessionAction.UpdateMediaFullscreenAction(
                tabId,
                fullscreen,
                elementMetadata,
            ),
        )
    }

    override fun onWebAppManifestLoaded(manifest: WebAppManifest) {
        store.dispatch(ContentAction.UpdateWebAppManifestAction(tabId, manifest))
    }

    override fun onCrash() {
        store.dispatch(
            CrashAction.SessionCrashedAction(
                tabId,
            ),
        )
    }

    override fun onProcessKilled() {
        store.dispatch(
            EngineAction.KillEngineSessionAction(
                tabId,
            ),
        )
    }

    override fun onStateUpdated(state: EngineSessionState) {
        store.dispatch(
            EngineAction.UpdateEngineSessionStateAction(
                tabId,
                state,
            ),
        )
    }

    override fun onRecordingStateChanged(devices: List<RecordingDevice>) {
        store.dispatch(
            ContentAction.SetRecordingDevices(
                tabId,
                devices,
            ),
        )
    }

    override fun onHistoryStateChanged(historyList: List<HistoryItem>, currentIndex: Int) {
        store.dispatch(
            ContentAction.UpdateHistoryStateAction(
                tabId,
                historyList,
                currentIndex,
            ),
        )
    }

    override fun onSaveToPdfException(throwable: Throwable) {
        store.dispatch(EngineAction.SaveToPdfExceptionAction(tabId, throwable))
    }

    override fun onPrintFinish() {
        store.dispatch(EngineAction.PrintContentCompletedAction(tabId))
    }

    override fun onPrintException(isPrint: Boolean, throwable: Throwable) {
        store.dispatch(EngineAction.PrintContentExceptionAction(tabId, isPrint, throwable))
    }

    override fun onSaveToPdfComplete() {
        store.dispatch(EngineAction.SaveToPdfCompleteAction(tabId))
    }

    override fun onCheckForFormData(containsFormData: Boolean, adjustPriority: Boolean) {
        store.dispatch(ContentAction.UpdateHasFormDataAction(tabId, containsFormData, adjustPriority))
    }

    override fun onCheckForFormDataException(throwable: Throwable) {
        store.dispatch(ContentAction.CheckForFormDataExceptionAction(tabId, throwable))
    }

    override fun onTranslateExpected() {
        store.dispatch(TranslationsAction.TranslateExpectedAction(tabId))
    }

    override fun onTranslateOffer() {
        store.dispatch(TranslationsAction.TranslateOfferAction(tabId = tabId, isOfferTranslate = true))
    }

    override fun onTranslateStateChange(state: TranslationEngineState) {
        store.dispatch(TranslationsAction.TranslateStateChangeAction(tabId, state))
    }

    override fun onTranslateComplete(operation: TranslationOperation) {
        store.dispatch(TranslationsAction.TranslateSuccessAction(tabId, operation))
    }

    override fun onTranslateException(operation: TranslationOperation, translationError: TranslationError) {
        store.dispatch(TranslationsAction.TranslateExceptionAction(tabId, operation, translationError))
    }
}