Skip to content

Add native Android media controls for HA media_player entities - #6626

Draft
FletcherD wants to merge 137 commits into
home-assistant:mainfrom
FletcherD:feature/native-media-controls
Draft

Add native Android media controls for HA media_player entities#6626
FletcherD wants to merge 137 commits into
home-assistant:mainfrom
FletcherD:feature/native-media-controls

Conversation

@FletcherD

@FletcherD FletcherD commented Mar 25, 2026

Copy link
Copy Markdown

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_player entities 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

  • New or updated tests have been added to cover the changes following the testing guidelines.
  • The code follows the project's code style and best_practices.
  • The changes have been thoroughly tested, and edge cases have been considered.
  • Changes are backward compatible whenever feasible. Any breaking changes are documented in the changelog for users and/or in the code for developers depending on the relevance.

Screenshots

notification_shade_media_controls settings_media_controls_entry_dark media_controls_settings_dark

Link to pull request in documentation repositories

User Documentation PR: home-assistant/companion.home-assistant#1304

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 configured media_player and map HA state/attributes into media metadata and supported commands
  • Adds HaMediaSessionService and HaRemoteMediaPlayer to 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)

Comment thread app/lint-baseline.xml Outdated
Comment thread automotive/lint-baseline.xml Outdated
@FletcherD
FletcherD force-pushed the feature/native-media-controls branch from c9a2d04 to 785d6a5 Compare March 25, 2026 07:04

@TimoPtr TimoPtr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't check the code yet, I'm curious could it also work on the watch? (In another PR)

@TimoPtr

TimoPtr commented Mar 25, 2026

Copy link
Copy Markdown
Member
Screen_recording_20260325_145042.mp4

When you open the setting screen, it has a weird animation that shouldn't be there.
On a wider screen it looks off (margin on the right side).
The screen feels empty I wonder if we can do something about it, like showing how it looks like?

@TimoPtr

TimoPtr commented Mar 25, 2026

Copy link
Copy Markdown
Member

Why limiting to only one entity ? The service does support multiple sessions it would be nice to be able to track multiple entities as per the documentaion

However, if you're capable of handling multiple playbacks and want to keep their sessions while the app is in the background, create multiple sessions and add them to this service with addSession(MediaSession).

