Commit Graph
4 Commits
Author SHA1 Message Date
KeelrLeek afcd3cc7f3 android: the media mirror grows a queue window the card never shows
The Kotlin side of the mirror has always known about exactly one track, and
media3's `SimpleBasePlayer.getState()` — the thing an Android Auto session is
built on — wants a playlist and an index into it. So the data lands a round
before the migration that needs it, where it cannot break anything:
`NowPlaying` and `NowPlayingArgs` carry a bounded `queue` of at most
twenty-one entries, the current item and ten either side, each with its id,
title, artist, album, duration and art key, plus `queueIndex`, where the
current one sits within that window rather than within the whole queue.
`src/mobile/media.svelte.js` fills them from `player.queue` and `player.qi`;
`MediaService` ignores every bit of it, so the card is byte for byte the card
it was. Ten either side because one covers the card's own prev and next and
nothing else while a queue list — which is what Auto asks for — wants a
screenful plus a scroll both ways, and it is a cap rather than a length: a
two-thousand-track queue crosses the bridge as twenty-one entries exactly
like a twenty-one-track one, measured at two kilobytes of JSON for twenty-one
short titles and six with long ones and real cover paths, against the thirty
to eighty of base64 cover that already ride the same call once per record.

Nothing renders it, which is the whole difficulty in checking it — and both
hops of this bridge fail silently, because serde drops an unknown field and
tauri's Jackson mapper has `FAIL_ON_UNKNOWN_PROPERTIES` off, so a key spelled
snake_case where camelCase was wanted arrives as nothing at all with no error
anywhere. `MediaSessionPlugin.update` logs one line per update, and the phone
walk holds that line against `__ss.card` — the mirror media.svelte.js now
keeps of what it last sent, the shape xfernote has kept of the shade since it
was written — field for field, through a next and a previous. The art-key
count is compared as well as the size and the index, since that is the only
camelCase field an entry has. The check arms at queue index one rather than
at wherever the walk left off, which buys a row on either side — `prev()`
seeks to zero at the top of a queue and `next()` stops at the end — a track
that has only just started, so `prev()` is still a step rather than the seek
it becomes past five seconds, and a window whose own index is the queue
index, because nothing is clamped off the front while the index is under ten.
Each of the three readings has to be a fresh one at both ends, the push
counter up and a new line in the log, since a `prev()` that lands back where
it started reads exactly like a stale line otherwise; and a queue too short
to arm, or repeat-one, under which `next()` seeks instead of stepping, fails
the check rather than skipping it.

The plugin's `consumer-rules.pro` keeps the new `@InvokeArg` class by name
beside the one it already keeps; without that line a release build would fill
`NowPlayingArgs` and leave every queue entry null, quietly. Rust and Kotlin
both changed, so this one cannot go down `dev.sh` — it needs the full
`deploy.sh` detour. And everything the walk already asserts about the card is
untouched, which is the real check on this round.
2026-09-15 07:22:02 -05:00
KeelrLeek d42645d2a0 android: the icon where it moves — the glyph, the lock screen and a splash that is not white
Both notification glyphs are the same falcon now, the logo kit's monochrome
path verbatim inside a group that re-frames it for a 24dp box — the playback
card's and the transfers notice's, because a note beside an arrow does not
read as one app, and the transfers one keeps the tray bar it always had so
the falcon dives into it. The fill stays white on transparent: the shade
composites a small icon from its alpha channel alone and tints it itself, so
a coloured export would arrive as a white blob.

The lock screen falls back to the app's own icon instead of a grey hole.
`MediaHub.art` is null from the moment a track changes until the frontend has
finished decoding its cover, and null forever for a track with none, so
`appIcon()` decodes the launcher icon ONCE — through the PackageManager,
because a library module's R has no `ic_launcher` — and hands it to both the
card and `METADATA_KEY_ALBUM_ART`. The app manifest lives in gitignored
`gen/` and declares no category, so the media-session plugin's own library
manifest contributes `android:appCategory="audio"` through the merger, and a
`Theme.App.Starting` on the brand ground replaces Android 12+'s white flash —
in `values-v31` and `values-night-v31` both, because the qualifier order puts
night mode ahead of platform version and a splash written only into
`values-v31` is silently ignored on a dark-mode phone.

