Files
encore/docs/TODO.md
T

11 KiB
Raw Blame History

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 ") 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.listAlbumsMpdAlbum.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 (findalbumart 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.