An extension to your PR (in another one) would be that we should be able to send a media player from HA to the phone through a command so that from an automation you can dynamically add/remove a session. (if you think it is feasible let's create an issue)

image "This phone" is wrong it should be probably the entity name, when clicking on it I can change the volume of my phone which is wrong. --> You probably need to set `.setDeviceInfo(deviceInfo)` on the State object and also use the device type `PLAYBACK_TYPE_REMOTE`. --> For the volume something similar like `setDeviceVolume` Or try to disable it in the scope of this PR.

@TimoPtr
TimoPtr marked this pull request as draft March 25, 2026 14:54
@TimoPtr

TimoPtr commented Mar 25, 2026

Copy link
Copy Markdown
Member

@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.

@FletcherD

FletcherD commented Mar 26, 2026

Copy link
Copy Markdown
Author

Thanks for the comments, they're good ideas.
I refactored to support multiple entities, now you can add and remove them in the setting.

"This phone" is wrong it should be probably the entity name, when clicking on it I can change the volume of my phone which is wrong. --> You probably need to set .setDeviceInfo(deviceInfo) on the State object and also use the device type PLAYBACK_TYPE_REMOTE. --> For the volume something similar like setDeviceVolume Or try to disable it in the scope of this PR.

I exposed volume set/adjust which also allows the volume to be adjusted with the hardware buttons.
Also the device info so the badge shows "Other device". As far as I can tell there's no way to set the name here, it is determined by the OS

@TimoPtr TimoPtr mentioned this pull request Mar 26, 2026
4 tasks
FletcherD added 6 commits May 15, 2026 16:17
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.
Two compounding problems caused the notification to lag several seconds
behind the HA frontend when skipping songs quickly:

1. collect {} is sequential: every state emission waited for the previous
   artwork load (including PNG encoding) to complete before processing.
   Rapid skipping queued all intermediate states, showing each song in
   turn long after it stopped playing. Fix: switch to collectLatest so
   a new emission cancels the in-flight artwork fetch for the previous
   state, and stale intermediate states are never processed.

2. PNG encoding of large album art took 6–7 s (a 4000×4000 bitmap with
   CompressFormat.PNG, 100 — PNG ignores the quality parameter and always
   does its slowest pass). Fix: switch to CompressFormat.JPEG, 90, which
   encodes in milliseconds.

Additionally, the player is now updated immediately on each state arrival
(with cached bytes if the URL is unchanged, or null bytes if it changed),
so metadata and playback state reach the notification without waiting for
the IO-bound artwork fetch to complete.

Rename artworkPngBytes → artworkBytes throughout.
- Switch collect → collectLatest so rapid track changes cancel
  stale in-flight artwork loads, preventing a queue of intermediate
  states from showing in turn.

- Update player immediately on each state arrival (metadata, playback
  state) without waiting for IO; keep old artwork bytes in the player
  until the new artwork finishes loading, eliminating the blank gap
  that appeared between tracks when the URL changed.

- Switch artwork encoding from PNG to JPEG (quality 90). PNG ignored
  the quality parameter and always ran its slowest compression pass;
  a 4000×4000 image took 6–7 s. JPEG encodes the same image in
  milliseconds.

- Replace fixed-square prescaling with a proportional scaleDownIfNecessary
  helper that mirrors the AOSP algorithm: only scales down when the bitmap
  exceeds notification_large_icon_width, preserves aspect ratio. Runs on
  IO to avoid the StrictMode CustomViolation triggered on API 36+.

- Add detailed Timber logging throughout the artwork path (state arrival,
  cache hits/misses, URL resolution, Coil fetch, JPEG encoding, scaling,
  player updates) to support timeline analysis.

- Rename artworkPngBytes → artworkBytes throughout.

@jpelgrom jpelgrom left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's way too much complexity in one PR here IMO but that's probably too late now. Only checked the app module so far.

Comment on lines +219 to +227
private suspend fun <T> loadRegistry(serverId: Int, name: String, loader: suspend (Int) -> List<T>?): List<T> =
try {
loader(serverId).orEmpty()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Timber.e(e, "Couldn't load $name for server $serverId")
emptyList()
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function feels weird to have here. @TimoPtr was this previously discussed in the huge list of comments? I would consider this maybe as a generic util function?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it was not.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed this pattern is used pretty much the same in other ViewModels like ManageTilesViewModel and ManageAndroidAutoViewModel. Would definitely make sense to extract it to a shared util as a follow up.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or extract it in a shared util function before, so we have less code in this PR

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted it into a shared util: loadListOrEmpty in common/.../common/util/ListLoader.kt (with unit tests), and this ViewModel now uses it. I kept the migration of ManageTilesViewModel/ManageAndroidAutoViewModel out of this PR to avoid growing it further — happy to do that in a small follow-up PR.

}

findPreference<PreferenceCategory>("media_controls")?.let {
it.isVisible = !isAutomotive && !QuestUtil.isQuest

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not on Automotive? It may be in the previous comments but there is too much too read all of them to find out.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea if it will work on automotive, since Google doesn't allow apps to open settings on automotive apparently. But TimoPtr says an incoming change may allow it, and anyway it doesn't hurt to allow it on automotive, so I'll add it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've merged the PR that allow opening the settings so it should allow setting up the media player controls. But it needs to be tested before allowing it. You can do it by using a automotive emulator.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please test this, as it will reduce complexity in this PR if we don't have to exclude Automotive.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on the thread above — I reverted the automotive enablement for now so it can be verified properly on an automotive emulator in a dedicated follow-up PR. The settings entry is hidden on Automotive again (!isAutomotive), matching the previously reviewed state.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I've tested by removing this and it doesn't crash but does nothing. So keep it like this.

FletcherD added 8 commits May 25, 2026 23:23
Pressing the volume button triggered a ~1-2 second backward jump in the
media notification seek bar. The pending future created by volume commands
deferred getState() until the WebSocket volume update arrived. That update
only changes volume_level (HA uses delta encoding), leaving media_position
unchanged. When getState() re-anchored SimpleBasePlayer to the same stale
raw position, the bar jumped back by the time elapsed since the last HA
position report.

Fix: track the position anchor locally using the device clock inside
HaRemoteMediaPlayer. The anchor only resets when media_position actually
changes value or when playback transitions from non-playing to playing.
Volume/mute/shuffle/repeat updates leave the anchor untouched, so
getState() computes the correct compensated position (anchor + elapsed)
and there is no visible jump.
Switching from DefaultMediaNotificationProvider to a custom buildNotification()
dropped the setMediaButtonPreferences() call that registers shuffle/repeat buttons
with the session. Without it, Media3 only surfaces the default transport controls
(play/pause, next, previous) in the media widget; shuffle and repeat, being
non-standard, are omitted.

Fix: call session.setMediaButtonPreferences() on every notification update with
CommandButton entries for shuffle (ICON_SHUFFLE_ON/OFF) and repeat (ICON_REPEAT_OFF/
ALL/ONE) when those commands are available. Each button encodes the target state as
the second setPlayerCommand argument so tapping cycles to the correct next mode.
- Remove redundant KDoc main-thread prose covered by @mainthread annotations
- Use @return tag format for boolean property KDocs
- Remove redundant @OptIn(UnstableApi::class) on buildNotification() (class-level annotation covers it)
- Infer buildMap type params; use bitmap.scale() KTX extension over Bitmap.createScaledBitmap
- Fix scaleDownIfNecessary KDoc reference (was pointing at nonexistent Icon method)
- Convert // field comment to KDoc on activeSessions
- Merge stacked comment block into KDoc on onUpdateNotification
- Remove duplicate zombie-guard comment (covered by the inline comment below it)
- Rename "Main dispatcher" to "Main thread" in tearDownSession/launchSession KDocs
- Remove Player. qualifiers from inherited COMMAND_* and REPEAT_MODE_* constants
- Add @return tag to handleCommand KDoc; fix block comment to KDoc on VOLUME_SCALE
- Inline C.TIME_UNSET; remove single-use DURATION_UNSET_US alias
- Remove private val from backgroundDispatcher constructor param (only used in init)
- Remove redundant KDoc block from test (test name is self-documenting)
- Consolidate two withContext(Dispatchers.Main) blocks in reconcileSessions
  into one wrapping the full body, using return@withContext for early exit
- Rename desiredKeys/currentKeys to desiredSessionIds/currentSessionIds
  to better reflect that the IDs are composite serverId:entityId strings
- Guard NotificationChannel creation with SDK_INT >= O check since minSdk
  is 23 and NotificationChannel is an API 26 class
Remove the !isAutomotive guard from the media controls settings
preference so it is visible on Automotive OS. Add the service
declaration and FOREGROUND_SERVICE_MEDIA_PLAYBACK permission to
the automotive manifest so HaMediaSessionService can run there.
Identifying the entity to remove by index is racy: the configured list
can change between the user tapping remove and the coroutine executing.
Removing by config equality is safe regardless of concurrent updates.
@jpelgrom

jpelgrom commented Jun 8, 2026

Copy link
Copy Markdown
Member

@FletcherD There are some merge conflicts and open comments to check. Please also rebase this on the latest commit from main/merge main so all tests in CI will work.

@TimoPtr
TimoPtr marked this pull request as draft June 9, 2026 08:06
FletcherD added 7 commits July 3, 2026 22:01
…controls

# Conflicts:
#	app/src/main/kotlin/io/homeassistant/companion/android/settings/SettingsFragment.kt
#	app/src/main/res/xml/changelog_master.xml
#	common/schemas/io.homeassistant.companion.android.database.AppDatabase/52.json
#	common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/Entity.kt
#	common/src/main/kotlin/io/homeassistant/companion/android/common/data/servers/ServerManagerImpl.kt
#	common/src/test/kotlin/io/homeassistant/companion/android/common/data/servers/ServerManagerImplTest.kt
Main independently used database version 52, so the media control table
migration moves to version 53.

Claude-Session: https://claude.ai/code/session_012gGmFUzuWpqFhWUcEnXDvC
- Restore smart cast in HaRemoteMediaPlayer.updateState that the earlier
  ktlint reformat broke by hoisting the expression out of the null check
- Drop ConsoleLogExtension/ConsoleLogRule usages; main replaced them with
  the auto-registered ConsoleLogPlatformListener

Claude-Session: https://claude.ai/code/session_012gGmFUzuWpqFhWUcEnXDvC
Automotive support needs to be verified on an automotive emulator before
it can ship (settings access on Automotive via home-assistant#6834 is still pending
Google approval). Defer it to a follow-up PR to keep this one testable.

This reverts commit b263ef0.

Claude-Session: https://claude.ai/code/session_012gGmFUzuWpqFhWUcEnXDvC
Addresses review feedback that the generic load-or-empty pattern in
MediaControlSettingsViewModel should live in a shared util. Other
ViewModels using the same pattern (ManageTilesViewModel,
ManageAndroidAutoViewModel) can migrate in a follow-up PR.

Claude-Session: https://claude.ai/code/session_012gGmFUzuWpqFhWUcEnXDvC
The reasoning is already documented on scaleDownIfNecessary itself.

Claude-Session: https://claude.ai/code/session_012gGmFUzuWpqFhWUcEnXDvC
Aligns with the convention introduced on main and addresses the
ObsoleteSdkInt lint warning flagged on the automotive variant.

Claude-Session: https://claude.ai/code/session_012gGmFUzuWpqFhWUcEnXDvC
@FletcherD

Copy link
Copy Markdown
Author

Merged latest main and addressed the open comments:

  • Merge with main: resolved conflicts; since main also introduced database version 52, the media_control_entity_config table moved to version 53 with its own auto-migration and generated schema.
  • Shared util: the registry-loading helper is now loadListOrEmpty in common (with unit tests); migrating the other ViewModels that use the same pattern can be a small follow-up PR.
  • Automotive: reverted the enablement for now — it needs verification on an automotive emulator, which I'd like to do in a dedicated follow-up PR once Add action to open Settings View from Automotive main screen #6834 is confirmed working.
  • Cleaned up the remaining doc/lint items (KDoc duplication, SdkVersion.isAtLeast for the notification channel check).

Media-control unit tests, :common tests, and ktlint all pass locally.

@FletcherD
FletcherD marked this pull request as ready for review July 4, 2026 05:34
@home-assistant
home-assistant Bot requested a review from TimoPtr July 4, 2026 05:34
@TimoPtr

TimoPtr commented Jul 6, 2026

Copy link
Copy Markdown
Member

Merged latest main and addressed the open comments:

* **Merge with main**: resolved conflicts; since main also introduced database version 52, the `media_control_entity_config` table moved to version 53 with its own auto-migration and generated schema.

* **Shared util**: the registry-loading helper is now `loadListOrEmpty` in `common` (with unit tests); migrating the other ViewModels that use the same pattern can be a small follow-up PR.

* **Automotive**: reverted the enablement for now — it needs verification on an automotive emulator, which I'd like to do in a dedicated follow-up PR once [Add action to open Settings View from Automotive main screen #6834](https://github.com/home-assistant/android/pull/6834) is confirmed working.

This has been approved by Google FYI.

* Cleaned up the remaining doc/lint items (KDoc duplication, `SdkVersion.isAtLeast` for the notification channel check).

Media-control unit tests, :common tests, and ktlint all pass locally.

* Call [observe] to start the session. The session and its Media3 resources are created when
* [observe] is called and released automatically when the calling coroutine is cancelled.
*
* @param context Application context used for Coil image loading and [MediaSession] construction.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @param context Application context used for Coil image loading and [MediaSession] construction.
* @param context Application context.

You'r also using it for other things like string loading.

Comment on lines +261 to +264
// SupervisorJob without a parent: command failures don't propagate to the
// observation scope, and this scope does not block coroutineScope from completing
// when the entity state flow ends naturally. Cancelled explicitly in the finally block.
val commandScope = CoroutineScope(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// SupervisorJob without a parent: command failures don't propagate to the
// observation scope, and this scope does not block coroutineScope from completing
// when the entity state flow ends naturally. Cancelled explicitly in the finally block.
val commandScope = CoroutineScope(
// Dedicated scope to not block coroutineScope from completing
// when the entity state flow ends naturally. Cancelled explicitly in the finally block.
val commandScope = CoroutineScope(

else -> Unit
}
}
Timber.d("startObservingState: flow collection ended for ${config.entityId}")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are still collecting

Suggested change
Timber.d("startObservingState: flow collection ended for ${config.entityId}")

Comment on lines +355 to +361
/**
* FLAG_ACTIVITY_NEW_TASK is required when starting an activity from a service context
* (PendingIntents from notifications always fire in a non-Activity context).
* FLAG_ACTIVITY_SINGLE_TOP prevents stacking a redundant WebViewActivity if one is
* already at the top; onNewIntent delivers the path to the existing instance instead.
*/
val tapIntent = LaunchActivity.newInstance(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/**
* FLAG_ACTIVITY_NEW_TASK is required when starting an activity from a service context
* (PendingIntents from notifications always fire in a non-Activity context).
* FLAG_ACTIVITY_SINGLE_TOP prevents stacking a redundant WebViewActivity if one is
* already at the top; onNewIntent delivers the path to the existing instance instead.
*/
val tapIntent = LaunchActivity.newInstance(
val tapIntent = LaunchActivity.newInstance(

}

override fun onDestroy() {
Timber.d("HaMediaSessionService destroyed")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's call this at the end of the method.

)
if (state.configuredEntityItems.none { it.config == config }) {
val newConfigs = state.configuredEntityItems.map { it.config } + config
mediaControlRepository.setConfiguredEntities(newConfigs)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a simpler addConfig to the repository would simplify things.

}

private suspend fun loadMediaPlayerEntities(serverId: Int): List<Entity> =
loadListOrEmpty("media_player entities for server $serverId") {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the impl of loadListOrEmpty within this file for now the name is missleading out of context. I'll work on improving the EntityPicker experience but for now keep things here.

deviceRegistry = uiState.deviceRegistryForServer(uiState.selectedServerId),
areaRegistry = uiState.areaRegistryForServer(uiState.selectedServerId),
modifier = modifier.padding(horizontal = HADimens.SPACE4),
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image It misses a bit of space

}

findPreference<PreferenceCategory>("media_controls")?.let {
it.isVisible = !isAutomotive && !QuestUtil.isQuest

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I've tested by removing this and it doesn't crash but does nothing. So keep it like this.

fun observeConfiguredEntities(): Flow<List<MediaControlEntityConfig>>

/** Replaces the full list of configured media_player entities. */
suspend fun setConfiguredEntities(entities: List<MediaControlEntityConfig>)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace this by 2 method add/remove

@home-assistant
home-assistant Bot marked this pull request as draft July 7, 2026 09:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants