11 KiB
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 fromstatus.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 unlessconsumeis 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
releasebuild 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 (
showSettingsinEncoreApp, only over the player screen; aLaunchedEffectdrops it when leaving the player) — no Navigation-Compose dependency yet. - Playback section: a Consume mode toggle (live-bound to
status.consume, writes via theconsumecommand). 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 byEncoreApplication) now owns the singleMpdClient, so the connection survives Activity recreation and runs while the service is up.PlayerViewModelis a thin delegate over it.PlaybackService— amediaPlaybackforeground service hosting aMediaSessionCompat:- Cast-style volume:
setPlaybackToRemote(VolumeProviderCompat)(absolute, 0–100). 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-apponKeyDownhack. Rapid presses accumulate viapendingVolume. - Now-playing notification / QS / lock-screen via a
MediaStylenotification bound to the session, with prev/play-pause/next actions (MediaButtonReceiver) and metadata/position mirrored from the flows. - Needs
POST_NOTIFICATIONS(requested inMainActivity) +FOREGROUND_SERVICE[_MEDIA_PLAYBACK].
- Cast-style volume:
- In-app
CastIndicator("Controlling ") still shown above the slider. - Foreground key handling (
VolumeKeyDispatcher, delegated fromMainActivity.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, theVolumeProvidertakes over (system UI is fine there). Gated byMpdConnectionManager.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: tryalbumart(folder cover), fall back toreadpicture(embedded);albumArtresolves a track viafind … window 0:1first.binarylimitis raised on connect so covers transfer in ~1 round-trip.- Coil (
io.coil-kt.coil3) with a customFetcher/Keyerover theMpdArtDatamodel (SongArt/AlbumArt), wired as the app'sSingletonImageLoader.Factory. Disk cache means each cover is fetched from the server once ever — important for the Pi.ArtImagecomposable shows a disc placeholder underneath. Service pulls the bitmap via Coil (shared cache) intoMETADATA_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→albumartACK →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.diskCacheitself, or art is only memory-cached (re-fetched every cold start).MpdArtFetchernow 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 threwExceptionInInitializerError. 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 explicitadd(factory, MpdArtData::class). - Coil pinned to 3.0.4: 3.5+ requires
compileSdk 36(we're on 35). Bumping later means updatingflake.nixplatform + 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 throwsMpdConnectionException("connection closed mid-response"); the idle loop'scatch (IOException)callsfailAndClose()→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 parkedidleshould count as active, but the command connection sits silent; a periodicpingkeepalive 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
mediaPlaybackforeground 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
pingevery 25s on the command connection — prevents theconnection_timeoutreap 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 afterMAX_RECONNECT_ATTEMPTSdoes it surfaceError.MpdAckExceptionis explicitly not treated as a connection failure. - Volume throttle:
setvolwrites are coalesced through a conflated channel (~1 per 120 ms, latest value) — kills the flood from holding volume keys while keeping accurate accumulation viapendingVolume. - Command-list refresh:
status+currentsongfetched 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.