fix(#109): image PiPs never painted — set slot token before decode

Emulator e2e of an image PiP (a QR PNG) found the image area always blank (box
background + title only). Pre-existing defect, also on main, independent of the
occlusion reparent.

Root cause in PipOverlay.show(): teardown() clears `current` to null, then
loadImageInto() captured `token = current` (null) as its drop-if-replaced guard,
but `current` was set to the new pip_id AFTER the media was built. The image
decode finishes on a background thread and posts back after show() returns, so
`token != current` (null != pip_id) was always true and every decoded bitmap was
dropped. Web PiPs and the box/title were unaffected, which masked it.

Fix: set `current = pip_id` before building media so loadImageInto's token
matches. Verified on emulator — a QR image PiP now renders over both a static
image and live YouTube (hardware screencap + the app's software view.draw
capture both show it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
ScreenTinker 2026-06-19 14:33:37 -05:00
parent ce7b2948ae
commit 4528d07c53
2 changed files with 24 additions and 1 deletions

View file

@ -68,6 +68,13 @@ class PipOverlay(
val w = p.optInt("width", 480).coerceIn(1, dm.widthPixels * 4)
val h = p.optInt("height", 360).coerceIn(1, dm.heightPixels * 4)
// Set the slot token BEFORE building media: loadImageInto captures `current` as
// its drop-if-replaced token, and its decode finishes on a background thread that
// posts back AFTER show() returns. If current were still null here (teardown
// clears it), that guard would always fail and the decoded bitmap would be
// dropped — i.e. image PiPs never painted. (#109 follow-up.)
current = p.optString("pip_id", "(anon)")
val box = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
clipToOutline = true
@ -129,7 +136,7 @@ class PipOverlay(
pipLayout.addView(box, lp)
pipLayout.visibility = View.VISIBLE
current = p.optString("pip_id", "(anon)")
// current was set above (before media build) so loadImageInto's token matches.
// #109 fix (1)/(3): the pip layer is a top-level view above the WebView (reparented
// to the window content in MainActivity), but make sure it composites last and is

View file

@ -137,6 +137,22 @@ reproduce the Fire TV / Android TV hardware-overlay punch-through that is the
strongest form of cause (1). That still needs the real signage device — use the
`pipDebug` magenta box there to confirm.
### Follow-up bug found in emulator testing: image PiPs never painted the image
Verifying an **image** PiP (a QR PNG) surfaced a separate, pre-existing defect
(present on `main`, unrelated to the occlusion reparent): the image area was
always blank — only the box background + title showed. Root cause in
`PipOverlay.show()`: `teardown()` clears `current` to null, then `loadImageInto`
captured `token = current` (null) as its drop-if-replaced guard, but `current`
was only set to the new `pip_id` *after* the media was built. The decode finishes
on a background thread and posts back **after** `show()` returns — so the guard
`token != current` (null ≠ pip_id) was always true and **every decoded bitmap was
dropped**. (Web PiPs and the box/title were unaffected, which masked it.)
Fix: set `current = pip_id` **before** building the media (so `loadImageInto`'s
token matches). Confirmed on the emulator — the QR now renders in the PiP box
over both a static image and live YouTube.
## If the magenta box is STILL hidden over YouTube on the test device
Then it is the stronger form of cause (1): the WebView places its video on a