Files

196 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Roadmap / TODO
Near-term work after the working MVP (connect + auto-connect, now-playing with
transport/volume/options, DataStore-persisted settings). Roughly ordered by
priority; not a commitment.
Status legend: `[ ]` todo · `[~]` in progress · `[x]` done
## [x] Queue / "Up Next" view (post-roadmap)
Done. `QueueScreen` (queue icon on the player) shows **"Up Next"** — the tracks
that will auto-play from the current one to the end of the queue, so the count
reflects "how many will play before MPD stops."
- Reads the full queue via `playlistinfo` (`MpdClient.queue`), then slices from
`status.song` (current queue index) to the end. The already-played prefix is
dropped, and the slice re-derives from the live current position, so the list
shrinks as playback advances **even without consume mode** (MPD keeps played
tracks in the queue unless `consume` is on; only the current-song pointer
moves — that's why a plain queue view looked static).
- Current track is first, highlighted (bold + tinted + speaker icon). Tapping any
track jumps to it (`playid`). Full queue re-fetched on queue-version change.
- Captions the cases where the count isn't literally "tracks until stop":
repeat (loops), single (stops after current), repeat+single (repeats current).
- Rows show art thumbnails (disk-cached); art-less tracks show the disc
placeholder. Overlay nav via `PlayerOverlay.Queue`.
---
## [x] 1. Proper icons
Done. Replaced the placeholder unicode glyphs in `NowPlayingScreen.kt` with real
Material icons (`Icons.Filled.SkipPrevious/PlayArrow/Pause/SkipNext/VolumeUp`,
plus `Repeat`/`Shuffle` leading icons on the chips) from
`androidx.compose.material:material-icons-extended`.
- That artifact bundles thousands of vectors, so the **debug** APK grew ~7 MB
(expected — debug can't shrink). Enabled **R8 + resource shrinking** on the
`release` build type, which strips the unused icons: release APK is ~1.2 MB.
- Considered `material-icons-core` (too small — no media icons) and bundling
individual vector drawables (leaner but manual); the extended dep + R8 was the
best effort/quality trade.
## [x] 2. Settings menu
Done. `SettingsScreen` (reached via a gear icon on the now-playing screen) shows
the current server (host/port) with **Change server** (disconnect → connect
form) and **Reset settings** (confirm dialog → clears DataStore + disconnects).
- `SettingsRepository.clear()` wipes DataStore; `MpdConnectionManager.resetSettings()`
clears + disconnects.
- Navigation is lightweight state (`showSettings` in `EncoreApp`, only over
the player screen; a `LaunchedEffect` drops it when leaving the player) — no
Navigation-Compose dependency yet.
- **Playback section**: a **Consume mode** toggle (live-bound to `status.consume`,
writes via the `consume` command). Consume is a dynamic MPD playback option, not
static config — toggling it persists server-side (MPD state file). Verified the
switch round-trips to the server (`consume: 0 → 1`).
- Room to grow: password field, connection timeout, theme, keep-screen-on, and
the other playback toggles (single/repeat/random) could join the Playback
section. If screens multiply, revisit adopting Navigation-Compose.
## [x] 3. Cast-style volume + OS media integration
Done — full MALP-style **OS integration**, not just in-app. Architecture:
- **`MpdConnectionManager`** (app-scoped, held by `EncoreApplication`) now
owns the single `MpdClient`, so the connection survives Activity recreation and
runs while the service is up. `PlayerViewModel` is a thin delegate over it.
- **`PlaybackService`** — a `mediaPlayback` foreground service hosting a
`MediaSessionCompat`:
- **Cast-style volume**: `setPlaybackToRemote(VolumeProviderCompat)` (absolute,
0100). The OS routes hardware volume keys to the *server* volume
**system-wide, even when the app is backgrounded** (verified: 50→40 via
injected keys from the launcher), and shows the remote-volume UI. Replaced
the earlier in-app `onKeyDown` hack. Rapid presses accumulate via
`pendingVolume`.
- **Now-playing notification / QS / lock-screen** via a `MediaStyle`
notification bound to the session, with prev/play-pause/next actions
(`MediaButtonReceiver`) and metadata/position mirrored from the flows.
- Needs `POST_NOTIFICATIONS` (requested in `MainActivity`) +
`FOREGROUND_SERVICE[_MEDIA_PLAYBACK]`.
- In-app **`CastIndicator`** ("Controlling <host>") still shown above the slider.
- **Foreground key handling** (`VolumeKeyDispatcher`, delegated from
`MainActivity.dispatchKeyEvent`): while focused, volume keys are consumed and
drive the server silently — no system volume slider (the app's own slider is
the feedback). Backgrounded, the `VolumeProvider` takes over (system UI is fine
there). Gated by `MpdConnectionManager.isControllingVolume`; passes through to
local volume when not connected.
Notes: verified on the FiiO via injected key events (a rotary volume knob may not
emit `VOLUME_UP/DOWN` — hardware-dependent). Album art in the notification/session
is pending item #5.
## [x] 4. Library browse — albums
Done. `AlbumsScreen` (library icon on the player) lists every album via
`list album group albumartist` (`MpdCommands.listAlbums``MpdAlbum.listFrom`),
sorted by artist then title. Tapping an album replaces the queue and plays it
(`MpdClient.playAlbum` = `clear` + `findadd album … albumartist …` + `play`),
then returns to the now-playing screen.
- Verified live: 149 albums listed and played correctly on the FiiO.
- Placeholder disc icon per row (real art is item #5).
- Interaction choice: tap = **replace queue + play** (the direct "play this
album" gesture). Could later add a long-press / menu for "add to queue" and an
album-detail/track view. Artist browse is a future extension.
## [x] 5. Album / artist images
Done — art on the now-playing screen, the album grid, and the media
notification / lock-screen / QS card.
- `MpdClient.songArt(uri)` / `albumArt(album, artist)` loop the chunked art
protocol: try `albumart` (folder cover), fall back to `readpicture` (embedded);
`albumArt` resolves a track via `find … window 0:1` first. `binarylimit` is
raised on connect so covers transfer in ~1 round-trip.
- **Coil** (`io.coil-kt.coil3`) with a custom `Fetcher`/`Keyer` over the
`MpdArtData` model (`SongArt`/`AlbumArt`), wired as the app's
`SingletonImageLoader.Factory`. **Disk cache** means each cover is fetched from
the server once ever — important for the Pi. `ArtImage` composable shows a disc
placeholder underneath. Service pulls the bitmap via Coil (shared cache) into
`METADATA_KEY_ALBUM_ART`.
- The now-playing screen is now vertically scrollable so art doesn't clip.
- **Art connection pool** (`MpdClient`, `ART_POOL_SIZE = 4`): covers are fetched
on a dedicated pool of connections, not the command connection — so they load
~4× in parallel while scrolling *and* never block play/pause/volume. (Measured:
a batch of 8 covers went from ~1.8s serialized on the tail to ~4-way parallel.)
Each album cover is 3 round-trips (`find``albumart` ACK → `readpicture`), so
the Pi is the remaining limit; disk cache means it's a one-time cost per cover.
- **Disk-cache persistence:** Coil's disk cache is only auto-managed by its
*network* fetchers — a custom fetcher must read/write `imageLoader.diskCache`
itself, or art is only memory-cached (re-fetched every cold start). `MpdArtFetcher`
now serves from the disk snapshot when present and write-through-caches misses.
Verified: after force-stop + relaunch, 19/19 covers loaded as DISK hits, 0
server fetches. (Known minor gap: art-*less* albums aren't negative-cached, so
they re-probe the server each cold start — future optimization if it matters.)
Gotchas hit & fixed:
- **Portability bug (not art-specific):** `MpdAckException`'s ACK regex had bare
`]`/`}` — fine on the JVM (tests passed) but Android's ICU engine rejects them,
so the first ACK parsed on-device threw `ExceptionInInitializerError`. Escaped
them. This had masked all art (albumart's "no cover" ACK).
- Coil's reified `components { add(factory) }` didn't match the sealed-interface
subtypes; had to register with the explicit `add(factory, MpdArtData::class)`.
- Coil pinned to **3.0.4**: 3.5+ requires `compileSdk 36` (we're on 35). Bumping
later means updating `flake.nix` platform + AGP.
Future: online art fallback (MusicBrainz/Last.fm) for albums with no local art,
like MALP; artist images.
## [x] 6. BUG: idle connection drops after a few minutes → kicked to connect page
After a few minutes idling, the app surfaces **"connection closed mid-response"**
and falls back to the connect screen. MALP does not do this.
- Error origin: `MpdConnection.readResponse()` hits EOF and throws
`MpdConnectionException("connection closed mid-response")`; the idle loop's
`catch (IOException)` calls `failAndClose()``MpdConnectionState.Error`
UI shows the connect form.
- Likely causes to investigate:
- **Android Doze / WiFi power-save** tearing down sockets when the screen is
off or the app is backgrounded (most likely on a portable DAP/phone).
- MPD's `connection_timeout` (default 60s) closing a connection it considers
idle — a parked `idle` should count as active, but the *command* connection
sits silent; a periodic `ping` keepalive may be needed.
- NAT/router idle-connection reaping (less likely on LAN).
- Fix direction: don't treat an idle-connection drop as a fatal error — instead
**auto-reconnect transparently** (re-open connections, re-issue `idle`, resync
state) and keep showing the player. Consider a keepalive ping and, for
backgrounded playback control, a foreground service / partial wakelock.
- PARTIALLY ADDRESSED by item #3: the `mediaPlayback` foreground service keeps
the process/connection alive in the background.
**FIXED.** Root cause was two-fold: (a) the quiet command connection was reaped
by MPD's `connection_timeout`, and (b) the idle loop treated *any* `IOException`
(including that reap) as fatal → `Error` → connect page. Plus a Pi-Zero overload
angle: volume-key floods + 2-round-trip refreshes. Fixes, all in `MpdClient`
/`MpdConnectionManager`/`MpdConnection`:
- **Keepalive `ping`** every 25s on the command connection — prevents the
`connection_timeout` reap and detects a dead socket early.
- **Transparent auto-reconnect** (`triggerReconnect`/`reconnectLoop`): a transport
drop reopens both sockets in the background (state → `Connecting`, "Connecting…"
splash, player stays), retrying with backoff; only after
`MAX_RECONNECT_ATTEMPTS` does it surface `Error`. `MpdAckException` is
explicitly *not* treated as a connection failure.
- **Volume throttle**: `setvol` writes are coalesced through a conflated channel
(~1 per 120 ms, latest value) — kills the flood from holding volume keys while
keeping accurate accumulation via `pendingVolume`.
- **Command-list refresh**: `status`+`currentsong` fetched in one round-trip
(`MpdConnection.executeList`, `command_list_ok_begin`).
Verified on the FiiO: Wi-Fi drop → "Connecting…" → auto-recovers to the player
(no connect-page bounce); 6 rapid volume presses = exactly 30, throttled.