javamedia-playerandroid-auto

Android Auto desktop head unit doesn't show play/pause controls when song is selected


I've added support for Android Auto in my existing mp3 player app. Album art shows up in the desktop head unit and if I click on a song, it begins playing, but no playback controls display. It just says "Getting your selection...":

enter image description here

If I try the spotify app, it does show the playback controls. How do I get the controls to appear for my mp3 app?

Here's my MediaPlaybackService (I must be missing something!) :

public class MediaPlaybackService extends MediaBrowserServiceCompat {
    private static final String MEDIA_ROOT_ID = "media_root_id";
    private static final String EMPTY_MEDIA_ROOT_ID = "empty_root_id";
    private static final String PLAYLIST = "playlist";
    private static final String ALBUMS = "albums";
    private static final String ALBUM = "album";
    private static final String ARTISTS = "artists";
    private static final String ARTIST = "artist";
    private static final String LOG_TAG = "mediaPlaybackService";
    private static final String SONG = "song";

    private MediaSessionCompat mediaSession;
    private Result<List<MediaBrowserCompat.MediaItem>> saveResult;
    private DaoHelper daoHelper;
    private MyDatabase db;
    List<SongItem> songItemList = new ArrayList<>();

    private static final String CURRENT_MEDIA_POSITION = "media_position_key";
    private static final int PLAY = 1;
    private static final int PAUSE = 2;
    private static final int BUFFERING = 3;
    private static final int CONNECTING = 4;
    private static final int STOPPED = 5;
    //MediaPlayer mediaPlayer;
    MediaService mediaService;
    String TAG = "DJ";
    FileContentProvider fileProvider;

    @Override
    public void onCreate() {
        super.onCreate();

        db = MyDatabase.getDatabase(this, "/data/data/com.emrick.dj");
        if (db == null) {
            Log.e(TAG,"In oncreate, db is null");
        }
        daoHelper = DaoHelper.getInstance(db, null);
        if (daoHelper ==null) {
            Log.e(TAG,"DaoHelper is null");
        }

        // Create a MediaSessionCompat
        Context context = getBaseContext();
        try {
            mediaSession = new MediaSessionCompat(context, TAG);
        } catch (Exception ex) {
            Log.e(TAG,"Exception while creating media session " + ex.getMessage());
        }
        if (mediaSession == null) {
            Log.e(TAG, "initMediaSession: mediaSession = null");
            return;
        }
        mediaSession.setActive(true);
        mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
        mediaService = MediaService.getInstance();
        if (mediaService.isPlaying()) {
            setMediaPlaybackState(PLAY);
        } else {
            setMediaPlaybackState(STOPPED);
        }

        fileProvider = new FileContentProvider();
        mediaSession.setCallback(new MediaSessionCompat.Callback() {
            @Override
            public void onPrepare() {
                Log.d(TAG, "prepare called");

            }
            @Override
            public boolean onMediaButtonEvent(Intent mediaButtonIntent) {
                Log.d(TAG, "onMediaButtonEvent called: " + mediaButtonIntent);
                return false;
            }
            @Override
            public void onPause() {
                Log.d(TAG, "onPause called (media button pressed)");
                super.onPause();
                setMediaPlaybackState(PAUSE);
                mediaService.pause();
            }
            @Override
            public void onPlay() {
                Log.d(TAG, "onPlay called (media button pressed)");
                super.onPlay();
                mediaService.resume();
            }
            @Override
            public void onPlayFromMediaId(String mediaId, Bundle extras) {
                Log.d(TAG, "onPlayFromMediaId");
                String[]  tokens = mediaId.split("_");
                if (tokens.length > 0) {
                    if (mediaId.toLowerCase().equals("playlist")) {
                        if (songItemList != null && songItemList.size() > 0) {
                            mediaService.play(songItemList.get(0));
                        }
                    } else {
                        String trackIdString = tokens[1];
                        long trackId = Long.parseLong(trackIdString);
                        boolean found = false;
                        for (SongItem item : songItemList) {
                            if (item.getTrackid() == trackId) {
                                found = true;
                                mediaService.play(item);
                                setMediaPlaybackState(PLAY);
                                break;
                            }
                        }
                        if (!found) {
                            Log.d(TAG, "Song item not found in list for trackId " + trackId);
                        }
                    }
                } else {
                    Log.e(TAG,"No track id in mediaId? " + mediaId);
                }
            }
            @Override
            public void onStop() {
                Log.d(TAG, "onStop called (media button pressed)");
                super.onStop();
                setMediaPlaybackState(STOPPED);
                mediaService.stop();
            }
            @Override
            public void onRewind () {
                Log.d(TAG, "rewind called");
            }
            @Override
            public void onSkipToNext () {
                Log.d(TAG, "skip to next called");

            }
            @Override
            public void onSkipToPrevious () {
                Log.d(TAG, "skip to previous called");

            }
        }) ;

        // Set the session's token so that client activities can communicate with it.
        setSessionToken(mediaSession.getSessionToken());
    }

