docs: record what a phone said that no tier could
The first device run of the published APK, and the first runtime evidence any of the Android work has ever had -- A4 shipped entirely reasoned from source. It confirms A4 whole: playback survives the screen locking, and the transport notification appears with cover art, which settles four open questions at once (the service starts, the permission was granted and the notification is visible, the lock screen picks up the session, and art decoded from a MANAGE_EXTERNAL_STORAGE path by a service is readable -- the one nobody could argue from documentation). It also found the two faults fixed in the preceding commits, and the lesson worth keeping is why *those two*: both are things the platform adds rather than things the app draws. So the skill's Android tier now says to ask a device about system bars, the back gesture, focus and audio interruptions, permissions and the keyboard -- and not about layout, which the other five tiers already cover.
This commit is contained in:
@@ -3034,3 +3034,107 @@ reason this was caught is that the assertion about the probe ran before
|
||||
the assertion about the export. A fixture built by string surgery on a
|
||||
formatted constant needs to be whitespace-independent; it filters the
|
||||
list now.
|
||||
|
||||
## Long-press is one document listener, and the header row is a row (2026-08-17)
|
||||
|
||||
Plan 016 B2 phase 3. A phone has no right-click, and every context menu
|
||||
in this app opens from a `contextmenu` event — six components' worth,
|
||||
bound three different ways (delegated on a virtualizer, per row, per
|
||||
card). `frontend/src/utils/long-press.ts` is one document-capture
|
||||
listener installed once from `index.ts`: a touch that holds still for
|
||||
500 ms dispatches a synthetic `contextmenu` at the touch point, and
|
||||
**every existing handler runs unchanged**. No component opted in, and
|
||||
none can forget to.
|
||||
|
||||
Four things it has to get right, and each is a way the obvious version
|
||||
fails:
|
||||
|
||||
- **The target is `composedPath()[0]`, not `elementFromPoint`**, which
|
||||
stops at the outermost shadow host. Every menu here is bound inside
|
||||
one, so a host-targeted event reaches a delegated listener and no
|
||||
per-row one.
|
||||
- **A browser that fires its own must win.** Chromium already dispatches
|
||||
`contextmenu` on long-press; WebKit and the WebView vary. One arriving
|
||||
during the press cancels ours; one arriving after ours is swallowed at
|
||||
document capture.
|
||||
- **Ours is told from theirs by identity** (a `WeakSet`), not by
|
||||
`isTrusted`. `isTrusted` would work in the app and is untestable — no
|
||||
test can dispatch a trusted event — so the suppression path would have
|
||||
been the one thing with no coverage.
|
||||
- **The click ending the gesture is swallowed**, keyed on the gesture
|
||||
(cleared by the next `pointerdown`) rather than a time window, or a
|
||||
quick tap on the menu that just opened is eaten too.
|
||||
|
||||
**What cost the time was the assertion, not the code.** The e2e spec
|
||||
pressed `[role="row"]` — which is the *column header*, and it is the
|
||||
first one. The gesture fired correctly, the header correctly ignored it,
|
||||
and the failure looked exactly like a menu that would not open. Found by
|
||||
probing the running app (`playwright-cli eval`, dispatching the same
|
||||
pointer events and logging what saw the `contextmenu`), which showed the
|
||||
event reaching the row's own listener with no menu behind it — i.e. the
|
||||
handler was refusing it, not missing it. `.track-row` is the selector.
|
||||
|
||||
Verified by execution: 8 component tests (real browser, real shadow
|
||||
boundary, real timings) and 2 e2e specs against the running app, twice
|
||||
in a row. Not verified: any of it under a real finger on a real
|
||||
WebView — the pointer events are dispatched, because neither Desktop
|
||||
Chrome nor Desktop Safari has touch and there is no device tier.
|
||||
|
||||
## The first device run: A4 works, and two things only a phone could say (2026-08-17)
|
||||
|
||||
The published v1.5.0 APK, on a real phone, owner-reported. **This is the
|
||||
first runtime evidence any of the Android work has ever had** — A4
|
||||
shipped entirely reasoned from source.
|
||||
|
||||
**What holds.** Playback survives the screen locking. The MediaSession
|
||||
notification appears in the status pane *with album art* — which
|
||||
answers, in one observation, four of the open questions from plan 016:
|
||||
the foreground service starts, POST_NOTIFICATIONS was granted and the
|
||||
notification is visible, the session is picked up, and **cover art
|
||||
decoded from a `MANAGE_EXTERNAL_STORAGE` path by a service is
|
||||
readable**. The last was the one nobody could argue from documentation.
|
||||
|
||||
**Two bugs, and neither is visible from any tier we have.**
|
||||
|
||||
*Back did not navigate back.* The scaffold's
|
||||
`MainActivity.onBackPressed` asks `webView.canGoBack()` and finishes the
|
||||
activity otherwise — and this app had never touched `history`, so that
|
||||
was false at every depth and back quit from anywhere. The fix is in the
|
||||
frontend, not in Java: a navigation is a `history` entry now
|
||||
(`recordNavigation` in `index.ts`, same URL, the destination in the
|
||||
entry's state) and `popstate` replays it with `_isBack`. The Java half
|
||||
needs no change, because the mechanism it already uses is the one we
|
||||
were failing to feed.
|
||||
|
||||
Two rules keep it honest. The **first** navigation replaces the launch
|
||||
entry rather than pushing one, or every launch costs a back press before
|
||||
the app will close. And the in-app back buttons go through
|
||||
`history.back()` rather than popping a stack of their own — `navStack`
|
||||
is **deleted**, not kept alongside, because two stacks is exactly how
|
||||
the detail view's own button and the phone's gesture come to disagree
|
||||
about how far back one press goes. `back-navigation.spec.ts` pins that
|
||||
invariant.
|
||||
|
||||
*The transport was off screen.* **`targetSdk 35` is Android 15, which
|
||||
lays every app out edge-to-edge**, ignores the deprecated
|
||||
`statusBarColor`/`navigationBarColor` the theme still sets, and hands
|
||||
the app a window the size of the screen. The WebView is `match_parent`,
|
||||
so the page's bottom band — the transport, and on a phone the tab bar —
|
||||
was drawn underneath the gesture bar. `applyWindowInsets()` pads the
|
||||
container by `systemBars | displayCutout | ime` and returns the insets
|
||||
rather than consuming them. The window background goes black to match
|
||||
the app's own ramp, or the padding shows as a blue-grey band.
|
||||
|
||||
**Neither is findable in the browser tier, and that is the lesson worth
|
||||
keeping**: a viewport has no system bars, so `phone-shell.spec.ts` at
|
||||
390x844 renders a shell that fits perfectly while the device cuts 48dp
|
||||
off the bottom — and `page.goBack()` was never called because nothing in
|
||||
a desktop shell has a back gesture. The Android tier's own note says
|
||||
failure there is invisible; this is the milder version, where the app
|
||||
works and is simply wrong in ways only the platform can show you.
|
||||
|
||||
Verified by execution: the APK builds with the Java change; 3 e2e specs
|
||||
cover the history behaviour, on Chromium locally and WebKit in CI.
|
||||
Not verified: the insets themselves, which need the next APK on the
|
||||
owner's phone. What to look for is one thing — the transport and the tab
|
||||
bar clear of the gesture bar, and the header clear of the status bar.
|
||||
|
||||
@@ -315,8 +315,8 @@ places had to agree — `abiFilters`, the Makefile's `android:package`
|
||||
anchor is what stops it also matching the fat APK's line. Adding the
|
||||
ABI back, if modernc ever fixes `Xlstat64`, is those same three edits.
|
||||
|
||||
**B2, the desktop shell.** Scope decided (below); **phases 1 and 2 are
|
||||
done.**
|
||||
**B2, the desktop shell.** Scope decided (below); **phases 1, 2 and 3
|
||||
are done.**
|
||||
|
||||
- *Phase 1, the shell.* Below 600px the sidebar column is gone,
|
||||
`<bottom-nav>` is the primary navigation, and the shell fits 320px
|
||||
@@ -326,15 +326,47 @@ done.**
|
||||
composes the real transport components rather than copying them; and
|
||||
it hides the bottom bar while it is up, so it carries its own queue
|
||||
button.
|
||||
- *Phase 3, long-press.* `utils/long-press.ts`: one document-capture
|
||||
listener, installed once from `index.ts`, which turns a 500 ms
|
||||
stationary touch into a synthetic `contextmenu` at the touch point.
|
||||
Every menu in the app opens from that event, so all six components
|
||||
gained the gesture without one of them changing — which is the same
|
||||
argument `ContextMenuController` rests on, one layer lower. The
|
||||
details that are not obvious are in `NOTES.md` (2026-08-17); the one
|
||||
worth repeating is that ours is told from the browser's own
|
||||
long-press event by **identity**, not `isTrusted`, because a test
|
||||
cannot dispatch a trusted event and that path would otherwise be the
|
||||
only uncovered one.
|
||||
|
||||
What is left is the rest of the *interactions*: long-press for the
|
||||
context menus that are right-click today, and the track list's
|
||||
resizable columns, which are a pointer feature with no touch
|
||||
equivalent. Neither is started.
|
||||
What is left of B2 is the track list, whose resizable columns are a
|
||||
pointer feature with no touch equivalent. Not started.
|
||||
|
||||
**B3/B4** are unchanged, and B3 is now *possible* where it was not:
|
||||
with all-files access, `tagwriter` can write in place.
|
||||
|
||||
### What the first device run answered (2026-08-17)
|
||||
|
||||
A4 **works**: playback survives the screen locking, and the transport
|
||||
notification appears with cover art — which also settles the service's
|
||||
access to a `MANAGE_EXTERNAL_STORAGE` path, the permission grant and
|
||||
the lock-screen session in one observation. Everything below in "what
|
||||
none of section A answered" was written before this and is now answered
|
||||
except the OEM permission-flow variance.
|
||||
|
||||
It also found two faults no browser tier can see, both fixed and both
|
||||
awaiting the next APK for confirmation (`NOTES.md`, same date):
|
||||
|
||||
- **Back quit the app from any depth.** The scaffold asks
|
||||
`webView.canGoBack()`; the frontend had never used `history`. A
|
||||
navigation is a history entry now, and `navStack` is gone rather than
|
||||
kept beside it.
|
||||
- **The transport was under the gesture bar.** `targetSdk 35` is
|
||||
edge-to-edge by force; `applyWindowInsets()` in `MainActivity` pads
|
||||
by `systemBars | displayCutout | ime`.
|
||||
|
||||
The standing item is unchanged in kind: **B3 (tag writing) and the
|
||||
permission flow still need a device**, and so does confirming these two.
|
||||
|
||||
### What none of section A answered
|
||||
|
||||
Nothing here has been observed on a device. The permission flow in
|
||||
|
||||
Reference in New Issue
Block a user