fix app link issues

This commit is contained in:
Fabian Freund
2026-07-08 08:28:25 +02:00
parent 31019a237b
commit 1d51b4049b
2 changed files with 151 additions and 0 deletions
@@ -24,6 +24,7 @@ import eu.weblibre.flutter_mozilla_components.services.MediaSessionService
import eu.weblibre.flutter_mozilla_components.activities.NotificationActivity
import eu.weblibre.flutter_mozilla_components.R
import eu.weblibre.flutter_mozilla_components.ext.getPreferenceKey
import eu.weblibre.flutter_mozilla_components.middleware.AppLinksCancelRetryMiddleware
import eu.weblibre.flutter_mozilla_components.middleware.FlutterEventMiddleware
import eu.weblibre.flutter_mozilla_components.middleware.HistoryMetadataMiddleware
import eu.weblibre.flutter_mozilla_components.middleware.HistoryMetadataService
@@ -231,6 +232,7 @@ class Core(
// Must run before any engine middleware so we can rewrite
// sandbox new-tab URLs before Gecko issues a request.
SandboxCaptureMiddleware,
AppLinksCancelRetryMiddleware(),
HistoryMetadataMiddleware(historyMetadataService),
// Correlates url -> contextId so WebLibreHistoryDelegate can
// resolve a visit's container at record time.
@@ -0,0 +1,149 @@
/*
* 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 https://mozilla.org/MPL/2.0/.
*/
package eu.weblibre.flutter_mozilla_components.middleware
import android.os.Handler
import android.os.Looper
import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.selector.findTabOrCustomTab
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineSession.LoadUrlFlags.Companion.EXTERNAL
import mozilla.components.concept.engine.EngineSession.LoadUrlFlags.Companion.LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.Store
/**
* Workaround for Android Components/GeckoView app-link cancel handling: the
* cancel load can be clobbered by Gecko's recovery load back to the previous
* history entry. This retries only that specific cancel-load signature.
*/
class AppLinksCancelRetryMiddleware(
private val handler: Handler = Handler(Looper.getMainLooper()),
private val retryDelayMillis: Long = RETRY_DELAY_MILLIS,
) : Middleware<BrowserState, BrowserAction> {
private val pendingCancels = mutableMapOf<String, PendingCancel>()
override fun invoke(
store: Store<BrowserState, BrowserAction>,
next: (BrowserAction) -> Unit,
action: BrowserAction,
) {
when (action) {
is EngineAction.OptimizedLoadUrlTriggeredAction -> {
recordCancelLoad(store, action)
}
is ContentAction.UpdateLoadRequestAction -> {
handleLoadRequest(store, action)
}
is ContentAction.UpdateUrlAction -> {
handleUrlUpdate(action)
}
else -> {}
}
next(action)
}
private fun recordCancelLoad(
store: Store<BrowserState, BrowserAction>,
action: EngineAction.OptimizedLoadUrlTriggeredAction,
) {
if (!action.flags.contains(EXTERNAL) ||
!action.flags.contains(LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE)
) {
return
}
val sourceUrl = store.state.findTabOrCustomTab(action.tabId)?.content?.url
?: return
if (sourceUrl == action.url || sourceUrl == ABOUT_BLANK) {
return
}
pendingCancels[action.tabId] = PendingCancel(
tabId = action.tabId,
sourceUrl = sourceUrl,
targetUrl = action.url,
)
}
private fun handleLoadRequest(
store: Store<BrowserState, BrowserAction>,
action: ContentAction.UpdateLoadRequestAction,
) {
val pending = pendingCancels[action.sessionId] ?: return
when (action.loadRequest.url) {
pending.sourceUrl -> scheduleRetry(store, pending)
pending.targetUrl -> pendingCancels.remove(action.sessionId)
ABOUT_BLANK -> {}
else -> pendingCancels.remove(action.sessionId)
}
}
private fun handleUrlUpdate(action: ContentAction.UpdateUrlAction) {
val pending = pendingCancels[action.sessionId] ?: return
when (action.url) {
pending.targetUrl -> pendingCancels.remove(action.sessionId)
pending.sourceUrl, ABOUT_BLANK -> {}
else -> pendingCancels.remove(action.sessionId)
}
}
private fun scheduleRetry(
store: Store<BrowserState, BrowserAction>,
pending: PendingCancel,
) {
if (pending.retryScheduled) {
return
}
val scheduled = pending.copy(retryScheduled = true)
pendingCancels[pending.tabId] = scheduled
handler.postDelayed({
if (pendingCancels[pending.tabId] != scheduled) {
return@postDelayed
}
val currentUrl = store.state.findTabOrCustomTab(pending.tabId)?.content?.url
if (currentUrl == pending.targetUrl) {
pendingCancels.remove(pending.tabId)
return@postDelayed
}
if (currentUrl == pending.sourceUrl || currentUrl == ABOUT_BLANK) {
pendingCancels.remove(pending.tabId)
store.dispatch(
EngineAction.LoadUrlAction(
tabId = pending.tabId,
url = pending.targetUrl,
flags = EngineSession.LoadUrlFlags.select(
LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE,
),
),
)
} else {
pendingCancels.remove(pending.tabId)
}
}, retryDelayMillis)
}
private data class PendingCancel(
val tabId: String,
val sourceUrl: String,
val targetUrl: String,
val retryScheduled: Boolean = false,
)
private companion object {
const val ABOUT_BLANK = "about:blank"
const val RETRY_DELAY_MILLIS = 1000L
}
}