diff --git a/.pi/skills/yellowjacket-dev/references/android-tier.md b/.pi/skills/yellowjacket-dev/references/android-tier.md index 62ff776..4b4afd3 100644 --- a/.pi/skills/yellowjacket-dev/references/android-tier.md +++ b/.pi/skills/yellowjacket-dev/references/android-tier.md @@ -515,6 +515,75 @@ Four things about it, each of which costs an hour if met cold: script. Plug in over USB for anything longer than a couple of probes. - **The socket name carries the pid**, which changes on every launch, so it is resolved rather than remembered. +- **A reinstall resets the runtime permissions**, and the grant dialog + is a separate activity that takes focus — so the app is up, `am start` + reports "delivered to currently running top-most instance", and + `pidof` is empty because it never got to the foreground. + `dumpsys window | grep mCurrentFocus` naming + `GrantPermissionsActivity` is the tell. `adb shell pm grant + app.yellowjacket.dev android.permission.READ_MEDIA_AUDIO` (and + `POST_NOTIFICATIONS`) ahead of the launch skips it. + +### Calling a binding on the device + +**The runtime call does not go over HTTP on Android**, and this is worth +knowing before an hour is spent on it. The WebView cannot deliver a +`fetch()` POST body to `shouldInterceptRequest`, so v3 routes runtime +calls through the `addJavascriptInterface` bridge instead: the +@wailsio/runtime installs a `customTransport` that calls +`window.wails.invokeAsync(id, payload)` and receives the answer on +`window._wailsAndroidCallback`. Two consequences: + +- **`.playwright/init-events.js` does not transfer to the device.** Its + outbound half hooks `fetch`, which sees nothing here, and its + `call()` posts to `/wails/runtime`, which answers + `Invalid runtime call: missing object value` — the interceptor got the + URL with no body. Its *inbound* half is still right, because + `dispatchWailsEvent` is the entry point in every mode. +- **Hooking `fetch` from an eval is too late anyway**, on any platform: + the bundle captured its reference at module scope, so a wrapper + installed afterwards records nothing. That is why the harness is an + `initScript` and not a step in a spec. + +What works is to borrow the bridge, chaining the runtime's own callback +so its pending calls still resolve: + +```js +const pending = new Map(); +const prev = window._wailsAndroidCallback; +window._wailsAndroidCallback = (id, response, error) => { + if (!pending.has(id)) return prev && prev(id, response, error); + const p = pending.get(id); pending.delete(id); + const env = JSON.parse(response || "{}"); + return env.ok ? p.resolve(env.data ?? env.text) : p.reject(new Error(env.error)); +}; +window.__yj = { call(name, args) { + return new Promise((resolve, reject) => { + const id = "yj" + Math.random().toString(36).slice(2); + pending.set(id, { resolve, reject }); + window.wails.invokeAsync(id, JSON.stringify({ + object: 0, method: 0, windowName: "", + args: { "call-id": id, methodName: "yellowjacket/backend/" + name, args: args || [] }, + clientId: window._wails.clientId, + })); + }); +} }; +``` + +That turns the device into a tier that can be *driven* rather than only +looked at — `__yj.call("player.Player.LoadFile", [path])` and +`__yj.call("library.Library.AddLibrary", ["/sdcard/Music/..."])` are how +#53 was measured. Names are the Go ones (`GetTracks`, not +`GetAllTracks`); an unknown one comes back as a plain +`unknown bound method name`, so a wrong guess is loud. + +**Getting audio onto the phone**: `adb push` into +`/sdcard/Android/data//files/` looks like it works and then the +files are not there — scoped storage. `/sdcard/Music/...` plus +`pm grant … READ_MEDIA_AUDIO` does work, and `AddLibrary` takes the +plain path. The generated fixtures are **~2 seconds** each, which is +fine for a scan and useless for watching a seek bar, so synthesise a +long one: `ffmpeg -f lavfi -i sine=frequency=440:duration=240`. **And the reason to bother: the phone is an engine, not a screen.** The first device here renders in **Chrome 113** at 424x439 CSS px. Every diff --git a/.planning/NOTES.md b/.planning/NOTES.md index 7c507e1..4fb55b8 100644 --- a/.planning/NOTES.md +++ b/.planning/NOTES.md @@ -4242,3 +4242,63 @@ took another ~10 s to come up. Harmless here (the emulator was up before anything used it) and a straightforward race otherwise: `start` should wait for a device that is an emulator, not for whatever `pick_device` returns. Filed as #162. + +## The Android runtime transport is not HTTP (measured 2026-08-20) + +Found while trying to drive the phone for #53. `wails3` routes runtime +calls through `addJavascriptInterface` on Android, not through +`/wails/runtime` — the WebView cannot deliver a `fetch()` POST body to +`shouldInterceptRequest`, which the v3 source says in as many words +(`application_android.go`, "The Android transport"). The runtime +installs a `customTransport` over `window.wails.invokeAsync(id, +payload)` and takes the answer on `window._wailsAndroidCallback`. + +Two things follow, and both cost time before the source was read: + +- **`.playwright/init-events.js` does not transfer to the device.** Its + outbound half hooks `fetch`; a POST to `/wails/runtime` answers + `Invalid runtime call: missing object value`, which reads like a + wrong payload shape and is actually the interceptor receiving a URL + with no body at all. The payload shape was right the whole time. Its + *inbound* half is still correct, because `dispatchWailsEvent` is the + entry point in every mode. +- **Hooking `fetch` from an `eval` is too late on any platform.** The + bundle captured its reference at module scope, so a wrapper installed + afterwards records nothing — which is exactly why the harness is an + `initScript`. Measured: zero calls captured while the app was + demonstrably making them. + +The working recipe is in `android-tier.md`; it chains the runtime's own +callback rather than replacing it, so its pending calls still resolve. +This is what makes the device a tier that can be *driven*. + +## #53's frontend is byte-identical to the build it was reported against (2026-08-20) + +`git diff v0.3.1 HEAD -- frontend/src/components/audio-player/seekbar/ +frontend/src/store/player-store.ts` is **empty**; the whole diff in that +area is `backend/player/`. The phone carries the released `v0.3.1`, so +whatever #53 saw, the component was not what changed — and v0.4.0 is +where the player audit (#122–#127) landed. + +Measured on that phone, current `main`, with a synthesised 4-minute +track: the Now Playing seek bar tracks correctly when mounted +mid-playback (`seekValue` 28 of 240), when the view is opened before +playback starts, after a tap on the track, and across an activity +recreation (same pid, bar resumes at 30 → 35). The issue's stated +symptom did not reproduce in any of them. + +Reverting **only** `backend/player/` to v0.3.1 — the frontend and +everything else at HEAD — does reproduce a real position defect on the +same device: six seconds into a 20-second file with no database row, +played after a 240-second one, the bar read **01:27 of 240**. That is +#125's stale `trackLengthMs` ("cleared only by UnloadTrack, so a file +with no row inherited the previous track's duration"), and it is fixed +at HEAD. Note the *shape* of it: the fraction is roughly right and the +absolute numbers are wrong, so it presents as a clock that lies rather +than as a handle that will not move. + +The one-line experiment is worth remembering: v0.3.1's `backend/player` +compiles against HEAD with a single shim +(`SetPlaybackFinishedHandler` gained a `srcErr error` parameter), which +makes "did the backend fix cause this" a ten-minute question instead of +a full checkout.