Two field reports from one Android user, with one shape between them: a decision taken once, at a moment when the answer was still wrong, and never revisited.
The console UI could not be dismissed
"With a controller" asks whether a pad is attached, and the client answered that with isPad — does this device's source class include gamepad or joystick. That is the right question for routing an event and the wrong one for presence: devices publish inputs that claim the source class while being no such thing (OEM game-mode overlays, gaming-phone shoulder triggers), and one of them is enough to pin the console UI on forever, because a pad that was never there can never disconnect.
pads() now filters on looksLikeController: the source claim and hardware behind it — a stick, a HAT, or the A/B face buttons — on a device the platform did not synthesize itself. The claim is cheap; the hardware is not. isPad keeps its looser meaning for the event lane, where it is correct.
This is not a complete defence, and the code says so: an OEM device that declares BTN_GAMEPAD and a pair of axes is indistinguishable from a pad at this layer. The master switch stays the guaranteed way out — and #354, merged a few hours ago, is what put that switch inside the console where the trapped user can reach it. The two are complementary: #354 gives the escape hatch, this makes the mode row actually decide something. The Controllers screen still lists every real device in one column or the other, which is where someone looks when the client's idea of "a pad is attached" disagrees with the room.
The picture sat in the upper-left corner
A regression in the ASurfaceControl backend from #290. The layer composites into the SurfaceView's on-screen rectangle, read once at surfaceCreated — but StreamScreen hides the system bars and switches the window to LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS in a DisposableEffect that runs a frame or two after first layout, and each one grows the video view under a surface that is never recreated. Only surfaceChanged fires, so the layer went on painting at its start-up size, anchored at the origin. It passed on glass because a device whose bars are already hidden when the surface arrives never sees the gap; rotation and multi-window had the same hole.
The size is now live: a packed Arc<AtomicU64> on SessionHandle, seeded by nativeStartVideo, re-reported by the new nativeVideoSurfaceSize from every surfaceChanged, and read by Layer before each present. One atomic load per frame, and rotation and multi-window come along for free.
Verified
:kit:cargoNdkClippy — arm64-v8a + armeabi-v7a, deny warnings, after cargo clean -p punktfunk-client-android --target aarch64-linux-android so the crate was genuinely re-checked and not served from cache.
:kit:testDebugUnitTest and :app:testDebugUnitTest, plus the native crate's own 31 tests.
New PadPresenceTest (4 cases) covers the presence truth table at a pure seam, since an InputDevice cannot be built off-device. It lives in :kit:, which CI runs unfiltered — unlike the :app: allowlist, so it actually gates.
Java_io_unom_punktfunk_kit_NativeBridge_nativeVideoSurfaceSize confirmed exported in the built .so, so the new external fun links at runtime.
Rebased onto current main (which brought the Skia console and #357's pad HID work) and re-run green there, not just where it was written.
Neither fix has been on glass — no device here. The corner fix is the one worth a live look, and it is cheap: start a stream and watch the picture fill the panel from the first frame.
Two field reports from one Android user, with one shape between them: a decision taken once, at a moment when the answer was still wrong, and never revisited.
## The console UI could not be dismissed
"With a controller" asks whether a pad is attached, and the client answered that with `isPad` — does this device's source class include gamepad or joystick. That is the right question for **routing** an event and the wrong one for presence: devices publish inputs that claim the source class while being no such thing (OEM game-mode overlays, gaming-phone shoulder triggers), and one of them is enough to pin the console UI on forever, because a pad that was never there can never disconnect.
`pads()` now filters on `looksLikeController`: the source claim **and** hardware behind it — a stick, a HAT, or the A/B face buttons — on a device the platform did not synthesize itself. The claim is cheap; the hardware is not. `isPad` keeps its looser meaning for the event lane, where it is correct.
This is **not** a complete defence, and the code says so: an OEM device that declares `BTN_GAMEPAD` and a pair of axes is indistinguishable from a pad at this layer. The master switch stays the guaranteed way out — and #354, merged a few hours ago, is what put that switch inside the console where the trapped user can reach it. The two are complementary: #354 gives the escape hatch, this makes the mode row actually decide something. The Controllers screen still lists every real device in one column or the other, which is where someone looks when the client's idea of "a pad is attached" disagrees with the room.
## The picture sat in the upper-left corner
A regression in the ASurfaceControl backend from #290. The layer composites into the SurfaceView's on-screen rectangle, read **once** at `surfaceCreated` — but `StreamScreen` hides the system bars and switches the window to `LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS` in a `DisposableEffect` that runs a frame or two after first layout, and each one grows the video view under a surface that is never recreated. Only `surfaceChanged` fires, so the layer went on painting at its start-up size, anchored at the origin. It passed on glass because a device whose bars are already hidden when the surface arrives never sees the gap; rotation and multi-window had the same hole.
The size is now live: a packed `Arc<AtomicU64>` on `SessionHandle`, seeded by `nativeStartVideo`, re-reported by the new `nativeVideoSurfaceSize` from every `surfaceChanged`, and read by `Layer` before each present. One atomic load per frame, and rotation and multi-window come along for free.
## Verified
- `:kit:cargoNdkClippy` — arm64-v8a + armeabi-v7a, deny warnings, after `cargo clean -p punktfunk-client-android --target aarch64-linux-android` so the crate was genuinely re-checked and not served from cache.
- `:kit:testDebugUnitTest` and `:app:testDebugUnitTest`, plus the native crate's own 31 tests.
- New `PadPresenceTest` (4 cases) covers the presence truth table at a pure seam, since an `InputDevice` cannot be built off-device. It lives in `:kit:`, which CI runs unfiltered — unlike the `:app:` allowlist, so it actually gates.
- `Java_io_unom_punktfunk_kit_NativeBridge_nativeVideoSurfaceSize` confirmed exported in the built `.so`, so the new `external fun` links at runtime.
Rebased onto current main (which brought the Skia console and #357's pad HID work) and re-run green there, not just where it was written.
**Neither fix has been on glass** — no device here. The corner fix is the one worth a live look, and it is cheap: start a stream and watch the picture fill the panel from the first frame.
Two field reports from one Android user, with one shape between them: a
decision taken once, at a moment when the answer was still wrong, and never
revisited.
The console UI could not be dismissed. "With a controller" asks whether a pad
is attached, and the client answered that with `isPad` — does this device's
source class include gamepad or joystick. That is the right question for
ROUTING an event and the wrong one for presence: devices publish inputs that
claim the source class while being no such thing (OEM game-mode overlays, the
gaming-phone shoulder triggers), and one of them is enough to pin the console
UI on forever, because a pad that was never there can never disconnect.
`pads()` now filters on `looksLikeController`: the source claim AND hardware
behind it — a stick, a HAT, or the A/B face buttons — on a device the platform
did not synthesize itself. The claim is cheap; the hardware is not. `isPad`
keeps its looser meaning for the event lane, where it is correct. It is not a
complete defence (an OEM device that declares BTN_GAMEPAD and two axes is
indistinguishable from a pad at this layer), so the master switch stays the
guaranteed way out — and the Controllers screen still lists everything real in
one column or the other, which is where someone looks when the client's idea of
"a pad is attached" disagrees with the room.
The picture sat in the top-left corner. The ASurfaceControl layer composites
into the SurfaceView's on-screen rectangle, read once at `surfaceCreated` — but
the stream screen hides the system bars and switches the window to draw into
the display cutout a frame or two later, and each of those grows the view under
a surface that is never recreated. The layer went on painting at the size it
started with, anchored at the origin. It passed on glass because a device whose
bars were already hidden when the surface arrived never sees the gap.
The size is now live: a packed atomic on the session handle, seeded by
`nativeStartVideo`, re-reported by `nativeVideoSurfaceSize` from every
`surfaceChanged`, and read by the layer before each present. One atomic load per
frame, and rotation and multi-window come along for free.
Verified: `:kit:cargoNdkClippy` (arm64 + armv7, deny warnings), `:kit:` and
`:app:` unit tests, and the native crate's own suite. The new JNI symbol is
exported in the built `.so`.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Two field reports from one Android user, with one shape between them: a decision taken once, at a moment when the answer was still wrong, and never revisited.
The console UI could not be dismissed
"With a controller" asks whether a pad is attached, and the client answered that with
isPad— does this device's source class include gamepad or joystick. That is the right question for routing an event and the wrong one for presence: devices publish inputs that claim the source class while being no such thing (OEM game-mode overlays, gaming-phone shoulder triggers), and one of them is enough to pin the console UI on forever, because a pad that was never there can never disconnect.pads()now filters onlooksLikeController: the source claim and hardware behind it — a stick, a HAT, or the A/B face buttons — on a device the platform did not synthesize itself. The claim is cheap; the hardware is not.isPadkeeps its looser meaning for the event lane, where it is correct.This is not a complete defence, and the code says so: an OEM device that declares
BTN_GAMEPADand a pair of axes is indistinguishable from a pad at this layer. The master switch stays the guaranteed way out — and #354, merged a few hours ago, is what put that switch inside the console where the trapped user can reach it. The two are complementary: #354 gives the escape hatch, this makes the mode row actually decide something. The Controllers screen still lists every real device in one column or the other, which is where someone looks when the client's idea of "a pad is attached" disagrees with the room.The picture sat in the upper-left corner
A regression in the ASurfaceControl backend from #290. The layer composites into the SurfaceView's on-screen rectangle, read once at
surfaceCreated— butStreamScreenhides the system bars and switches the window toLAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYSin aDisposableEffectthat runs a frame or two after first layout, and each one grows the video view under a surface that is never recreated. OnlysurfaceChangedfires, so the layer went on painting at its start-up size, anchored at the origin. It passed on glass because a device whose bars are already hidden when the surface arrives never sees the gap; rotation and multi-window had the same hole.The size is now live: a packed
Arc<AtomicU64>onSessionHandle, seeded bynativeStartVideo, re-reported by the newnativeVideoSurfaceSizefrom everysurfaceChanged, and read byLayerbefore each present. One atomic load per frame, and rotation and multi-window come along for free.Verified
:kit:cargoNdkClippy— arm64-v8a + armeabi-v7a, deny warnings, aftercargo clean -p punktfunk-client-android --target aarch64-linux-androidso the crate was genuinely re-checked and not served from cache.:kit:testDebugUnitTestand:app:testDebugUnitTest, plus the native crate's own 31 tests.PadPresenceTest(4 cases) covers the presence truth table at a pure seam, since anInputDevicecannot be built off-device. It lives in:kit:, which CI runs unfiltered — unlike the:app:allowlist, so it actually gates.Java_io_unom_punktfunk_kit_NativeBridge_nativeVideoSurfaceSizeconfirmed exported in the built.so, so the newexternal funlinks at runtime.Rebased onto current main (which brought the Skia console and #357's pad HID work) and re-run green there, not just where it was written.
Neither fix has been on glass — no device here. The corner fix is the one worth a live look, and it is cheap: start a stream and watch the picture fill the panel from the first frame.