Android Auto changed presentation logic of browsable content
@marcbaechinger is already working on this.
Since Feb 3, 2025.
- Dominant language
- Java
- Stars
- 3k
- Forks
- 955
- Avg merge
- 12d 14h
- Merged PRs (30d)
- 2
Description
I've run into a bit of a weird issue with my production Android Auto app. In the last few days I've had quite a few users message me saying my app no longer works, instead showing a large white exclamation mark icon. I noticed Android Auto reciently updated so I suspect something has broken in that proccess.
After hours of debugging, I realised that the exclamation mark was not a random, unhelpful error message, it was my playlist which didn't specify an artwork or title specified, all of a sudden deciding to show up.
For context, my MediaLibraryService is setup as follows:
Root
|-Playlist
|-Song
|-Song
I settled on this structure as it was the only way to get a grid layout working (see my last issue https://github.com/androidx/media/issues/1112). For the last 12 months, this has worked great, when users launch my app, they skip over the root element, straight into the only defined playlist and just see the audio files available for playing.
As of this week, for some reason some users are seeing the root element which contains only a playlist whilst others see the playlists contents:
For example, the following screenshot was taken from a Pixel 7, it shows a user seeing the root object (on a Pixel 7)
Whist the following shows the contents of the playlist when launching the app (Pixel 3)
The pixel 3 behavious is the way things have worked for the last 12 month and is the desired behaviour. Weirdly, the age of the defice appears to be irrelivent as my Pixel 8 is working correctly as well.
Heres my MediaLibraryService if its of any help:
package tech.zeroed.myapp;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.OptIn;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.session.LibraryResult;
import androidx.media3.session.MediaConstants;
import androidx.media3.session.MediaLibraryService;
import androidx.media3.session.MediaSession;
import androidx.media3.session.SessionError;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.List;
@UnstableApi
public class MyAppMediaBrowserService extends MediaLibraryService implements AudioManager.OnAudioFocusChangeListener {
public static final String TAG = "MyApp_Log";
private MediaLibrarySession mediaLibrarySession;
private final MediaItem rootNode = new MediaItem.Builder()
.setMediaId("ROOT")
.setMediaMetadata(
new MediaMetadata.Builder()
.setIsBrowsable(true)
.setIsPlayable(false)
.setTitle("Root")
.setArtworkUri(Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://tech.zeroed.myapp/" + tech.zeroed.myapp.R.drawable.silentandroid))
.build())
.build();
private final MediaItem playList = new MediaItem.Builder()
.setMediaId("Playlist")
.setMediaMetadata(
new MediaMetadata.Builder()
.setIsBrowsable(true)
.setIsPlayable(false)
.setArtworkUri(Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://tech.zeroed.myapp/" + tech.zeroed.myapp.R.drawable.silentandroid))
.build())
.build();
private final MediaItem myappTitleNode = new MediaItem.Builder()
.setMediaId("MyAppTitle")
.setMediaMetadata(
new MediaMetadata.Builder()
.setIsBrowsable(false)
.setIsPlayable(true)
.setTitle("Tap to MyApp")
//.setAlbumArtist("The Silent Android")
.setArtworkUri(Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://tech.zeroed.myapp/" + tech.zeroed.myapp.R.drawable.silentandroid))
.build())
.setUri(new Uri. Builder().scheme(ContentResolver. SCHEME_ANDROID_RESOURCE).path(Integer.toString(R.raw.silent)).build())
.build();
private final MediaItem myappNode = new MediaItem.Builder()
.setMediaId("MyApp")
.setMediaMetadata(
new MediaMetadata.Builder()
.setIsBrowsable(false)
.setIsPlayable(true)
.setTitle("MyApp")
//.setAlbumArtist("The Silent Android")
.setArtworkUri(Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://tech.zeroed.myapp/" + tech.zeroed.myapp.R.drawable.silentandroid))
.build())
.setUri(new Uri. Builder().scheme(ContentResolver. SCHEME_ANDROID_RESOURCE).path(Integer.toString(R.raw.silent)).build())
.build();
private final MediaItem invisNode = new MediaItem.Builder()
.setMediaId("MyAppInvis")
.setMediaMetadata(
new MediaMetadata.Builder()
.setIsBrowsable(false)
.setIsPlayable(true)
//.setAlbumArtist("The Silent Android")
.setArtworkUri(Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://tech.zeroed.myapp/"))
.build())
.setUri(new Uri. Builder().scheme(ContentResolver. SCHEME_ANDROID_RESOURCE).path(Integer.toString(R.raw.silent)).build())
.build();
MediaLibrarySession.Callback callback = new MediaLibrarySession.Callback() {
@OptIn(markerClass = UnstableApi.class)
@Override
public ListenableFuture<LibraryResult<MediaItem>> onGetLibraryRoot(MediaLibrarySession session, MediaSession.ControllerInfo browser, LibraryParams params) {
Bundle extras = new Bundle();
extras.putInt(
MediaConstants.EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
MediaConstants.EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM
);
extras.putInt(
MediaConstants.EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
MediaConstants.EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM
);
return Futures.immediateFuture(LibraryResult.ofItem(rootNode, new LibraryParams.Builder().setExtras(extras).build()));
}
@OptIn(markerClass = UnstableApi.class)
@Override
public ListenableFuture<LibraryResult<MediaItem>> onGetItem(MediaLibrarySession session, MediaSession.ControllerInfo browser, String mediaId) {
if (mediaId.equals(myappNode.mediaId) || mediaId.equals(myappTitleNode.mediaId) || mediaId.equals(invisNode.mediaId) ) {
return Futures.immediateFuture(LibraryResult.ofItem(myappNode, null));
} else if (mediaId.equals(rootNode.mediaId)) {
return Futures.immediateFuture(LibraryResult.ofItem(rootNode, null));
} else if (mediaId.equals(playList.mediaId)) {
return Futures.immediateFuture(LibraryResult.ofItem(playList, null));
}
return Futures.immediateFuture(LibraryResult.ofError(SessionError.ERROR_BAD_VALUE));
}
@Override
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> onGetChildren(MediaLibrarySession session, MediaSession.ControllerInfo browser, String parentId, int page, int pageSize, LibraryParams params) {
ArrayList<MediaItem> items = new ArrayList<>();
if (parentId.equals(rootNode.mediaId)) {
items.add(playList);
} else if (parentId.equals(playList.mediaId)) {
items.add(invisNode);
items.add(myappTitleNode);
items.add(invisNode);
}
return Futures.immediateFuture(LibraryResult.ofItemList(items, params));
}
@Override
public ListenableFuture<List<MediaItem>> onAddMediaItems(MediaSession mediaSession, MediaSession.ControllerInfo controller, List<MediaItem> mediaItems) {
ArrayList<MediaItem> items = new ArrayList<>();
items.add(myappNode);
return Futures.immediateFuture(items);
}
@NonNull
@Override
public MediaSession.ConnectionResult onConnect(@NonNull MediaSession session, @NonNull MediaSession.ControllerInfo controller) {
Log.i(TAG, "Accepted a connection");
return new MediaSession.ConnectionResult.AcceptedResultBuilder(session)
.setAvailablePlayerCommands(
MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
.remove(COMMAND_SEEK_TO_NEXT)
.remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
.remove(COMMAND_SEEK_TO_PREVIOUS)
.remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
.remove(Player.COMMAND_GET_TIMELINE)
.build())
.build();
}
@Override
public ListenableFuture<MediaSession.MediaItemsWithStartPosition> onPlaybackResumption(MediaSession mediaSession, MediaSession.ControllerInfo controller) {
ArrayList<MediaItem> items = new ArrayList<>();
items.add(myappNode);
return Futures.immediateFuture(new MediaSession.MediaItemsWithStartPosition(items, 0, 0));
}
};
@Override
public MediaLibrarySession onGetSession(MediaSession.ControllerInfo controllerInfo) {
// If desired, validate the controller before returning the media library session
Log.i(TAG, "Started a session");
return mediaLibrarySession;
}
// Create your player and media library session in the onCreate lifecycle event
@Override
public void onCreate() {
super.onCreate();
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
AudioFocusRequest request = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(
new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
)
.setAcceptsDelayedFocusGain(true)
.setWillPauseWhenDucked(false)
.setOnAudioFocusChangeListener(this)
.build();
ExoPlayer player = new ExoPlayer.Builder(this).setAudioAttributes(androidx.media3.common.AudioAttributes.DEFAULT, false).build();
player.setRepeatMode(Player.REPEAT_MODE_ONE);
audioManager.requestAudioFocus(request);
mediaLibrarySession = new MediaLibrarySession.Builder(this, player, callback).build();
}
@Override
public void onTaskRemoved(Intent rootIntent) {
if (mediaLibrarySession != null) {
mediaLibrarySession.getPlayer().release();
mediaLibrarySession.release();
mediaLibrarySession = null;
}
super.onTaskRemoved(rootIntent);
}
// Remember to release the player and media library session in onDestroy
@Override
public void onDestroy() {
Log.i(TAG, "Closed a connection");
mediaLibrarySession.release();
mediaLibrarySession.getPlayer().release();
super.onDestroy();
}
@Override
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
Log.i(TAG, "Gained Audio Focus");
break;
case AudioManager.AUDIOFOCUS_LOSS:
Log.i(TAG, "Lost Audio Focus");
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
Log.i(TAG, "Lost Audio Focus for a bit");
break;
default:
Log.i(TAG, "Audio focus "+focusChange);
break;
}
}
}
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.