Monday, February 16, 2015

Using the Android Auto Media Browser

    During the 2014 I/O, Google made the announcement that Android would be available for in-vehicle systems, but left everyone hanging on the major details. Luckily, in November 2014 they released two simulator APKs for developers to begin testing different features of their apps for Android Auto: audio and messaging services. For this tutorial, I will go over using the Android Auto framework to browse media and create a simple media player (which can obviously be expanded on for a real application). Source code for this tutorial can be found on GitHub.

   In order to start, we'll first need to install the Media Browser Simulator APK for Android Auto onto a Lollipop device. I ended up using a 2013 Nexus 7, though a Lollipop emulator should also work for our purposes. This can be found under your Android SDK folder along the path /extras/google/simulators/media-browser-simulator.apk after installing Android Auto API Simulators from the SDK Manger. You can install this through the Android Device Bridge (ADB) with the following command:
adb install media-browser-simulator.apk
    Once this is loaded up, we should be able to open the application on our device to see a screen similar to this (minus the AndroidAutoMedia item that we'll build through this tutorial):


    Next we'll want to get going on our actual app. We'll start by creating an app in Android Studio for phone/tablet. Once that's done and we have our standard "Hello World" template together, we'll need to go under /res/xml and create a new xml file - under my source code it's called automotive_app_desc.xml, though you can call it whatever you'd like. This file will be used by our application to let Android Auto know that we're building components to work with it. For now, simply copy the following source code into the XML file that tells Android Auto that we're making a media Auto plugin.
<automotiveApp>
    <uses name="media" />
</automotiveApp>
    With our XML file created, we'll move over and open AndroidManifest.xml to make a few additions to work with Auto. Within the application tag, we're going to want to add two meta-data items - one points to the XML file we just created, and the other provides the Android Auto launcher icon used in the media browser shown above (I just used the standard ic_launcher.png).
<meta-data android:name="com.google.android.gms.car.application"
    android:resource="@xml/automotive_app_desc"/>

<meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
    android:resource="@drawable/ic_launcher" />
    After the meta-data nodes are made, we'll want to add a new Service node to the manifest, and include an intent-filter for "android.media.browse.MediaBrowserService."
<service android:name="com.ptrprograms.androidautomedia.service.AutoMediaBrowserService">
    <intent-filter>
        <action android:name="android.media.browse.MediaBrowserService" />
    </intent-filter>
</service>
    Now that the manifest is filled out, we can start working on our Java code. We'll create a service (I called mine AutoMediaBrowserService, as seen above) and extend the MediaBrowserService class provided in the SDK. There are two methods that we will want to override here in order to get started. The first is onGetRoot( String clientPackageName, int clientUid, Bundle rootHints ), which is called by the Auto media browser application when first interacting with our service. In this method, we're only going to add one line to return a new BrowserRoot object using our root identifier string (I've defined mine at the top of the class as a final variable of BROWSEABLE_ROOT), though you can also use this method to validate the calling package to verify that it should have access to your media assets and return null if it fails validation.
@Override
public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
    return new BrowserRoot(BROWSEABLE_ROOT, null);
}
    The other method that we'll need to override is onLoadChildren(String parentId, Result<List<MediaBrowser.MediaItem>> result). This method is called when the root item or any subsequent items are clicked on in the Android Auto media browser. This is where you will check the parentId parameter and build out the list of MediaBrowser.MediaItems based on that id in order to provide a file structure for your app. In order to simplify things, I've moved this logic into a separate method so that onLoadChildren() is easily digestible.
@Override
public void onLoadChildren(String parentId, Result<List<MediaBrowser.MediaItem>> result) {

    List<MediaBrowser.MediaItem> items = getMediaItemsById( parentId );
    if( items != null ) {
        result.sendResult( items );
    }

}
    Now before I go into explaining how getMediaItemsById() works for providing media items and folders, I need to go over a bit of the prep code that I placed into onCreate() for this service. I created a model object called Song that consists of general information about the media object, such as artist, title, genre, etc. and a quick generator class to create the media objects that will be used for this sample. How you create and work with your own media objects will depend on your implementation and applications, so I tried to keep mine as simple as possible The Song model object source can be found here, and the generator helper class can be found here. With that in mind, my onCreate() looks like this
    @Override
    public void onCreate() {
        super.onCreate();

        mSongs = SongGenerator.generateSongs();

        initMediaSession();
    }
    where initMediaSession simply builds out the MediaSession and Token used for this sample:
private void initMediaSession() {
    mMediaSession = new MediaSession( this, "Android Auto Audio Demo" );
    mMediaSession.setActive( true );
    mMediaSession.setCallback( mMediaSessionCallback );

    mMediaSessionToken = mMediaSession.getSessionToken();
    setSessionToken( mMediaSessionToken );
}
    With this foundation in mind, we can move over to looking at getMediaItemsById(). This method will take an id, either our root id or one associated with a clicked item in the Auto browser, and create a folder or file structure to display to the user.
    private List<MediaBrowser.MediaItem> getMediaItemsById( String id ) {
        List<MediaBrowser.MediaItem> mediaItems = new ArrayList<MediaBrowser.MediaItem>();
        if( BROWSEABLE_ROOT.equalsIgnoreCase( id ) ) {
            mediaItems.add( generateBrowseableMediaItemByGenre(BROWSEABLE_CAJUN) );
            mediaItems.add( generateBrowseableMediaItemByGenre(BROWSEABLE_JAZZ) );
            mediaItems.add( generateBrowseableMediaItemByGenre(BROWSEABLE_ROCK) );
        } else if( !TextUtils.isEmpty( id ) ) {
            return getPlayableMediaItemsByGenre( id );
        }

        return mediaItems;
    }
    As you can see, if BROWSEABLE_ROOT is the clicked item, we create three media items with the method generateBrowseableMediaItemByGenre() and pass in one of three strings defined at the top of the class, and then return those items as part of our MediaItems list. generateBrowseableMediaItemByGenre() simply creates a MediaItem with the FLAG_BROWSEABLE flag set and some basic information to define the folders, as shown here:
private MediaBrowser.MediaItem generateBrowseableMediaItemByGenre( String genre ) {
    MediaDescription.Builder mediaDescriptionBuilder = new MediaDescription.Builder();
    mediaDescriptionBuilder.setMediaId( genre );
    mediaDescriptionBuilder.setTitle( genre );
    mediaDescriptionBuilder.setIconBitmap( BitmapFactory.decodeResource( getResources(), R.drawable.folder ) );

    return new MediaBrowser.MediaItem( mediaDescriptionBuilder.build(), MediaBrowser.MediaItem.FLAG_BROWSABLE );
}
    This will provide us with three folders for three different genres under our root structure, like so


    When one of these three folders is tapped on by a user, we return to onLoadChildren(), which in turn calls getMediaItemsById() again, but this getMediaItemsById() will have an id that is not our root folder, so getPlayableMediaItemsByGenre( String genre ) is called. This will loop through all of our songs and create a list of MediaItems based on the passed genre
private List<MediaBrowser.MediaItem> getPlayableMediaItemsByGenre( String genre ) {
    if( TextUtils.isEmpty( genre ) )
        return null;

    List<MediaBrowser.MediaItem> mediaItems = new ArrayList();

    for( Song song : mSongs ) {
        if( !TextUtils.isEmpty( song.getGenre() ) && genre.equalsIgnoreCase( song.getGenre() ) ) {
            mediaItems.add( generatePlayableMediaItem( song ) );
        }
    }
    return mediaItems;
}
    where generatePlayableMediaItem( song ) creates a MediaItem with properties based on the song and the FLAG_PLAYABLE flag set
private MediaBrowser.MediaItem generatePlayableMediaItem( Song song ) {
    if( song == null )
        return null;

    MediaDescription.Builder mediaDescriptionBuilder = new MediaDescription.Builder();
    mediaDescriptionBuilder.setMediaId( song.getuId() );

    if( !TextUtils.isEmpty( song.getTitle() ) )
        mediaDescriptionBuilder.setTitle( song.getTitle() );

    if( !TextUtils.isEmpty( song.getArtist() ) )
        mediaDescriptionBuilder.setSubtitle( song.getArtist() );

    if( !TextUtils.isEmpty( song.getThumbnailUrl() ) )
        mediaDescriptionBuilder.setIconUri( Uri.parse( song.getThumbnailUrl() ) );

    return new MediaBrowser.MediaItem( mediaDescriptionBuilder.build(), MediaBrowser.MediaItem.FLAG_PLAYABLE );
}
    At this point we should be able to click through the Auto browser to get to a screen similar to this:



    Now if you remember, back in initMediaSession() we had this line:
mMediaSession.setCallback( mMediaSessionCallback );
    mMediaSessionCallback is defined at the top of our class and is meant to handle actions from the buttons available in the Auto media player. This is where we start and stop our media, define which controls are available and set the metadata for our media that will be displayed by Android Auto.
private MediaSession.Callback mMediaSessionCallback = new MediaSession.Callback() {
    @Override
    public void onPlay() {
        super.onPlay();

        toggleMediaPlaybackState( true );
        playMedia( PreferenceManager.getDefaultSharedPreferences( getApplicationContext() ).getInt( CURRENT_MEDIA_POSITION, 0 ), null );
    }

    //This is called when the pause button is pressed, or when onPlayFromMediaId is called in
    //order to pause any currently playing media
    @Override
    public void onPause() {
        super.onPause();

        toggleMediaPlaybackState( false );
        pauseMedia();
    }

    @Override
    public void onPlayFromMediaId(String mediaId, Bundle extras) {
        super.onPlayFromMediaId(mediaId, extras);

        initMediaMetaData( mediaId );
        toggleMediaPlaybackState( true );
        playMedia( 0, mediaId );
    }
};
    As you can see, I have a few helper methods here that I'll go over next. onPlayFromMediaId( String mediaId, Bundle extras ) is the callback that the system uses when a MediaItem with the FLAG_PLAYABLE property is clicked. In this callback we first set up our metadata for displaying information about the MediaItem with initMediaMetaData()
private void initMediaMetaData( String id ) {

    for( Song song : mSongs ) {
        if( !TextUtils.isEmpty( song.getuId() ) && song.getuId().equalsIgnoreCase( id ) ) {
            MediaMetadata.Builder builder = new MediaMetadata.Builder();

            if( !TextUtils.isEmpty( song.getTitle() ) )
                builder.putText( MediaMetadata.METADATA_KEY_TITLE, song.getTitle() );

            if( !TextUtils.isEmpty( song.getArtist() ) )
                builder.putText( MediaMetadata.METADATA_KEY_ARTIST, song.getArtist() );

            if( !TextUtils.isEmpty( song.getGenre() ) )
                builder.putText( MediaMetadata.METADATA_KEY_GENRE, song.getGenre() );

            if( !TextUtils.isEmpty( song.getAlbum() ) )
                builder.putText( MediaMetadata.METADATA_KEY_ALBUM, song.getAlbum() );

            if( !TextUtils.isEmpty( song.getAlbumUrl() ) )
                builder.putText( MediaMetadata.METADATA_KEY_ALBUM_ART_URI, song.getAlbumUrl() );

            mMediaSession.setMetadata( builder.build() );
        }
    }
}
     Then we toggle our MediaState and set our controls with toggleMediaPlaybackState(). Note that the controls are set using setAction on our PlaybackState.Builder() by bitwise ORing our actions, so when our media is playing we'll display the Pause, Skip to Next and Skip to Previous, and when our media is paused we'll only display the Play button.
private void toggleMediaPlaybackState( boolean playing ) {
    PlaybackState playbackState;
    if( playing ) {
        playbackState = new PlaybackState.Builder()
                .setActions( PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_SKIP_TO_NEXT | PlaybackState.ACTION_SKIP_TO_PREVIOUS )
                .setState( PlaybackState.STATE_PLAYING, 0, 1 )
                .build();
    } else {
        playbackState = new PlaybackState.Builder()
                .setActions( PlaybackState.ACTION_PLAY_PAUSE )
                .setState(PlaybackState.STATE_PAUSED, 0, 1)
                .build();
    }

    mMediaSession.setPlaybackState( playbackState );
}
 


        The final methods in our service simply control starting, pausing and resuming our media. For this sample I only have one MP3 available to keep things simple, though you can set what plays based on the media id passed to the callback methods. The MP3 I'm using here was provided by a good friend of mine, Geoff Ledak, who you can catch DJing online at AfterHoursDJs.org.
private void playMedia( int position, String id ) {
    if( mMediaPlayer != null )
        mMediaPlayer.reset();

    //Should check id to determine what to play in a real app
    int songId = getApplicationContext().getResources().getIdentifier("geoff_ledak_dust_array_preview", "raw", getApplicationContext().getPackageName());
    mMediaPlayer = MediaPlayer.create(getApplicationContext(), songId);

    if( position > 0 )
        mMediaPlayer.seekTo( position );
    mMediaPlayer.start();

}

private void pauseMedia() {
    if( mMediaPlayer != null ) {
        mMediaPlayer.pause();
        PreferenceManager.getDefaultSharedPreferences( this ).edit().putInt( CURRENT_MEDIA_POSITION,
                mMediaPlayer.getCurrentPosition() ).commit();
    }
}

@Override
public void onDestroy() {
    super.onDestroy();
    if( mMediaPlayer != null ) {
        pauseMedia();
        mMediaPlayer.release();
        PreferenceManager.getDefaultSharedPreferences( this ).edit().putInt( CURRENT_MEDIA_POSITION,
                0 ).commit();
    }

}
    One more thing we can do is add a colorAccent to our app theme in order to add a bit of personalization to fit our app under /res/values-v21
<style name="AppTheme" parent="android:Theme.Material.Light">
    <item name="android:colorAccent">@android:color/holo_green_dark</item>
</style>
    And with that, we should have a working Android Auto browser service plugin. I hope you've enjoyed the tutorial, and good luck building your own implementations for this new platform!

1 comment: