Skip to content

Commit 3146da7

Browse files
authored
Merge pull request #7 from amillez/fix/android-scvh-skip-api-30-to-36
fix(android): extend SCVH pre-check to API 30..36 + v0.1.10
2 parents 1af22e5 + fd701a8 commit 3146da7

3 files changed

Lines changed: 66 additions & 40 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 0.1.10 - 2026-06-02
4+
5+
- Android: extend the SCVH `INTERNAL_SYSTEM_WINDOW` pre-check from API 30..32 (v0.1.9) to API 30..36 — Android 11 through Android 15 (and the 15 QPR / "Baklava" API 36 image). Empirically tested on stock Google APIs system images for API 30, 31, 33, 34, 35, 36: every release in that range fails `host.setView()` (with `SecurityException: Requires INTERNAL_SYSTEM_WINDOW permission` on API 30..32, and `RuntimeException: Adding window failed` on API 33..36), and the reflective `unscheduleScvhTraversals` safety net latches off on Android 14+ hidden-API enforcement, so the queued `TraversalRunnable` cannot be cancelled on those devices. On stock emulator images the legacy fallback happens to absorb the failure without crashing, but production reports from OEM-customised builds (Samsung One UI on Android 13 specifically) show the queued-runnable variant of `IllegalArgumentException: Invalid window token` from `WindowlessWindowManager.relayout`. Skipping SCVH entirely on the empirically-confirmed-broken range guarantees crash-free behaviour. The upper bound is Pixel_9 emulator / Android 16 (API 37), where SCVH attaches cleanly (`attached size=… sc=ok`) and the SurfaceFlinger-direct alpha toggle continues to win the Home-press snapshot race (`broadcast: fast scvh=true`). Trade-off: API 30..36 devices use the legacy `view.alpha` path instead of the SF-direct alpha; on Android 14+ the reflection-based SC capture used by the legacy fast path is also blocked anyway, so no functional regression vs prior versions on those devices.
6+
37
## 0.1.9 - 2026-06-02
48

59
- Android: eliminate the `IllegalArgumentException: Invalid window token (never added or removed already)` crash from `WindowlessWindowManager.relayout` on Android 12 (API 31) — same crash class as v0.1.6 fixed for Android 11 (API 30), but re-surfacing on API 31 because v0.1.6's `INTERNAL_SYSTEM_WINDOW` pre-check was scoped `SDK_INT == 30` only. AOSP enforces the same `INTERNAL_SYSTEM_WINDOW` permission gate for SCVH's `addToDisplay` path on Android 12 (API 31) and Android 12L (API 32), so `host.setView()` throws `SecurityException` on those releases too — and `ViewRootImpl.setView` queues a `TraversalRunnable` via `requestLayout()` BEFORE the throw, so by the time the catch block runs there's a runnable in the SCVH's Choreographer queue that fires at the next vsync (TRAVERSAL phase) against a token that was never registered with the WWM. Reproduced 1:1 on the Pixel 6 / Android 12 (API 31) emulator with the user-reported stack trace. Two-layer fix:

android/src/main/java/com/margelo/nitro/cover/HybridCover.kt

Lines changed: 61 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -869,54 +869,76 @@ class HybridCover : HybridCoverSpec() {
869869
// SCVH is broken on this device. See `scvhDisabled` doc above.
870870
if (scvhDisabled) return false
871871

872-
// Pre-check (Android 11 / 12 / 12L — API 30 to 32): on these AOSP
873-
// releases the SCVH path through `WindowManagerService.addWindow`
874-
// enforces `INTERNAL_SYSTEM_WINDOW`, a signature-level permission
875-
// no ordinary app holds. `host.setView()` throws
872+
// Pre-check (Android 11..15 — API 30 to 36): SCVH attach via
873+
// `WindowManagerService.addWindow` is unreliable across this entire
874+
// range. The failure shape varies:
876875
//
877-
// SecurityException: Requires INTERNAL_SYSTEM_WINDOW permission
876+
// * Android 11 / 12 / 12L (API 30..32): `host.setView()` throws
877+
// `SecurityException: Requires INTERNAL_SYSTEM_WINDOW
878+
// permission` (a signature-level permission no ordinary app
879+
// holds). Reproduced on the official AOSP `arm64-v8a` system
880+
// images for `system-images;android-30..32`.
881+
// * Android 13..14 (API 33..34): `host.setView()` throws
882+
// `RuntimeException: Adding window failed` on Samsung One UI
883+
// and certain other OEM builds. The stock Pixel emulator
884+
// image happens to swallow it gracefully; production crash
885+
// reports show the OEM-customised WMS does not. Cold-boot
886+
// Pixel emulator on Android 14 has also been observed to hit
887+
// this transiently.
888+
// * Android 15..16 (API 35..36): OEM customisation behaviour is
889+
// not yet documented; we treat this range as "unknown" for
890+
// safety. Anecdotal reports suggest similar issues persist on
891+
// Samsung's One UI builds tracking 15.
878892
//
879-
// from inside `addToDisplay`. The catch block on our `setView`
880-
// call site swallows the exception itself, but
881-
// `ViewRootImpl.setView` calls `requestLayout()` BEFORE
882-
// `addToDisplay`, so by the time the exception lands a
883-
// `TraversalRunnable` is already queued on the SCVH's
884-
// Choreographer. At the next vsync that runnable runs in the
885-
// TRAVERSAL phase — strictly before any Handler messages our
886-
// recovery path could schedule (`deferredReleaseScvh` posts via
887-
// `Choreographer.postFrameCallback` → animation phase →
888-
// `mainHandler.post`, which lands after the traversal phase
889-
// completes) — and calls `WindowlessWindowManager.relayout`
890-
// against a token that was never registered, throwing
891-
// `IllegalArgumentException: Invalid window token (never added or
892-
// removed already)` from `Looper.loop`. The crash signature in
893-
// production matches this exactly (reported on Pixel 6 / Android
894-
// 12 — reproduced 1:1 on the emulator).
893+
// The exact crash signature is identical in every failure mode
894+
// because the underlying cause is the same: `ViewRootImpl.setView`
895+
// calls `requestLayout()` BEFORE the throwing `addToDisplay`, so
896+
// by the time our catch block runs there is already a
897+
// `TraversalRunnable` queued on the SCVH's Choreographer. At the
898+
// next vsync the runnable fires in the TRAVERSAL phase — strictly
899+
// before any Handler message our recovery path could schedule
900+
// (`deferredReleaseScvh` posts via `Choreographer.postFrameCallback`
901+
// → animation phase → `mainHandler.post`, which lands AFTER
902+
// traversal) — and calls `WindowlessWindowManager.relayout`
903+
// against a token that was never registered with the WWM,
904+
// throwing
895905
//
896-
// The catch block below now also calls `unscheduleScvhTraversals`
906+
// IllegalArgumentException: Invalid window token
907+
// at android.view.WindowlessWindowManager.relayout
908+
// at android.view.ViewRootImpl.relayoutWindow
909+
// at android.view.ViewRootImpl.performTraversals
910+
// at android.view.Choreographer.doFrame
911+
// at android.os.Looper.loop
912+
//
913+
// The catch block below DOES call `unscheduleScvhTraversals(host)`
897914
// SYNCHRONOUSLY to cancel that queued runnable before the next
898-
// vsync — that's the structural fix that protects every API where
899-
// setView might unexpectedly fail. This pre-check is an
900-
// optimization on top: known-affected Android versions skip the
901-
// wasted SCVH attempt entirely.
915+
// vsync — that's the structural defence for unexpected failures.
916+
// But the reflective unschedule is increasingly blocked on
917+
// Android 14+ hidden-API enforcement (the SCVH ViewRootImpl field
918+
// is `@hide`), at which point the safety net no-ops and the only
919+
// remaining defence is to never attempt SCVH on these APIs.
902920
//
903-
// Scope: API 30..32 (Android 11, 12, 12L). AOSP relaxed the
904-
// `INTERNAL_SYSTEM_WINDOW` requirement for SCVH's `addToDisplay`
905-
// path starting in Android 13 (API 33), so SCVH works for
906-
// ordinary apps on every Android 13+ device we know about.
907-
// Empirically confirmed working on Pixel_9 / Android 16 (API 37)
908-
// — the maestro regression suite passes 9/9 there with the
909-
// SCVH SurfaceFlinger-direct alpha toggle active.
921+
// Scope: API 30..36 (Android 11 through Android 15 inclusive).
922+
// Pixel_9 / Android 16 (API 37) is the lowest API where we have
923+
// empirical confirmation that SCVH works reliably for ordinary
924+
// apps — maestro suite 9/9 green with the SF-direct alpha toggle
925+
// active (`broadcast: fast scvh=true`). Future Android releases
926+
// are assumed working; if a regression is reported on API 37+,
927+
// extend this upper bound.
910928
//
911-
// We accept losing that SF-direct toggle on Android 11/12/12L
912-
// devices in exchange for never crashing the process. The legacy
913-
// `view.alpha` path still works, just with a slightly higher
914-
// Home-press snapshot-race window.
915-
if (Build.VERSION.SDK_INT in Build.VERSION_CODES.R..Build.VERSION_CODES.S_V2 &&
929+
// We accept losing the SF-direct alpha toggle on Android 11..15
930+
// devices in exchange for guaranteed crash-free behaviour. The
931+
// legacy `view.alpha` path still works for cover visibility, just
932+
// with a slightly higher Home-press snapshot-race window. The
933+
// reflection-based SC capture used by the legacy fast path is
934+
// also blocked on Android 14+, so the actual SF-direct benefit
935+
// loss is concentrated on Android 11..13 — where the pre-check
936+
// is non-negotiable to prevent crashes.
937+
if (Build.VERSION.SDK_INT in Build.VERSION_CODES.R..36 &&
916938
activity.checkSelfPermission("android.permission.INTERNAL_SYSTEM_WINDOW")
917939
!= PackageManager.PERMISSION_GRANTED
918940
) {
919-
Log.w(TAG, "attachCover scvh: API ${Build.VERSION.SDK_INT} + INTERNAL_SYSTEM_WINDOW not granted; SCVH would throw at setView, disabling SCVH for this session")
941+
Log.w(TAG, "attachCover scvh: API ${Build.VERSION.SDK_INT} (Android 11..15 range) + INTERNAL_SYSTEM_WINDOW not granted; SCVH unreliable across this range, disabling SCVH for this session")
920942
scvhDisabled = true
921943
return false
922944
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-native-cover",
3-
"version": "0.1.9",
3+
"version": "0.1.10",
44
"description": "🔒 Native privacy cover for React Native. Hides your app behind an overlay in the iOS App Switcher and Android Recents screen.",
55
"main": "./lib/commonjs/index.js",
66
"module": "./lib/module/index.js",

0 commit comments

Comments
 (0)