ReviewOS

also looking at this

craft-native/craft

fix(android): tie a ready mark to its own navigation, and inject a document once

#239
Merged glennmichael123 wants to merge fix/android-injection-generation into main
5 files +159 -3
packages/android/templates/CraftBridge.kt.templatemodified+42-1
Changes to packages/android/templates/CraftBridge.kt.template
@@ -219,6 +219,19 @@ class CraftBridge(
219219 private val mainExecutor: Executor = ContextCompat.getMainExecutor(activity)
220220 @Volatile
221221 private var bridgeReady = false
222
223 /**
224 * Which navigation the bridge belongs to.
225 *
226 * `evaluateJavascript`'s completion is posted, so it can run after the
227 * next navigation has already started. Without this, a completion from
228 * the page that has just left marks the page that has just arrived as
229 * ready, and the queues below flush into a document with no bridge and no
230 * replay listener — where they are lost rather than delivered (#228).
231 *
232 * Main thread only, like everything that touches `bridgeReady`.
233 */
234 private var navigationGeneration = 0
222235 private val pendingEvents = mutableListOf<Pair<String, Map<String, Any>>>()
223236
224237 // What the app was built with (#209). The page refuses a disabled call
@@ -327,6 +340,24 @@ class CraftBridge(
327340 */
328341 private fun jsQuote(value: Any?): String = JSONObject.quote(value.toString())
329342
343 /**
344 * The script, guarded so a second run in the *same* document does nothing.
345 *
346 * Chromium calls `onPageFinished` without a matching `onPageStarted` for
347 * same-document navigations — a fragment change, a `history.pushState` —
348 * and `MainActivity` injects on every one of them. Running the whole
349 * script again replaces `window.craft`, fires `craftReady` a second time,
350 * and rejects every in-flight promise with "Android bridge reinitialized"
351 * (#228).
352 *
353 * A block, not a function: `var` and function declarations are
354 * function-scoped, so the script behaves exactly as it does unwrapped,
355 * and everything it publishes it publishes on `window` regardless. A real
356 * navigation gets a new `window`, so the flag is absent and it installs.
357 */
358 private fun onlyOnce(script: String): String =
359 "if (!window.__craftBridgeInstalled) {\nwindow.__craftBridgeInstalled = true;\n$script\n}"
360
330361 fun injectBridge() {
331362 // Before the script, so a native callback arriving while the page is
332363 // still loading has somewhere to go rather than being counted as
@@ -1653,8 +1684,15 @@ class CraftBridge(
16531684
16541685 activity.runOnUiThread {
16551686 if (closed) return@runOnUiThread
1656 webView.evaluateJavascript(script) {
1687 // Read here rather than at entry: this is the navigation the
1688 // script is actually being injected into.
1689 val generation = navigationGeneration
1690 webView.evaluateJavascript(onlyOnce(script)) {
16571691 if (closed) return@evaluateJavascript
1692 // A completion for a page that has since navigated away.
1693 // Marking ready now would flush into the new document, which
1694 // has no bridge yet, and the queues would be spent on it.
1695 if (generation != navigationGeneration) return@evaluateJavascript
16581696 bridgeReady = true
16591697 val events = pendingEvents.toList()
16601698 pendingEvents.clear()
@@ -1670,6 +1708,9 @@ class CraftBridge(
16701708
16711709 fun markBridgeLoading() {
16721710 bridgeReady = false
1711 // A document is on its way out, so any injection still in flight is
1712 // for a page that no longer exists.
1713 navigationGeneration += 1
16731714 }
16741715
16751716 // ==================== Haptics ====================