also looking at this
fix(android): tie a ready mark to its own navigation, and inject a document once
#239
5 files
+159
-3
| @@ -219,6 +219,19 @@ class CraftBridge( | ||
| 219 | 219 | private val mainExecutor: Executor = ContextCompat.getMainExecutor(activity) |
| 220 | 220 | @Volatile |
| 221 | 221 | 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 | |
| 222 | 235 | private val pendingEvents = mutableListOf<Pair<String, Map<String, Any>>>() |
| 223 | 236 | |
| 224 | 237 | // What the app was built with (#209). The page refuses a disabled call |
| @@ -327,6 +340,24 @@ class CraftBridge( | ||
| 327 | 340 | */ |
| 328 | 341 | private fun jsQuote(value: Any?): String = JSONObject.quote(value.toString()) |
| 329 | 342 | |
| 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 | ||
| 330 | 361 | fun injectBridge() { |
| 331 | 362 | // Before the script, so a native callback arriving while the page is |
| 332 | 363 | // still loading has somewhere to go rather than being counted as |
| @@ -1653,8 +1684,15 @@ class CraftBridge( | ||
| 1653 | 1684 | |
| 1654 | 1685 | activity.runOnUiThread { |
| 1655 | 1686 | 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)) { | |
| 1657 | 1691 | 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 | |
| 1658 | 1696 | bridgeReady = true |
| 1659 | 1697 | val events = pendingEvents.toList() |
| 1660 | 1698 | pendingEvents.clear() |
| @@ -1670,6 +1708,9 @@ class CraftBridge( | ||
| 1670 | 1708 | |
| 1671 | 1709 | fun markBridgeLoading() { |
| 1672 | 1710 | 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 | |
| 1673 | 1714 | } |
| 1674 | 1715 | |
| 1675 | 1716 | // ==================== Haptics ==================== |