Skip to content

fix(android): API 31+ Invalid window token crash + v0.1.9 - #6

Merged
amillez merged 2 commits into
mainfrom
fix/android-api31-scvh-queued-traversal
Jun 2, 2026
Merged

amillez merged 2 commits into
mainfrom
fix/android-api31-scvh-queued-traversal

Conversation

@amillez

@amillez amillez commented Jun 2, 2026

Copy link
Copy Markdown
Owner

Summary

v0.1.9 — fixes a production crash recurrence on Android 12 (Pixel 6, API 31). Same IllegalArgumentException: Invalid window token class that v0.1.6 fixed for Android 11 (API 30), reappearing because v0.1.6's INTERNAL_SYSTEM_WINDOW pre-check was scoped to SDK_INT == 30 only.

Reproduced 1:1 on the Pixel 6 / Android 12 emulator with the exact stack trace from the user's crash report:

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$TraversalRunnable.run
  at android.view.Choreographer.doFrame
  at android.os.Looper.loop

host.setView() throws SecurityException: Requires INTERNAL_SYSTEM_WINDOW permission on Android 12 (same as 11), but ViewRootImpl.setView queues a TraversalRunnable via requestLayout() before the throw. Runnable fires at next vsync (TRAVERSAL phase) → WWM.relayout → token never registered → crash from Looper.loop.

Two-layer fix

1. Pre-check broadened to API 30..32 (Android 11, 12, 12L). AOSP enforces INTERNAL_SYSTEM_WINDOW for SCVH addToDisplay on all three; API 33 (Android 13) relaxed it.

-if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R &&
+if (Build.VERSION.SDK_INT in Build.VERSION_CODES.R..Build.VERSION_CODES.S_V2 &&
   activity.checkSelfPermission("android.permission.INTERNAL_SYSTEM_WINDOW")
     != PackageManager.PERMISSION_GRANTED
 ) {
   scvhDisabled = true
   return false
 }

2. New failScvhAttach(host, reason) helper invoked from all 5 SCVH attach recovery branches. SYNCHRONOUSLY calls unscheduleScvhTraversals(host) to cancel the queued TraversalRunnable BEFORE returning to the caller — main-thread synchronous, drains the queue strictly before the next vsync. deferredReleaseScvh was structurally too late: it posts via Choreographer.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

Device Result
Pixel 6 / Android 12 (API 31) — the reproducer ✅ No FATAL EXCEPTION, no Invalid 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.
Pixel_9 emulator / Android 16 (API 37) ✅ 9/9 maestro flows pass in 3m 26s — identical to v0.1.6 / v0.1.8 baseline. SCVH still works, SurfaceFlinger-direct alpha toggle still wins the snapshot race (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.json0.1.80.1.9.
  • CHANGELOG.md — new 0.1.9 - 2026-06-02 entry.

amillez added 2 commits June 2, 2026 15:28
…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`).
@amillez
amillez merged commit 1af22e5 into main Jun 2, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant