androidx / androidx/media

Android Auto changed presentation logic of browsable content

Open
#2,093 12 comments 0 reactions 1 assignee View on GitHub

@marcbaechinger is already working on this.

Since Feb 3, 2025.

question
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)

Image

Whist the following shows the contents of the playlist when launching the app (Pixel 3)

Image

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.