140 lines
7.5 KiB
Markdown
140 lines
7.5 KiB
Markdown
# 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] 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 `MusicRemoteApp`, only over
|
||
the player screen; a `LaunchedEffect` drops it when leaving the player) — no
|
||
Navigation-Compose dependency yet.
|
||
- Room to grow: password field, connection timeout, theme, keep-screen-on — and
|
||
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 `MusicRemoteApplication`) 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,
|
||
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-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.
|
||
|
||
## [ ] 5. Album / artist images
|
||
|
||
Pull artwork for the now-playing track and for the album browse grid.
|
||
|
||
- MPD serves art over the protocol via `albumart <uri> <offset>` and
|
||
`readpicture <uri> <offset>` (embedded art). **`MpdConnection` already handles
|
||
binary responses**, so the transport groundwork is done — add the commands,
|
||
loop over offsets to fetch the whole image, and decode.
|
||
- Needs an image loader + caching. Coil (`io.coil-kt`) is the standard Compose
|
||
choice; a custom `MpdArtFetcher` could feed it. Dependency → `deps.json` regen.
|
||
|
||
## [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.
|