4fde634074
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>