    @Nullable
    @Override
    public BrowserRoot onGetRoot(String clientPackageName, int clientUid,
                                 Bundle rootHints) {
        Log.d(TAG,"onGetRoot called");
        Bundle extras = new Bundle();
        extras.putInt(
                MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
                MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM);
        extras.putInt(
                MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
                MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM);

        return new BrowserRoot(MEDIA_ROOT_ID, extras);
    }

    @Override
    public void onLoadChildren(@NonNull String parentId, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
        Log.d(TAG,"onLoadChildren called " + parentId);
        saveResult = result;
        //  Browsing not allowed
        if (TextUtils.equals(EMPTY_MEDIA_ROOT_ID, parentId)) {
            result.sendResult(null);
            return;
        }

        List<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>();
        MediaDescriptionCompat.Builder descBuilder = new MediaDescriptionCompat.Builder();

        // Check if this is the root menu:
        if (MEDIA_ROOT_ID.equals(parentId)) {
            MediaBrowserCompat.MediaItem item =
                    new MediaBrowserCompat.MediaItem(descBuilder
                            .setMediaId(PLAYLIST)
                            .build(),MediaBrowserCompat.MediaItem.FLAG_BROWSABLE );
            mediaItems.add(item);

            result.sendResult(mediaItems);

        } else {
            // Examine the passed parentMediaId to see which submenu we're at,
            // and put the children of that menu in the mediaItems list...

            if (parentId.equals((PLAYLIST))) {
                Log.d(TAG,"getChildren playlist");
                daoHelper.getPlaylistSongs(this,Playlist.PLAYING_LIST);

                result.detach();
            } else if (parentId.startsWith(SONG)) {
                Log.d(TAG, "getChildren song " + parentId);

                String idArray[] = parentId.split("_");
                String id = idArray[1];
                long trackId = Long.parseLong(id);
                for (SongItem songItem : songItemList) {
                    if (songItem.getTrackid() == trackId) {
                        Uri uri = null;
                        try {
                            uri = Uri.parse(songItem.getLocation());
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                        Uri artUri = null;
                        try {
                            artUri = Uri.parse(songItem.getArtLocation());
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                        MediaBrowserCompat.MediaItem item =
                            new MediaBrowserCompat.MediaItem(descBuilder.setMediaId(id)
                                .setDescription(songItem.getName())
                                .setMediaUri(uri)
                                .setIconUri(artUri)
                                .build(), MediaBrowserCompat.MediaItem.FLAG_PLAYABLE);
                        mediaItems.add(item);
                    }
                }
                result.sendResult(mediaItems);
            }
        }
    }
    public void setSongItemList(List<SongItem> songList) {
        if (songItemList == null) {
            songItemList = new ArrayList<SongItem>();
        }
        songItemList.clear();
        songItemList.addAll(songList);
        songItemList= songList;
        List<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>();
        MediaDescriptionCompat.Builder descBuilder = new MediaDescriptionCompat.Builder();

        int count = 0;
        for (SongItem songItem:songItemList) {
            String id = SONG + "_" + songItem.getTrackid();
            Uri uri = null;
            Uri mediaUri = null;
            Bitmap art = null;
            if (songItem.getArtLocation() != null) {
                File f = new File(songItem.getArtLocation());
                
                if (f.exists()) {
                    //Must use parse, not fromFile!
                    uri = Uri.parse(songItem.getArtLocation());
                    uri = fileProvider.mapUri(uri);
                } else {
                    Log.d(TAG,"File doesn't exist:" + songItem.getArtLocation());
                }
                f = new File(songItem.getLocation());
                if (f.exists()) {
                    //Must use parse, not fromFile!
                    mediaUri = Uri.parse(songItem.getLocation());
                    mediaUri = fileProvider.mapUri(mediaUri);
                } else {
                    Log.d(TAG, "File doesn't exist: " + songItem.getLocation());
                }
            }
            Bundle extras = new Bundle();
            MediaBrowserCompat.MediaItem item =
                    new MediaBrowserCompat.MediaItem(descBuilder.setMediaId(id)
                            .setDescription(songItem.getName())
                            .setSubtitle(songItem.getArtist())
                            .setIconUri(uri)
                            .setMediaUri(mediaUri)
                            .setMediaId(id)
                            .setTitle(songItem.getName())
                            .setExtras(extras)
                            .build(),MediaBrowserCompat.MediaItem.FLAG_PLAYABLE);
            mediaItems.add(item);
            count++;
            if (count > 20) {
                break;
            }
        }

        saveResult.sendResult(mediaItems);
    }

    private void setMediaPlaybackState( int state ) {
        PlaybackStateCompat playbackState = null;
        switch (state) {
            case PLAY:
                playbackState = new PlaybackStateCompat.Builder()
                        .setActions( PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS )
                        .setState( PlaybackStateCompat.STATE_PLAYING, 0, 1 )
                        .build();
                break;

            case PAUSE:
                playbackState = new PlaybackStateCompat.Builder()
                        .setActions( PlaybackStateCompat.ACTION_PLAY_PAUSE )
                        .setState(PlaybackStateCompat.STATE_PAUSED, 0, 1)
                        .build();
                break;

            case BUFFERING:
                playbackState = new PlaybackStateCompat.Builder()
                        .setActions( PlaybackStateCompat.ACTION_STOP )
                        .setState(PlaybackStateCompat.STATE_BUFFERING, 0, 1)
                        .build();
                break;

            case CONNECTING:
                playbackState = new PlaybackStateCompat.Builder()
                        .setActions( PlaybackStateCompat.ACTION_STOP )
                        .setState(PlaybackStateCompat.STATE_CONNECTING, 0, 1)
                        .build();
                break;

            case STOPPED:
                playbackState = new PlaybackStateCompat.Builder()
                        .setActions( PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID )
                        .setState(PlaybackStateCompat.STATE_STOPPED, 0, 1)
                        .build();
                break;
        }
        mediaSession.setPlaybackState( playbackState );
    }
}

And here is what I added to the AndroidManifest:

<service
        android:name=".MediaService"
        android:enabled="true"
        android:icon="@mipmap/ic_launcher"/>

    <meta-data android:name="com.android.automotive"
        android:resource="@xml/automotive_app_desc"/>
    <meta-data android:name="com.google.android.gms.car.application"
        android:resource="@xml/automotive_app_desc"/>
    <receiver android:name="androidx.media.session.MediaButtonReceiver"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MEDIA_BUTTON" />
        </intent-filter>
    </receiver>
    <service android:name="com.emrick.dj.MediaPlaybackService"
        android:exported="true"
        android:enabled="true"
        tools:ignore="ExportedService">
        <intent-filter>
            <action android:name="android.media.browse.MediaBrowserService"/>
        </intent-filter>

    </service>
    <provider
        android:name=".utils.FileContentProvider"
        android:authorities="com.emrick.dj"
        android:exported="true" />

Solution

  • Ironically, just an hour or so after I added the bounty, I stumbled on the solution. I wanted info about the song to show on the Android Auto screen, so I found documentation on the mediasession.setMetadata() method. I added code to set the metadata and suddenly the playback controls were there (and the metadata). I think the "Getting your Selection..." message was a clue that it might have been hanging on getting metadata for the song, which was missing before.

    This has been a long project with many bumps in the road, but I think I'm in the home stretch now! Just got a new car with Android Auto support, so I can try it out on the road :).