Add native Android media controls for HA media_player entities#6626
Add native Android media controls for HA media_player entities#6626FletcherD wants to merge 118 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a native Android MediaSession-backed surface for controlling a selected Home Assistant media_player entity from the notification shade, plus companion settings and supporting data plumbing.
Changes:
- Introduces
MediaControlRepository+ state model to observe a configuredmedia_playerand map HA state/attributes into media metadata and supported commands - Adds
HaMediaSessionServiceandHaRemoteMediaPlayerto expose the entity via Android’s media controls and forward transport/seek actions back to HA - Adds a new “Media controls” settings screen and preference entry to select/clear the exposed entity, plus unit tests and changelog entry
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| common/src/test/kotlin/io/homeassistant/companion/android/common/data/mediacontrol/MediaControlRepositoryImplTest.kt | Unit tests for repository configuration and HA→media state mapping |
| common/src/main/res/values/strings.xml | New UI strings for the media controls settings |
| common/src/main/kotlin/io/homeassistant/companion/android/common/data/prefs/PrefsRepositoryImpl.kt | Persists configured media control server/entity IDs; clears on server removal |
| common/src/main/kotlin/io/homeassistant/companion/android/common/data/prefs/PrefsRepository.kt | Adds prefs API for media controls configuration |
| common/src/main/kotlin/io/homeassistant/companion/android/common/data/mediacontrol/MediaControlState.kt | New state model + playback state types for media controls |
| common/src/main/kotlin/io/homeassistant/companion/android/common/data/mediacontrol/MediaControlRepositoryImpl.kt | Observes websocket entity updates and emits MediaControlState |
| common/src/main/kotlin/io/homeassistant/companion/android/common/data/mediacontrol/MediaControlRepository.kt | Repository interface for configuration + observation |
| common/src/main/kotlin/io/homeassistant/companion/android/common/data/mediacontrol/MediaControlModule.kt | Hilt binding for the new repository |
| common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/Entity.kt | Adds media_player supported-features constants and helper accessors |
| automotive/src/main/AndroidManifest.xml | Declares MediaSessionService and media playback FGS permission |
| automotive/lint-baseline.xml | Updates lint baseline (new ComposeUnstableCollections entries) |
| app/src/test/kotlin/io/homeassistant/companion/android/settings/mediacontrol/MediaControlSettingsViewModelTest.kt | Unit tests for settings ViewModel selection/save/clear behaviors |
| app/src/test/kotlin/io/homeassistant/companion/android/mediacontrol/HaRemoteMediaPlayerTest.kt | Robolectric tests for player state mapping, commands, and callbacks |
| app/src/main/res/xml/preferences.xml | Adds preference category/entry for “Media controls” |
| app/src/main/res/xml/changelog_master.xml | Adds user-facing changelog entry for the feature |
| app/src/main/res/drawable/ic_play_circle_outline.xml | New icon for the settings entry |
| app/src/main/kotlin/io/homeassistant/companion/android/webview/WebViewActivity.kt | Starts the media session service on app start when configured |
| app/src/main/kotlin/io/homeassistant/companion/android/settings/mediacontrol/views/MediaControlSettingsView.kt | Compose UI for selecting server/entity and saving/clearing |
| app/src/main/kotlin/io/homeassistant/companion/android/settings/mediacontrol/MediaControlSettingsViewModel.kt | Loads servers/entities/registries, manages selection, and starts/stops service |
| app/src/main/kotlin/io/homeassistant/companion/android/settings/mediacontrol/MediaControlSettingsFragment.kt | Fragment host for the Compose settings screen (+ help link) |
| app/src/main/kotlin/io/homeassistant/companion/android/settings/SettingsFragment.kt | Wires the new preference click to open the media controls settings |
| app/src/main/kotlin/io/homeassistant/companion/android/mediacontrol/HaRemoteMediaPlayer.kt | Media3 SimpleBasePlayer proxy translating commands to HA callbacks |
| app/src/main/kotlin/io/homeassistant/companion/android/mediacontrol/HaMediaSessionService.kt | MediaSessionService that observes HA state, loads artwork, and calls HA actions |
| app/src/main/AndroidManifest.xml | Declares MediaSessionService and media playback FGS permission |
| app/lint-baseline.xml | Updates lint baseline (new ComposeUnstableCollections entries) |
c9a2d04 to
785d6a5
Compare
Screen_recording_20260325_145042.mp4When you open the setting screen, it has a weird animation that shouldn't be there. |
|
@FletcherD it does look promising, I didn't go in details for now but I gave you some comments that are important to look at before going any further. |
|
Thanks for the comments, they're good ideas.
I exposed volume set/adjust which also allows the volume to be adjusted with the hardware buttons. |
| </intent-filter> | ||
| </service> | ||
|
|
||
|
|
There was a problem hiding this comment.
Actually it would be nice to test if it works on automotive
There was a problem hiding this comment.
Apparently there is no way to access settings on automotive at all, due to Google's restrictions, lol. So there is definitely no way to support automotive without a lot of work.
https://community.home-assistant.io/t/ha-with-android-auto-dashboard/593994/16
- Thread 33: Replace implementation-detail KDoc on MediaControlSettingsScreen with caller-facing description - Thread 35: Change subtitle parameter in ConfiguredEntityRow from String? to String; remove null-check - Thread 48: Add serverId to loadRegistry error log message - Thread 51: Replace all getSessions() calls with sessions Kotlin accessor in HaMediaSessionServiceTest
- Thread 29: Use ServiceController.of(HaMediaSessionService(observationScope), null) in tests instead of Robolectric.buildService; remove reflection-based scope injection - Thread 30: Make startObservingEntities() @VisibleForTesting internal; replace reflection-based invocation in test with direct call - Thread 32: Add notificationEntityName field (set from entityFriendlyName on Main); use as fallback in buildNotification title instead of raw id string - Thread 36: Remove Surface wrapper from ConfiguredEntityRow; apply background color directly to Row via Modifier.background - Thread 52: Consolidate entity config key — move session id format into MediaControlEntityConfig as a property
…tion On API 36+, Notification.Builder.build() calls Icon.scaleDownIfNecessary() on the Main thread, triggering a StrictMode CustomViolation that crashes debug builds via FailFast. Remove the hardcoded 256px Coil size constraint so artwork loads at native server resolution (better quality on lock screen / expanded controls). After compressing the full-resolution PNG bytes for MediaMetadata, explicitly call Bitmap.createScaledBitmap() to android.R.dimen.notification_large_icon_width on IO before passing the bitmap to setLargeIcon() — ensuring scaleDownIfNecessary has nothing to do on Main.
- Split MediaControlSettingsContent into private composables (DescriptionSection, ServerDropdownSection, EntityPickerSection) - Always render icon slot (Image or Spacer) in ConfiguredEntityRow so rows align when mixing entities with and without icons - Use Arrangement.spacedBy(SPACE3) for horizontal rhythm in ConfiguredEntityRow; remove per-child start padding
…ingsViewModel - Merge configuredEntities + entityNamesByConfig + entityIconsByConfig into List<ConfiguredEntityItem> updated atomically (thread 42, 44) - Move IIcon resolution into Compose layer via LocalContext (thread 44) - Make availableEntities a computed val on UiState instead of a stored field (thread 43), removing updateAvailableEntities() entirely (thread 46) - Invert data flow: addEntity/removeEntity write to the repository only; observeConfiguredEntities() drives configuredEntityItems reactively, eliminating the read-after-write race in persistAndNotifyService() (thread 47) and the copy() duplication in add/remove (thread 45) - Add multi-server screenshot test variant (thread 38)
Ordering of media control notifications is not important, so there is no need to persist list order in the DB.
…mandFuture in updateState() Previously handleCommand() completed the SettableFuture via invokeOnCompletion when the HTTP call returned. SimpleBasePlayer then called getState() with the stale pre-command mediaState (e.g. Playing@178s after a pause), briefly flashing the old position before the WebSocket state confirmation arrived ~25ms later. The future is now stored in pendingCommandFuture and completed by updateState() after the server-confirmed state has been written to mediaState, so getState() always sees fresh data. invokeOnCompletion is kept only as an error fallback for unrecoverable job failures.
Adds multi-server variant references and refreshes existing ones.
Resolve changelog conflict: keep 2026.5.3 version and include both the native media controls entry and the upstream bug fixes entry.

Summary
I wanted to be able to control a media player entity natively on Android without having to open the app or navigate to a widget. So this feature exposes one or more Home Assistant
media_playerentities as native Android Media Controls (described here) in the notification shade, the same UI used by other media players on Android.The media controls show the currently playing track info and play position with album art. Prev/next track, play/pause and seek controls work and are forwarded to the media_player entity (if the entity supports them).
A new "Media controls" setting is added under "Companion app" to choose which media_player entities to expose in the media controls, if any. You can choose more than one entity, in which case a notification will be created for each one.
Unit tests are added to test playback state mapping, state flow, settings and everything else I could think of.
Checklist
Screenshots
Link to pull request in documentation repositories
User Documentation PR: home-assistant/companion.home-assistant#1304