PdfStateMiddleware.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.middleware

import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext

private const val PAGE_FULLY_LOADED_PROGRESS = 100

/**
 * Middleware that checks if the current page is a PDF and dispatches [ContentAction.EnteredPdfViewer]
 * with the result.
 *
 * @param scope [CoroutineScope] used for long running tasks.
 */
internal class PdfStateMiddleware(
    private val scope: CoroutineScope,
) : Middleware<BrowserState, BrowserAction> {
    override fun invoke(
        context: MiddlewareContext<BrowserState, BrowserAction>,
        next: (BrowserAction) -> Unit,
        action: BrowserAction,
    ) {
        next(action)

        if (action is ContentAction.UpdateProgressAction && action.progress == PAGE_FULLY_LOADED_PROGRESS) {
            scope.launch {
                val newPdfRenderingStatus = isRenderingPdf(action.sessionId, context.state)
                val previousRenderingStatus = previousPdfRenderingStatus(action.sessionId, context.state)

                if (newPdfRenderingStatus != previousRenderingStatus) {
                    dispatchPdfStatusUpdate(action.sessionId, newPdfRenderingStatus, context)
                }
            }
        }
    }

    private fun dispatchPdfStatusUpdate(
        sessionId: String,
        isPdf: Boolean,
        context: MiddlewareContext<BrowserState, BrowserAction>,
    ) = context.store.dispatch(
        when (isPdf) {
            true -> ContentAction.EnteredPdfViewer(sessionId)
            false -> ContentAction.ExitedPdfViewer(sessionId)
        },
    )

    private fun previousPdfRenderingStatus(sessionId: String, state: BrowserState): Boolean {
        return state.findTabOrCustomTabOrSelectedTab(sessionId)?.content?.isPdf ?: false
    }

    private suspend fun isRenderingPdf(sessionId: String, state: BrowserState): Boolean {
        val tab = state.findTabOrCustomTabOrSelectedTab(sessionId) ?: return false
        val deferredResult = CompletableDeferred<Boolean>()

        tab.engineState.engineSession?.checkForPdfViewer(
            onResult = { isPdf -> deferredResult.complete(isPdf) },
            onException = { deferredResult.complete(false) },
        )

        return deferredResult.await()
    }
}