fix(android): API 31+ Invalid window token crash + v0.1.9 - #6
Merged
Merged
Conversation
…ttach failure + extend pre-check to API 30..32
Production crash recurred on Android 12 (Pixel 6, API 31). Same stack
trace and crash class as v0.1.6 fixed for Android 11 (API 30):
Exception java.lang.IllegalArgumentException: Invalid window token (never added or removed already)
at android.view.WindowlessWindowManager.relayout (WindowlessWindowManager.java:275)
at android.view.ViewRootImpl.relayoutWindow (ViewRootImpl.java:7792)
at android.view.ViewRootImpl.performTraversals (ViewRootImpl.java:2840)
at android.view.ViewRootImpl.doTraversal (ViewRootImpl.java:2143)
at android.view.ViewRootImpl$TraversalRunnable.run (ViewRootImpl.java:8665)
at android.view.Choreographer$CallbackRecord.run
at android.view.Choreographer.doCallbacks
at android.view.Choreographer.doFrame
at android.os.Looper.loop
Reproduced 1:1 on a fresh Pixel 6 / Android 12 (API 31) emulator: tap
Enable cover -> process dies immediately. v0.1.6's pre-check was
scoped `SDK_INT == Build.VERSION_CODES.R` (API 30) only, so API 31
fell through to `host.setView()` which throws SecurityException after
already queuing a TraversalRunnable via requestLayout() — runnable
fires at next vsync, hits WWM.relayout against a never-registered
token.
Two-layer fix:
1. Extended pre-check to `SDK_INT in Build.VERSION_CODES.R..Build.VERSION_CODES.S_V2`
(API 30..32 / Android 11, 12, 12L). AOSP enforces the
`INTERNAL_SYSTEM_WINDOW` permission gate for SCVH's `addToDisplay`
path on all three releases; API 33 (Android 13) relaxed it. Known-
affected versions skip SCVH entirely now — no setView, no queued
runnable, no crash.
2. Restructured the SCVH attach recovery: all five failure branches
(`setView`, null `surfacePackage`, initial `setAlpha`,
`setChildSurfacePackage`, `addView`) now go through a single
`failScvhAttach(host, reason)` helper that:
a. latches `scvhDisabled = true`
b. SYNCHRONOUSLY calls `unscheduleScvhTraversals(host)` to cancel
any TraversalRunnable that ViewRootImpl.setView queued via
requestLayout() before the throw. Main-thread synchronous,
drains the queue strictly before the next vsync fires. The
existing `deferredReleaseScvh` was structurally too late: it
posts via `Choreographer.postFrameCallback` (animation phase)
-> `mainHandler.post`, which lands AFTER the same vsync's
TRAVERSAL callbacks complete. The queued TraversalRunnable
runs IN the TRAVERSAL phase, strictly before any Handler
message we schedule during the same frame, so the deferred
release couldn't cancel it.
c. defers host.release() via deferredReleaseScvh as before, for
the SCVH ViewRootImpl teardown.
Layer 2 is the structural fix that protects every API where SCVH
might unexpectedly fail (cold-boot Pixel emulator quirks, future
Android versions reintroducing similar restrictions, OEM-customised
WindowManager builds). Layer 1 is an optimization on top: known-bad
APIs skip the wasted SCVH attempt.
Side benefit: the helper collapses ~110 lines of duplicated catch-
block comments and code across five recovery branches down to a
single one-line invocation per branch.
Verified:
- Pixel 6 / Android 12 (API 31, the reproducer): tap Enable cover ->
no FATAL EXCEPTION, no Invalid window token in `logcat *:E`. App
PID stable through 5x home/app-switcher + 10x rapid enable/disable
toggle. Broadcast pipeline fires (`broadcast: reason=homekey
isEnabled=true`, `performUserLeaveMount`, `setCoverVisibility:
visible=true scvh=false`). API 31 pre-check log line confirms the
range expansion: `attachCover scvh: API 31 + INTERNAL_SYSTEM_WINDOW
not granted; SCVH would throw at setView, disabling SCVH for this
session`.
- Pixel_9 / Android 16 (API 37): 9/9 maestro flows pass in 3m 26s,
identical to the v0.1.6 / v0.1.8 baseline. SCVH still works,
SurfaceFlinger-direct alpha toggle still wins the snapshot race
(`broadcast: fast scvh=true`).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
v0.1.9 — fixes a production crash recurrence on Android 12 (Pixel 6, API 31). Same
IllegalArgumentException: Invalid window tokenclass that v0.1.6 fixed for Android 11 (API 30), reappearing because v0.1.6'sINTERNAL_SYSTEM_WINDOWpre-check was scoped toSDK_INT == 30only.Reproduced 1:1 on the Pixel 6 / Android 12 emulator with the exact stack trace from the user's crash report:
host.setView()throwsSecurityException: Requires INTERNAL_SYSTEM_WINDOW permissionon Android 12 (same as 11), butViewRootImpl.setViewqueues aTraversalRunnableviarequestLayout()before the throw. Runnable fires at next vsync (TRAVERSAL phase) →WWM.relayout→ token never registered → crash fromLooper.loop.Two-layer fix
1. Pre-check broadened to API 30..32 (Android 11, 12, 12L). AOSP enforces
INTERNAL_SYSTEM_WINDOWfor SCVHaddToDisplayon all three; API 33 (Android 13) relaxed it.2. New
failScvhAttach(host, reason)helper invoked from all 5 SCVH attach recovery branches. SYNCHRONOUSLY callsunscheduleScvhTraversals(host)to cancel the queuedTraversalRunnableBEFORE returning to the caller — main-thread synchronous, drains the queue strictly before the next vsync.deferredReleaseScvhwas structurally too late: it posts viaChoreographer.postFrameCallback(animation phase) →mainHandler.post, which lands AFTER the same vsync's TRAVERSAL phase completes. The runnable runs IN the traversal phase, strictly before our handler message.Layer 2 protects every API where SCVH might unexpectedly fail — cold-boot Pixel emulator quirks on Android 14, future Android versions, OEM-customised WindowManager builds. Layer 1 is an optimization on top: known-affected APIs skip the wasted SCVH attempt.
Side benefit: helper collapses ~110 lines of duplicated catch-block comments/code across five recovery branches into one-line invocations.
Verified
FATAL EXCEPTION, noInvalid window token. App PID stable through 5× home/app-switcher + 10× toggle stress. New API 31 pre-check log:attachCover scvh: API 31 + INTERNAL_SYSTEM_WINDOW not granted; SCVH would throw at setView, disabling SCVH for this session.broadcast: fast scvh=true).Files changed
android/src/main/java/com/margelo/nitro/cover/HybridCover.kt— net -5 lines (the helper refactor consolidates duplicated catch blocks).package.json—0.1.8→0.1.9.CHANGELOG.md— new0.1.9 - 2026-06-02entry.