The walk gains `media-card`: after the session block, the card is still
posted on the `playback` channel with the playing track's title on it, and
`media-keys` below it still gets the transport back — which is what a
drawable the shade cannot inflate would actually break. The glyph itself and
the splash are screenshots; `dumpsys notification` prints a resource id and
never a pixel, and the splash is gone before the walk's first frame.
2026-09-15 07:22:01 -05:00
KeelrLeek 07dad65ca5 android: the keep rules, and the release build that proves them
Every APK this port has produced is `assembleArm64Debug`, and the generated
project turns R8 on for `release` and only for `release` — while tauri finds a
plugin class by string, files every `@Command` under `commands[method.name]`,
and lets Jackson fill every `@InvokeArg` field by field. Minification breaks
exactly that, and it breaks it without a word: the app compiles, installs,
launches, shows Home, and then a tap on Library folder answers "No command
pickFolder found for plugin ..." in logcat and nothing on screen. So each
plugin module now carries its own `consumer-rules.pro` naming its own
reflected surface — the shape tauri-android and tauri-plugin-dialog already
use in this build, and one that travels with the plugin into a regenerated
`gen/android`, which a rule written into that gitignored tree would not.

What belongs to no single plugin is in `src-tauri/android/proguard-soulstream.pro`,
copied into the generated project by the build script because the app's own
Gradle file is regenerated: the runtime annotation attributes, a `-dontwarn`
pair for the two `java.beans` classes jackson-databind references and Android
does not have (R8 turns a missing class into a build error, so without them
there may be no APK at all), and a keep for the four `@Command` methods
declared on `app.tauri.plugin.Plugin` itself. Tauri's own rule matches classes
annotated `@TauriPlugin` and the members they declare; the base class carries
no such annotation, so nothing names `registerListener` — which is what the
lock-screen card's buttons and the download notice's Cancel and Open ride on.

`build.sh` grows `--release`: the release profile, `--features tauri/devtools`
so the walk can still attach at all, `assembleArm64Release`, and a zipalign and
sign with the same debug key the phone already trusts, so the APK installs over
the app rather than forcing an uninstall that would take Kyle's settings with
it. With no argument the script is what it has always been, cargo line for
cargo line, so deploy.sh and ship.sh keep the path they run every twenty
minutes. And it has been built. Not on the phone and not as arm64 — as **x86_64**, on an
emulator, because that is what this machine can run — but the flavour is the
only difference: the same Gradle, the same R8, the same rules, the same
zipalign and apksigner. `minifyX86_64ReleaseWithR8` succeeded and wrote **no**
`missing_rules.txt` at all, which is the `-dontwarn` pair doing its job;
`lintVitalX86_64Release` passed, so the `-x lintVital` advice is for a failure
that has not happened. The signed APK went over the debug app with
`adb install -r` and left its data alone, exactly as the same-key argument
above says it must.

Then the minified app was walked: Home, the record, playback, the queue, the
sheets, the lyrics, the drags, the export — **131 of 131, no exceptions, no
console errors, every step as expected**, and nothing in logcat about a missing
command or class. That is the failure this round exists to prevent, looked for
and not found.

What it does not cover is the hands-only list in ANDROID.md — the pickers, the
notification buttons, the share sheet — which no walk reaches, and the four
`@Command` methods on `Plugin` itself remain a precaution rather than a
diagnosed bug. The document says so, command by command.
2026-09-14 11:09:33 -05:00
KeelrLeek bb828ff2dc android: the phone, on the real core — the mockup shell, playback, the media card, the catalog, the resolver, Save, the dev flavour
The same Tauri 2 project builds for Android: mobile.html as a third Vite
entry (src/mobile/, the mockup on real data), tauri.android.conf.json, the
mobile stubs (updater, lyric alignment, vocal separation), rustls reqwest
on Android, the AAudio buffer, and a hand-driven detour build
(src-tauri/android/{env,build,deploy,dev}.sh) while Developer Mode is off.
ANDROID.md is the recipe.

Playback: the media-session plugin crate (lock-screen card, MediaStyle
notification, audio focus, headset buttons, a foreground service that
promotes itself first — Android 14 kills the app otherwise), session
restore across reload and restart, solid chrome after measuring the
backdrop-filter lag.

The catalog: Search is the desktop's top bar (the top-result rule, artist
cards), the artist page and the record page are the desktop's, Home is the
desktop's landing page, Gaps per followed shelf. Playing goes through the
desktop's own source rules, moved verbatim from state.svelte.js into
src/lib/sources.js so both apps share one resolver; the phone readies the
next three tracks and crosses boundaries gapless. Save fetches a record or
a track into the library by the download rules; Transfers shows the
burst. The raw Soulseek search stays, offered on request, a peer's copy
streaming with the lane rising as the bytes arrive. Community is the
Soulseek side (rooms, chat, buddies); the private layer waits.

The opener and storage plugins (social links, the picked library folder,
all-files access), the volume normalization switch, and the harness:
tests/mobile/smoke.mjs walks the mock (a fake network behind
src/lib/events.js) and the real phone over CDP, perf.mjs measures frames.
2026-09-13 09:30:06 -05:00