Unable to surface error messages in Android Auto when onGetChildren fails
I'm adding Android Auto support to a Media3-based audio library and need to surface errors to users when browsing fails in onGetChildren (for example, due to network or authentication problems).
However, any time onGetChildren() returns LibraryResult.ofError(), Android Auto displays only “No items”. I haven’t found a way to show a meaningful error message.
What I’ve tried
-
Returning different
SessionErrortypes:LibraryResult.ofError(SessionError.ERROR_UNKNOWN) -
Returning a
SessionErrorwith a custom message:LibraryResult.ofError(SessionError(SessionError.ERROR_UNKNOWN, "Something went wrong")) -
Sending an error separately:
val error = SessionError( SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED, "Error message" ) session.sendError(browser, error) -
Using
LibraryParamswith extras:val errorExtras = Bundle().apply { putString( MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT, "Error" ) } val errorParams = LibraryParams.Builder() .setExtras(errorExtras) .build() LibraryResult.ofError(error, errorParams)
None of these approaches result in Android Auto showing the provided error message. The UI always falls back to “No items”.
Question
How can an app display a custom error message in Android Auto when a browse operation fails in onGetChildren? Is this currently supported in Media3, and if so, what is the correct mechanism? Apps like Spotify appear to show error messages when browsing fails, so I’m assuming there must be a supported path.
Environment
- Media3: 1.8.0 / 1.9.0-alpha01
- Device: Pixel 6a (API 36.0)
- Android Auto: 15.5
Thanks for your excellent report and problem description.
I think this is a bug.
How can an app display a custom error message in Android Auto when a browse operation fails in onGetChildren?
I think the intended way would be LibraryResult.ofError(SessionError(SessionError.ERROR_UNKNOWN, "Something went wrong")) as you suggest above.
For the case of Android Auto that is still using the old legacy API, Media3 needs to translate this back to the legacy API. As far as I can see in the source code, this does not happen. If I'm not mistaken, we drop the error message around [1].
I need to look into this some more closely because I don't exactly know what API AAuto is using for getting the error message.
I'll be back on this issue when I have some news to share. Thanks again for your report!
[1] https://github.com/androidx/media/blob/1.4.0/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java#L428
I think I was a bit quick in saying this is a bug in Media3 error handling.
Android Auto subscribes to the browsable media items it get presented by the MediaLibrarySession.Callback by calling subscribe(parentId) for parent items. Long story short, this boils down to MediaBrowserService not doing any error handling in [1] so we can't send the error back with the async Result but must retrun null.
is this currently supported in Media3, and if so, what is the correct mechanism? Apps like Spotify appear to show error messages when browsing fails, so I’m assuming there must be a supported path.
I wonder what error message screen you see.
Auto documents in [2] that an app can set a fatal error state in the PlaybackStateCompat. AFAIKT this shows the error message on the playback screen.
Can you try to set the error replication mode to fatal and see whether this creates the user experience you are aiming for?
MediaLibrarySession.Builder(this, player, callback)
.setLibraryErrorReplicationMode(MediaLibrarySession.LIBRARY_ERROR_REPLICATION_MODE_FATAL)
.build()
The error message you return with LibraryResult.ofError(SessionError(SessionError.ERROR_UNKNOWN, "Something went wrong")) is replicated to the error message of PlaybackStateCompat. That's already happening by default with LIBRARY_ERROR_REPLICATION_MODE_NON_FATAL
If you change to fatal, then the playback state of PlaybackStateCompat is set to STATE_ERROR in addition to the error message. So this may work . Please note that this actually signals to the media session that there is a playback error. So you would see the notification on mobile in error state as well I guess. You would probably want to stop payback accordingly.
Apps like Spotify appear to...
You can verify whether they do the same, by checking the mobile notification or by inspecting the session state when dumping adb shell dumpsys media_session.
[1] https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/media/java/android/service/media/MediaBrowserService.java;l=875 [2] https://developer.android.com/training/cars/media/errors
Using setLibraryErrorReplicationMode together with
LibraryResult.ofError(SessionError(SessionError.ERROR_UNKNOWN, "Something went wrong"))
still produces the same behavior for me. I pushed a minimal example to the demo-session-service here: https://github.com/puckey/media/commit/89320dec4c64a941ec51f3194c48943262c305be
When running the demo and launching it from the DHU, I get the "no items" screen again:
I also tried to recreate the Spotify browse error for comparison. Previously I could trigger it by disabling Wi-Fi and opening the Browse tab. This time, though, the system detects that Wi-Fi is off and surfaces the error as a media item in the list. Earlier I was getting a full-screen, centered error message with larger text.
If there’s no way to force that behavior, I can also fall back to exposing errors as media items, though I’m still hoping to match the full-screen error state if possible.
I'd like to add my 2 cents here as someone having spent quite some time circumventing Android Auto limitations in terms of errors (especially after Google Play rejections related to non-detailed error texts).
Per my understanding, the only true error in terms of Android Auto occurs only on Player#Listener#onPlayerErrorChanged called with an exception. Only then the full-screen, centered error message with larger text will be presented (see the screenshot). This implies that there should be some item playing, which might not be the case when building the list.
We had a slightly different request from the design team: to present a customized error message when requesting the media items for some items from our backend for the already built list returned 401 or 403. I came up with a workaround which is not straight-forwardly applicable to your use case since you might not have an active media item when building a content list, but maybe you'll get an idea on how to extend it: https://github.com/androidx/media/issues/543#issuecomment-2503634829.
This could be simplified further a bit (if you're using the latest media3 version): https://github.com/androidx/media/issues/543#issuecomment-2913140986. I'm wondering if this method could work for you when building the content lists? I unfortunately cannot check it right away since we have yet to update to 1.8.0
For the time being, within our app, for errors when building content lists our design team and I agreed upon just return a MediaItem with title set to an error text.
Many thanks for your input @NikSatyr ! Very useful!
Android Auto occurs only on Player#Listener#onPlayerErrorChanged called with an exception
I think a playback error boils down to a fatal exception in the legacy playback state that Auto is reading [1].
If working as intended, this can be done with Media3 in two ways:
Error replication
The preferred way is using LIBRARY_ERROR_REPLICATION_MODE_FATAL. When this is set a SessionError returned from a domain service method of the callback like onGetChildren() is translated to such a fatal error in the platform playbackState that Auto is reading from [1]. When using LIBRARY_ERROR_REPLICATION_MODE_FATAL then any SessionError is translated to a fatal error in the PlaybackState of the platform [3][5]. When not fatal the error state is not set, but only the error message [4].
MediaLibrarySession.Builder(this, player, callback)
.setLibraryErrorReplicationMode(MediaLibrarySession.LIBRARY_ERROR_REPLICATION_MODE_FATAL)
.build()
This is the preferred way because it's the intention that an app doesn't need to know whether a clinet is using the legacy API or the Media3 API. We do an effort to provide a Media3 API that should result in sensible and backwards-compatible behavior for apps accessing a Media3 app.
Error replication has been introduced mainly for Android Automotive that has an authentication work flow with a activity for authentication. AFAIKT, for AAOS the non-fatal replication mode is sufficient. If you know this isn't the case please let me know. We are very much keen on getting feedback for these cases as they can be kind of tricky as we can't exactly know what apps are doing. So please tell me if we are doing the wrong thing for your use case.
custom playback exception
We introduced custom playback errors (with 1.8 I believe). With that an app can simulate a playback error for a given controller. So while the actual player may still work, a PlaybackException can be set for a specific controller. That specific controller then gets the Player#Listener#onPlayerErrorChanged callback called while other controllers still see the actual state of the player (please see [2]).
This would be another way to tell Auto that something is wrong. I was able to produce the full screen error panel with a custom error message with that. As I said, my understanding is that error replication in fatal mode should result in the same. If this isn't the case I'd say this is a bug. Please shout if you observe this.
remarks
-
Obviously I'd ask apps to do the error replication mode. When later Auto/AAOS changes to use the Media3 on their side, then Auto.AAOS would consume the
SessionErrordirectly. With error replication, an app would then not be required to be changed their code to support Auto/AAOS with Media3 (in the future, there may also be users with AAOS/Auto clients that are still using the legacy API while newer/updated devices use Media3). -
Using a fatal error is IMO a bit disruptive because it signals a playback error. Any controller (like SystemUI) would then display a playback error which isn't necessarily true I think. To be consistent, an app probably want's to pause/stop playback when a fatal error is simulated with any of the techniques available.
-
Lastly, you are probably aware that if you signal errors to Android Auto, then any other controller that is reading from the platform session sees the same error and acts upon this. There is not way to sent this error to Andorid Auto or another legacy controller only. It's all or nothing. This is different for Media3 controller which is why we think the Media3 approach is superior.
Please let me know if any of my above talking isn't working as intended or doesn/t make sense. I'll see that I can take some time next week to change our demo app so that I can sensibly send such errors as a result of Auto calling for instance onGetChildren(). My current hacky approach just always throws an error which leads to strange behavior. I'm sorry to have to ask you for patience before I can give you some better grounded results that I tested myself with the Auto emulator. I'll give my best to get on this next week. Feel free to nudge me here on this bug if I'm late.
[1] https://developer.android.com/training/cars/media/errors [2] https://developer.android.com/media/media3/session/control-playback#error-handling [3] https://github.com/androidx/media/blob/release/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java#L1756-L1773 [4] https://github.com/androidx/media/blob/release/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java#L1876 [5] https://github.com/androidx/media/blob/release/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java#L1783-L1786
@marcbaechinger thanks for the details and your will to invest some time into a deeper research for this issue :) a couple of follow-ups from me
Error replication
I tried your suggestion with .setLibraryErrorReplicationMode(MediaLibraryService.MediaLibrarySession.LIBRARY_ERROR_REPLICATION_MODE_FATAL). The setup looks like this:
override fun onGetChildren(
session: MediaLibrarySession,
browser: MediaSession.ControllerInfo,
parentId: String,
page: Int,
pageSize: Int,
params: LibraryParams?
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
if (parentId == "test_error") {
return Futures.immediateFuture(
LibraryResult.ofError(
SessionError(
SessionError.ERROR_UNKNOWN,
"Custom message: something went wrong"
)
)
)
}
// return regular children
}
With this, there's no error surfaced and just "No items" state (see the screenshot).
Custom playback exception
I hacked together some quick demo based on the 1.8.0. This kinda works with the setPlaybackException, but not in an intuitive or user-friendly way: you'd still see "No items", but tapping on a player button (red equalizer at the bottom right) would open the error state (see the screenshots).
The problem with this is that if the player is already playing something, the actual player state would be overwritten with an error, which means the user loses the ability to control the active playback. Not only this is a bad UX, this is a violation of Google Play requirements to the Android Auto apps.
The setup for this scenario looks like this:
override fun onGetChildren(
session: MediaLibrarySession,
browser: MediaSession.ControllerInfo,
parentId: String,
page: Int,
pageSize: Int,
params: LibraryParams?
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
if (parentId == "test_error") {
session.setPlaybackException(
PlaybackException(
"Test error", IllegalStateException("Test"),
PlaybackException.ERROR_CODE_BAD_VALUE
)
)
return Futures.immediateFuture(
LibraryResult.ofError(
SessionError(
SessionError.ERROR_UNKNOWN,
"Customized message"
)
)
)
}
// return regular children
}
My current approach (note that I'm not the original author of the issue)
We just return a non-playable MediaItem, and get the error to be displayed in a content list like a regular clickable item. This does not affect the player state at all.
override fun onGetChildren(
session: MediaLibrarySession,
browser: MediaSession.ControllerInfo,
parentId: String,
page: Int,
pageSize: Int,
params: LibraryParams?
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
if (parentId == "test_error") {
val metadata = MediaMetadata.Builder()
.setDisplayTitle("Custom: Something went wrong")
.setTitle("Custom: Something went wrong")
.setIsBrowsable(true)
.setIsPlayable(false)
.build()
val item = MediaItem.Builder()
.setMediaId("error")
.setMediaMetadata(metadata)
.build()
return Futures.immediateFuture(LibraryResult.ofItemList(listOf(item), params))
}
// return regular children
}
Thanks for digging into this, @marcbaechinger. Looking forward to your findings.
And thanks for weighing in, @NikSatyr. The main UX drawback of exposing a browsable MediaItem with an error message as the title is that it remains clickable, which means every tap still triggers navigation and pushes the user deeper into the hierarchy.
Spotify appears to use a similar approach, but they somehow manage to present the item as greyed out and non-interactive. It would be great to understand how they achieve that, and whether this is possible through the current API. Maybe you have some insight here, @marcbaechinger.
When testing again, I was able to trigger the full-screen error view that Spotify shows within the browsing context:
For reference, adb shell dumpsys media_session output at that moment:
spotify-media-session com.spotify.music/spotify-media-session/111 (userId=0)
ownerPid=18524, ownerUid=10298, userId=0
package=com.spotify.music
launchIntent=PendingIntent{f7afa16: PendingIntentRecord{42ba4df com.spotify.music startActivity}}
mediaButtonReceiver=MBR {pi=PendingIntent{2935f97: PendingIntentRecord{e3a2539 com.spotify.music broadcastIntent}}, componentName=ComponentInfo{com.spotify.music/com.spotify.mediasession.mediasession.receiver.MediaButtonReceiver}, type=1, pkg=com.spotify.music}
active=true
flags=3
rating type=2
controllers: 6
state=PlaybackState {state=PAUSED(2), position=0, buffered position=0, speed=0.0, updated=338234484, actions=3025844, custom actions=[Action:mName='Toggle shuffle, mIcon=2131233093, mExtras=Bundle[mParcelledData.dataSize=144], Action:mName='Remove from collection, mIcon=2131233053, mExtras=Bundle[mParcelledData.dataSize=144], Action:mName='Start radio, mIcon=2131233098, mExtras=Bundle[mParcelledData.dataSize=144], Action:mName='Start repeating one track, mIcon=2131233088, mExtras=Bundle[mParcelledData.dataSize=144]], active item id=1, error=null}
audioAttrs=AudioAttributes: usage=USAGE_MEDIA content=CONTENT_TYPE_UNKNOWN flags=0x800 tags= bundle=null
volumeType=LOCAL, controlType=ABSOLUTE, max=0, current=0, volumeControlId=null
metadata: size=18, description=Envoys, Life Without Buildings, Any Other City
queueTitle=Play Queue, size=50
Here is a screenshot of the greyed out button the Spotify app is able to render in AA:
@marcbaechinger hey there, kind reminder in case this issue got lost in your tasks
Hey @marcbaechinger, any updates on this when you get a chance? Happy to help if needed!