# 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 |