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 74 commits intohome-assistant:mainfrom
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. |
TimoPtr
left a comment
There was a problem hiding this comment.
I didn't review in depth the screen and tests. I focused on the session and service that we get right.
Please also take a look at my previous comment about the behavior. Test it manually with multi session and some edge cases.
It takes a lot of effort to review.
| <string name="matter_commissioning_cancelled">Unable to add Matter device?</string> | ||
| <string name="media_controls">Media controls</string> | ||
| <string name="media_controls_summary">Control a media player from the notification shade</string> | ||
| <string name="media_control_description">Select one or more media player entities to show as native media controls in the notification shade. You can control playback without opening the app.</string> |
There was a problem hiding this comment.
The description and screenshot of the PR needs to be updated.
| @@ -1,6 +1,8 @@ | |||
| package io.homeassistant.companion.android.settings.server | |||
|
|
|||
| import android.content.Context | |||
| @@ -1,6 +1,8 @@ | |||
| package io.homeassistant.companion.android.settings.server | |||
|
|
|||
| import android.content.Context | |||
|
|
||
| import android.content.Context | ||
| import androidx.preference.PreferenceDataStore | ||
| import dagger.hilt.android.qualifiers.ApplicationContext |
|
|
||
| import android.content.Context | ||
| import androidx.preference.PreferenceDataStore | ||
| import dagger.hilt.android.qualifiers.ApplicationContext |
| @@ -0,0 +1,408 @@ | |||
| package io.homeassistant.companion.android.mediacontrol | |||
|
|
|||
| import android.os.Looper | |||
|
|
||
| org.junit.Assert.assertFalse(scope.isActive) | ||
| } | ||
|
|
…bSocket subscription when we shouldn't Remove unused HaMediaNotificationProvider.kt Move commandCallback out of init block Rename reconnect() to observe()
…stead convert observe() to a suspend fun that owns the full resource lifecycle and provides the scope. Add an ArtworkCache value class to store artwork url and bytes.
Add missing SEEK commands
MediaControlSettingsScreen.kt:
Use HADropdownMenu
Extract Key function
Use takeIf to get friendlyName
MediaControlSettingsViewModel.kt:
Fix drag-to-reorder
Launch the init coroutine on Dispatchers.Default
- Entity.kt: Fix log message typo "get getVolumeMuted" → "get volumeMuted"
- strings.xml: Pluralise media_controls_summary ("a media player" → "media players")
- HaMediaSessionTest.kt: Add test for callMediaAction exception-catch path (line 228)
- HaMediaSessionService.kt: Restructure reconcileSessions to precompute diff on Default
then batch all Android/Media3 API calls into a single withContext(Main) hop
Rename startIfConfigured(context, repo) to start(context) — the service already calls stopSelf() when reconcileSessions receives an empty entity list, so the pre-check was redundant. Remove MediaControlRepository from MediaControlStarterViewModel, MediaControlSettingsFragment, and WebViewActivity accordingly.
…ocks don't need global cleanup
'position' was ambiguous — it looked like a playback position. Rename to 'index' throughout (entity, DAO, repository).
…change 'Unable to get getVolumeMuted' to match other log messages; Add OptIn import
…rselves, Override onUpdateNotification() with Per-Session Notification IDs
…cation remaining when removing entity
…ing finishes, fade in animation, correct text color
…ivate Addresses PR review comments 9, 10, and 11: - activeSessions: private (tests now assert via MediaSessionService.getSessions()) - reconcileSessions: private (no longer takes sessionScope; uses serviceScope directly) - startObservingEntities: @VisibleForTesting internal (no longer takes scope params) - serviceScope: moved to @VisibleForTesting primary constructor; secondary @Inject no-arg constructor provides the production default, following the AudioUrlPlayer pattern - Tests replace serviceScope via reflection after Robolectric.buildService().get() so the test-controlled UnconfinedTestDispatcher scope is used without calling onCreate() - Default entity state flow changed to non-completing MutableSharedFlow so observe() stays suspended, keeping sessions alive in getSessions() for assertions - Fixed pre-existing bug in onTaskRemoved: anyPlaying was computed but never used; stopSelf() now only called when nothing is playing
…the ViewModel and replace it with testDispatcher in tests
…rate reference screenshots
Fix lint errors (notification permissions)


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 a Home Assistant
media_playerentity 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 entity to expose in the media controls, if any. One entity at a time can be selected.
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