diff --git a/docs/plans/2026-03-18-media3-android-auto-design.md b/docs/plans/2026-03-18-media3-android-auto-design.md new file mode 100644 index 0000000..d194c7f --- /dev/null +++ b/docs/plans/2026-03-18-media3-android-auto-design.md @@ -0,0 +1,176 @@ +# Media3 Migration + Android Auto Support + +## Goals + +1. Proper system media notification with metadata, album art, and transport controls (Play/Stop + Seek to Live) +2. Android Auto compatibility — app registers as a media source with a browsable station catalog + +## Approach + +Migrate from legacy `MediaSessionCompat` + manual `NotificationHelper` to **Media3 `MediaLibraryService`** with a thin `Player` adapter wrapping the existing custom audio engine. The custom audio pipeline (OkHttp → IcyParser → MediaCodec → AudioTrack) remains completely untouched. + +## Architecture + +``` +RadioController ──intent──▶ RadioPlaybackService (MediaLibraryService) + │ + ├── RadioPlayerAdapter (implements Player) + │ └── delegates to AudioEngine (untouched) + │ + ├── MediaLibrarySession + │ ├── auto-publishes metadata/state/art to OS + │ ├── auto-manages notification (Play/Stop/SeekLive) + │ └── serves browse tree for Android Auto + │ + └── AudioEngine (unchanged custom pipeline) +``` + +## RadioPlayerAdapter + +A thin facade implementing `androidx.media3.common.Player` that maps the small radio-relevant command surface to existing logic. It does NOT produce or route audio. + +### Supported commands + +- `COMMAND_PLAY_PAUSE` +- `COMMAND_STOP` +- `COMMAND_GET_CURRENT_MEDIA_ITEM` +- `COMMAND_GET_MEDIA_ITEMS_METADATA` +- Custom `SessionCommand("SEEK_TO_LIVE")` — exposed as a skip-forward button + +### State mapping + +| PlaybackState (existing) | Media3 Player.STATE_* | isPlaying | +|---|---|---| +| Idle | STATE_IDLE | false | +| Connecting | STATE_BUFFERING | false | +| Playing | STATE_READY | true | +| Paused | STATE_READY | false | +| Reconnecting | STATE_BUFFERING | false | + +### Command mapping + +- `play()` → `RadioController.play(currentStation)` +- `stop()` → `RadioController.stop()` +- `pause()` → `RadioController.stop()` (radio streams don't pause) +- Metadata changes from ICY events update `MediaMetadata` on the adapter, which auto-propagates to notification/lockscreen/Auto/Bluetooth + +## RadioPlaybackService Changes + +Converts from `LifecycleService` to `MediaLibraryService`. + +### Stays the same + +- Intent-based ACTION_PLAY/STOP/PAUSE/SEEK_LIVE control flow +- AudioEngine lifecycle (create, start, stop, event collection) +- Wake lock + WiFi lock management +- Reconnect loop with backoff +- Connection span / listening session DB tracking +- StreamResolver URL resolution + +### Changes + +- `LifecycleService` → `MediaLibraryService` +- `MediaSessionCompat` → `MediaLibrarySession` (built with `RadioPlayerAdapter`) +- `onGetSession()` returns the `MediaLibrarySession` +- `NotificationHelper` deleted — Media3 auto-generates notifications from session state +- `startForeground()` calls removed — `MediaLibraryService` handles foreground lifecycle +- On ICY metadata change, adapter's `MediaMetadata` updates → auto-propagates everywhere +- Album art: `AlbumArtResolver` resolves artwork → set on `MediaMetadata.artworkUri` + +### Notification layout (auto-managed) + +- Compact: Play/Stop toggle + Seek to Live (custom command button) +- Expanded: same + album art + station name + track info +- Channel importance: LOW + +## Android Auto Browse Tree + +``` +[ROOT] + ├── Playlist: "SomaFM" + │ ├── Groove Salad + │ ├── Drone Zone + │ └── ... + ├── Playlist: "My Favorites" + │ ├── Station A + │ └── Station B + └── Unsorted (if any stations have no playlist) + ├── Station X + └── Station Y +``` + +### Callbacks + +- `onGetLibraryRoot()` → root MediaItem (browsable) +- `onGetChildren(rootId)` → playlists + unsorted folder (browsable MediaItems) +- `onGetChildren(playlistId)` → stations in playlist (playable MediaItems) +- `onGetChildren("unsorted")` → stations with no playlist +- `onSetMediaItem()` / `onAddMediaItems()` → parse mediaId, look up station, call `RadioController.play()` + +### Media IDs + +- Root: `"root"` +- Playlist folder: `"playlist:{id}"` +- Unsorted folder: `"unsorted"` +- Station item: `"station:{id}"` + +### Data freshness + +DAO `Flow` methods use `.first()` for snapshots. Updates visible on next navigation (acceptable for a radio catalog). + +## Dependencies + +### Add + +```toml +media3 = "1.6.0" +media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3" } +media3-common = { group = "androidx.media3", name = "media3-common", version.ref = "media3" } +``` + +### Remove + +- `media-session` (`androidx.media:media:1.7.1`) + +No `media3-exoplayer` needed — custom audio engine stays. + +## Manifest Changes + +```xml + + + + + + + + +``` + +## New File: `res/xml/automotive_app.xml` + +```xml + + + + +``` + +## Files Affected + +| File | Action | +|---|---| +| `libs.versions.toml` | Add media3 version + libraries, remove old media | +| `app/build.gradle.kts` | Swap dependencies | +| `AndroidManifest.xml` | Update service declaration, add Auto meta-data | +| `res/xml/automotive_app.xml` | New file | +| `RadioPlayerAdapter.kt` | New file — Player facade | +| `RadioPlaybackService.kt` | Rewrite to extend MediaLibraryService | +| `NotificationHelper.kt` | Delete | +| `RadioController.kt` | Minor — may need adapter reference for metadata updates | +| `RadioApplication.kt` | Minor — remove NotificationHelper if referenced |