- Android: extend the SCVH
INTERNAL_SYSTEM_WINDOWpre-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 failshost.setView()(withSecurityException: Requires INTERNAL_SYSTEM_WINDOW permissionon API 30..32, andRuntimeException: Adding window failedon API 33..36), and the reflectiveunscheduleScvhTraversalssafety net latches off on Android 14+ hidden-API enforcement, so the queuedTraversalRunnablecannot 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 ofIllegalArgumentException: Invalid window tokenfromWindowlessWindowManager.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 legacyview.alphapath 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.
- Android: eliminate the
IllegalArgumentException: Invalid window token (never added or removed already)crash fromWindowlessWindowManager.relayouton 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'sINTERNAL_SYSTEM_WINDOWpre-check was scopedSDK_INT == 30only. AOSP enforces the sameINTERNAL_SYSTEM_WINDOWpermission gate for SCVH'saddToDisplaypath on Android 12 (API 31) and Android 12L (API 32), sohost.setView()throwsSecurityExceptionon those releases too — andViewRootImpl.setViewqueues aTraversalRunnableviarequestLayout()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:- Extended the
INTERNAL_SYSTEM_WINDOWpre-check toSDK_INT in 30..32(Android 11, 12, 12L). On those releases SCVH is skipped entirely, nosetViewattempt, no queued runnable, no crash. AOSP relaxed the requirement starting in API 33 (Android 13), so SCVH and the SurfaceFlinger-direct alpha toggle keep working unmodified on every Android 13+ device — confirmed on Pixel_9 / Android 16 (API 37) where the maestro regression suite still passes 9/9. - New
failScvhAttach(host, reason)helper invoked from all five SCVH attach recovery branches intryAttachCoverViaScvh(setView, null surfacePackage, initial setAlpha, setChildSurfacePackage, addView). It now SYNCHRONOUSLY callsunscheduleScvhTraversals(host)to cancel any TraversalRunnable already queued by the failedsetViewBEFORE returning to the caller — main-thread synchronous, drains the queue before the next vsync fires. The previousdeferredReleaseScvh(Choreographer animation phase →mainHandler.post) was structurally too late: the queued runnable runs in the TRAVERSAL phase, strictly before any Handler message scheduled during the same vsync. This synchronous cancel is the structural defence that protects every API where SCVH might unexpectedly fail (not just the API 30..32 range covered by the pre-check) — observed value: cold-boot Pixel emulator quirks on Android 14 where SCVH intermittently fails, and any future Android version that might re-introduce a similar restriction.
- Extended the
minSdkVersion=23, public API, and iOS behavior unchanged.
- Android: complete the Samsung One UI Recents-cover fix from v0.1.7. The instance-based
exclude2 = coverContentexclusion intopmostHostViewForonly filtered the current cover content; on Samsung One UI (Galaxy A05 / SM-A057F, Android 14, One UI 6/7), rapidsetColor/setImage/enableJS calls duringLockCovermount trigger 2-3 sequential attach cycles (ensurePreMounted→refreshCoverContentIfMounted), and the previous attach'sFreezableFrameLayoutlingers briefly inWindowManagerGlobal.mViewsafter teardown. The walk picked the stale instance as "topmost",addCovertried to attach as a sub-window of an invalidated token, andWindowManager.addViewthrewBadTokenException: Unable to add window — token … is not valid; is your activity running?on the third attach in the burst. The result: cover never appeared in the Recents thumbnail. Fix: add a class-based filter inCoverWindowAttachment.topmostHostViewForthat rejects anyFreezableFrameLayoutregardless of instance, so stale entries from previous attach cycles can't be mistaken for the topmost host.FreezableFrameLayoutwas madeinternal(package-visible) to keep the type reachable fromCoverWindowAttachment. The instance-matchexclude2 = coverContentis preserved as a fast-path. On every non-Samsung device the SCVH-hosted content never appears inWindowManagerGlobal.mViewsto begin with, so the new filter is a no-op. Verified end-to-end on Samsung Galaxy A05 (cover paints in Recents with#5F8AFAbackground and splash icon, noBadTokenException), Pixel_9 emulator (9/9 maestro flows green, same baseline as v0.1.6), and Favvy_Android_30 / API 30 (5× home/app-switcher + 10× rapid toggle stress, zero crashes, API 30INTERNAL_SYSTEM_WINDOWpre-check still fires).
- Android: fix the cover never appearing on Samsung One UI (observed on Samsung Galaxy A05 / SM-A057F, Android 14). The host-window walk in
ensureCoverOnTopmostandaddCoverexcluded the cover Window's root (coverView, aSurfaceViewon the SCVH path) but not the SCVH-hosted content (coverContent, aFreezableFrameLayout). On Samsung One UI's customised WindowManager the SCVH content appears inWindowManagerGlobal.mViewsdespite living inside the SCVH's ownViewRootImpl, so the walk identified our own content view as the topmost host, tried to attach the cover as a sub-window of itself, andWindowManager.addViewthrewBadTokenException: Unable to add window — token … is not valid; is your activity running?. The cover never reached the Recents thumbnail on those devices. Fix: passexclude2 = coverContenttoCoverWindowAttachment.topmostHostViewForat all three callsites so the walk skips both views and falls back to the activity decor.CoverWindowAttachmentalready supported the parameter (the blur source picker uses it for the same reason). Verified on Samsung Galaxy A05 (Android 14, One UI) — Recents thumbnail now shows the configured#5F8AFAbackground with the splash icon, exactly the iOS App Switcher parity it was designed for.
minSdkVersion=23, public API, and iOS behavior unchanged.
- Android: eliminate the
IllegalArgumentException: Invalid window token (never added or removed already)crash fromWindowlessWindowManager.relayoutreported on production devices (NEAR Mobile Play Console and others). v0.1.5's reflective traversal cancel only works on Android versions that don't block the hidden-API probe; Android 14+ hidden-API enforcement leaves the crash unfixed there. Five new layered defences indetachCoverViewclose the race deterministically without sacrificing the snapshot-race fix:FreezableFrameLayoutcover-content root whoserequestLayout()/invalidate()no-op while afrozenflag is set. Set first thing indetachCoverView; from that point on norequestLayoutreachesViewRootImpl.scheduleTraversals, so no newTraversalRunnablecan be queued during teardown.- Snapshot-then-null-out of shared state plus a
coverDetachingre-entrance guard. Any synchronous re-entrant call (animation cancel listener,dispatchDetachedFromWindow) sees cleared fields and bails before double-removing or double-releasing. WindowManager.removeViewImmediatesodispatchDetachedFromWindowruns inline while the freeze is active; falls back to asyncremoveViewon OEM impls that reject immediate removal in transitional states.setCoverVisibilityvalidity-checksview.windowToken != nullbeforeupdateViewLayout— same WMS/WWM path that throws when the token is gone.deferredReleaseScvhschedulessafeReleaseScvhviaChoreographer.postFrameCallback(animation phase) → nestedmainHandler.post, so it runs strictly AFTER the next frame's traversal callbacks. AnyTraversalRunnablealready in the queue (including ones reflection couldn't cancel) fires with the WWM token still valid; release happens after, removing the token only when nothing is left to relayout.ViewRootImpl.mTraversalScheduledguarantees at most one queued runnable per ViewRootImpl, so one frame's wait drains it. Wired into all six release call sites — five SCVH attach recovery branches intryAttachCoverViaScvhplus the teardown indetachCoverView.
- Android: skip SCVH entirely on Android 11 (API 30) when
INTERNAL_SYSTEM_WINDOWis denied (signature-level permission no normal app holds). On this AOSP release the SCVH path throughWindowManagerService.addWindowenforces this permission andhost.setViewthrowsSecurityExceptionAFTERViewRootImpl.setViewhas already calledrequestLayout()— queuing aTraversalRunnablethat fires the same vsync (TRAVERSAL phase, before any deferred work) on a token that was never registered with the WWM, throwing the "never added" variant ofInvalid window tokenfromLooper.loop. The deferred release can't help in this case (the runnable fires in the same vsync's traversal phase, before our queued Handler message); the only safe path is to never callsetViewon these devices. Scoped to API 30 only — Android 12+ dropped the permission check for SCVH'saddToDisplaypath, so SCVH and the SurfaceFlinger-direct alpha toggle continue to work unmodified on every modern device. - Android: real frosted-glass blur on Android 11 (API 30) instead of a flat tint.
RenderEffect.createBlurEffectrequires API >= 31, and the previous fallback dropped the captured bitmap and painted a flat ~80% white tint, leaving underlying app content fully readable through the cover — broken privacy on every Android 11 host. The fallback now reuses the captured bitmap at 1/12 in each dimension (1/144 pixels) and letsImageView's bilinear filter smudge it back to display size on draw, layered with the style tint as foreground exactly like the API >= S path. The visual contract matches across versions and the cover obscures content to a similar degree as iOS'sUIBlurEffect.
minSdkVersion=23, public API, and iOS behavior unchanged.
- Android: cancel pending Choreographer traversals on the SCVH's internal
ViewRootImplbefore every release. Without this, aTraversalRunnablequeued by the partialsetViewcould fire after the windowless window token was removed, throwingIllegalArgumentException: Invalid window token (never added or removed already)fromWindowlessWindowManager.relayouton a later vsync, outside any try/catch of ours because the runnable is dispatched fromLooper.loop. Implemented as a reflective call toViewRootImpl.unscheduleTraversals()via a field probe (the SCVH field name has not been stable across Android versions); on reflection failure the helper latches off so subsequent calls don't keep retrying. - Android: latch SCVH as "known-bad" for the rest of the process on the first failure of any step in
tryAttachCoverViaScvh(ctor, setView, null surfacePackage, initial setAlpha, setChildSurfacePackage, addView). Belt-and-suspenders alongside the unschedule helper: if the reflective traversal cancel silently no-ops on a future Android version, the latch caps the orphaned-traversal blast radius to exactly one queued crash per process — subsequent attempts go straight to the legacy non-SCVH attach path.
- Android: fix process crash in the SCVH attach recovery path. When
SurfaceControlViewHost.setView()(or one of the follow-up steps intryAttachCoverViaScvh) failed, the recoveryhost.release()propagated a framework NPE.ViewRootImpl$InputStage.onDetachedFromWindow()on a null head, because the SCVH's input stage chain hadn't been wired up yet. All five recoveryrelease()calls plus the teardown release indetachCoverViewnow go through asafeReleaseScvhhelper that swallows the framework exception, so the legacy non-SCVH attach path actually runs as the intended fallback. Recovery log lines also now include the exception class name.
- Android: fix the Recents thumbnail race that intermittently leaked the underlying app content — especially under load or with a
<Modal>open. On API 30+, the cover content is now hosted in aSurfaceControlViewHostand visibility is toggled viaSurfaceControl.Transaction.setAlphastraight from the broadcast HandlerThread, bypassing the View →ViewRootImpl→ compose chain that was losing the race. Closes #1. - Android: fix the cover missing on the first Home press while a
<Modal>is open. The pre-mounted cover is now re-parented to the modal's window the moment it opens (via anOnWindowFocusChangeListeneron the activity decor), so it's always composed above the modal. Out-of-process focus changes (permission prompts, biometrics, notification shade) still don't trigger a re-parent. - Android: fix blur mode showing only a translucent tint on the new SCVH path — the blur-source picker now skips the cover's wrapping
SurfaceView(which software-draws as transparent) and falls back to the activity decor. - Android: ask the OS for the display's highest refresh rate while the cover is attached, shortening the snapshot race window on 90/120 Hz panels.
minSdkVersion=23, public API, and iOS behavior unchanged.
- Android: fix soft keyboard input being swallowed by the pre-mounted cover panel. The invisible panel now sets
FLAG_ALT_FOCUSABLE_IMalongsideFLAG_NOT_TOUCHABLEso the IME sits above it and keystrokes reach the focusedTextInput; both flags are cleared again when the cover becomes visible so it still paints over any lingering keyboard.
Initial release.
- Native privacy cover for React Native built on Nitro Modules.
- iOS: hides app content behind an overlay in the App Switcher.
- Android: hides app content in the Recents screen.