Files
minstrel/android
bvandeusen 4fde634074 feat(android): PlayerController + PlayerUiState (M8 phase 6.3)
Hilt-singleton facade over Media3 MediaController (the IPC client to
MinstrelPlayerService's MediaSession). One process-wide controller +
one StateFlow projection means ViewModels don't each attach their
own Player.Listener.

PlayerUiState: data class with currentTrack / queue / queueIndex /
isPlaying / isBuffering / positionMs / durationMs / bufferedPositionMs
/ playbackError. The mini-player + NowPlayingScreen (Phase 6.4) read
this; the rest of the app sees one consistent player snapshot.

PlayerController:
  - init: async connectAndObserve via suspendCancellableCoroutine
    bridging Media3's ListenableFuture<MediaController>.buildAsync().
    Skips the kotlinx-coroutines-guava dep (Runnable::run is a direct
    executor; the listener just unparks our continuation).
  - Transport methods (play/pause/seekTo/skipToNext/skipToPrevious)
    are no-ops until the controller connects; safe to call early.
  - setQueue(tracks, initialIndex, source) — TrackRef -> MediaItem
    with mediaId + uri + metadata; source tag goes in extras for the
    server-side rotation reporter (#415 parity).
  - Player.Listener.onEvents drives uiState snapshot — Media3 batches
    related events so we don't churn the StateFlow per-event.
  - queueRefs kept as our own list so the UiState projection has
    domain TrackRefs (Media3 has MediaItems internally).

No tests yet — PlayerController's main behavior is IPC-mediated and
benefits from an instrumented test (Robolectric or device). JVM
unit tests for it would mostly mock the MediaController and verify
trivial method-forwarding. Deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 22:06:46 -04:00
..