Files
Android-247-Radio/docs/plans/2026-03-18-media3-android-auto-design.md
cottongin cfc845479b Add design doc for Media3 migration + Android Auto support
Covers architecture for replacing MediaSessionCompat with Media3
MediaLibraryService, RadioPlayerAdapter facade, browse tree for
Android Auto, and notification improvements.

Made-with: Cursor
2026-03-18 05:50:40 -04:00

177 lines
6.1 KiB
Markdown

# 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
<service
android:name=".service.RadioPlaybackService"
android:exported="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="androidx.media3.session.MediaLibraryService"/>
<action android:name="android.media.browse.MediaBrowserService"/>
</intent-filter>
</service>
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app" />
```
## New File: `res/xml/automotive_app.xml`
```xml
<?xml version="1.0" encoding="utf-8"?>
<automotiveApp>
<uses name="media"/>
</automotiveApp>
```
## 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 |