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
This commit is contained in:
176
docs/plans/2026-03-18-media3-android-auto-design.md
Normal file
176
docs/plans/2026-03-18-media3-android-auto-design.md
Normal file
@@ -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
|
||||
<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 |
|
||||
Reference in New Issue
Block a user