12 May 2015

Android Auto First Play

Sadly I'm not lucky enough to have an Android Auto headset in my car, nor will my current car support one. However, I am desperately keen to have Android Auto in my car, to me it make so much sense as most proprietary systems really are awful. So in lieu of an actual system to play with, I thought I’d give Android Auto app creation a go, and see how it worked.

First of all read the dev guide:

There are currently limitations, meaning only Audio and Messaging apps are available, so I thought I’d have a crack at creating an Audio app. I’m really not looking at a shiny well designed app here, I just want to get a proof of concept type app out the door.

  1. Setup
    1. Create a new project selecting Android 5.0 (Api 21) or newer as the target
    2. Import support library (22.1.1 or better) in gradle
compile 'com.android.support:appcompat-v7:22.1.1'
    1. Open SDK manager and install “Android Auto API Simulators” from the Extras branch

  1. Update Android Project to use Auto
We need to tell Android Studio we’re creating an Auto project, so create an xml folder in the res directory and add a file named
automotive_app_desc.xml
With the following contents

<automotiveApp>
    <uses name="media" />
</automotiveApp>

Now tell the manifest where to find this file by adding inside the application tag:

<meta-data android:name="com.google.android.gms.car.application" android:resource="@xml/automotive_app_desc"/>

You can also give yourself an icon for your app

<meta-data android:name="com.google.android.gms.car.notification.SmallIcon" android:resource="@mipmap/ic_launcher" />

  1. Install the simulator
This is explained here:
You basically need to use adb to install an app which is supplied in the auto simulator downloaded in step 2. You can find the apk here:
<sdk>/extras/google/simulators/media-browser-simulator.apk
This isn’t what I expected at all. I was expecting a virtual device, but instead you get a simulator that runs on your actual phone or device and simulates the two types of android auto app. It’s a bit odd, but I guess it works.
If you’re setup you should find an app on your phone named “Media Sim”, run this and you should see the Google Play App running and working fine.

Code!

OK Now we’re ready to write some code. Don’t forget, I’m just creating a proof of concept Audio app here. So instead of streaming music I’ve copied an mp3 to res/raw and I’m going to try and play this file.

Create a service in the Manifest:

<service android:name=".MusicService" android:exported="true">
    <intent-filter>
        <action android:name="android.media.browse.MediaBrowserService"/>
    </intent-filter>
</service>

Create a class in your package and make it extend MediaBrowserService. This will mean you’ve got to implement the method onLoadChildren() and onGetRoot(). Now as you will see if you walk through the Google example this is how we create a tree structure of bands, albums and songs. Meaning you can traverse your music library. I was simply looking for the quickest route through all this to display one file, so I’ve created an array list of one mediaItem which is loaded with my mp3 and returned.
If you’re struggling to figure what to do here I advise to download the Google sample:

I’ve also created a MediaSessionCallback class which extends MediaSession.Callback. As you can see by the implemented methods, this is just a callback class for the play, pause, skip etc buttons. My version is pretty quick and dirty. Google provides a standard button interface for audio apps and in order to interface with these buttons we’re going to use the MediaSession callback.

Here’s the manifest:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.wunelli.android.autotest" >

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >

        <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="@mipmap/ic_launcher" />

        <activity
            android:name=".ActivityMain"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service android:name=".MusicService" android:exported="true">
            <intent-filter>
                <action android:name="android.media.browse.MediaBrowserService"/>
            </intent-filter>
        </service>
    </application>
</manifest>



Here’s the code for the service:

package com.wunelli.android.autotest;

import android.media.MediaMetadata;
import android.media.MediaPlayer;
import android.media.browse.MediaBrowser;
import android.media.session.MediaSession;
import android.os.Bundle;
import android.service.media.MediaBrowserService;
import android.util.Log;

import java.util.ArrayList;
import java.util.List;

public class MusicService extends MediaBrowserService{

    private MediaSession mSession;
    MediaPlayer mPlayer;

    private static final String TAG = MusicService.class.getSimpleName();
    public static final String CUSTOM_METADATA_TRACK_SOURCE = "__SOURCE__";

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate");

        initMedia();

        // Start a new MediaSession
        mSession = new MediaSession(this, "MusicService");
        setSessionToken(mSession.getSessionToken());
        mSession.setCallback(new MediaSessionCallback());
        mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
    }

    @Override
    public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
        Log.d(TAG, "OnGetRoot: clientPackageName=" + clientPackageName + "; clientUid=" + clientUid + " ; rootHints=" + rootHints);

        return new BrowserRoot("__ROOT__", null);
    }

    private void initMedia(){
        mPlayer = MediaPlayer.create(this, R.raw.roboto);
    }

    @Override
    public void onLoadChildren(String parentId, Result<List<MediaBrowser.MediaItem>> result) {

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

        MediaMetadata item = new MediaMetadata.Builder()
                .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, "1")
                .putString(CUSTOM_METADATA_TRACK_SOURCE, "roboto.mp3")
                .putString(MediaMetadata.METADATA_KEY_ALBUM, "Kilroy Was Here")
                .putString(MediaMetadata.METADATA_KEY_ARTIST, "Styx")
                .putLong(MediaMetadata.METADATA_KEY_DURATION, 330000)
                .putString(MediaMetadata.METADATA_KEY_GENRE, "rock")
                .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, "album_art.jpg")
                .putString(MediaMetadata.METADATA_KEY_TITLE, "Roboto")
                .putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, 1)
                .putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, 1)
                .build();
        String musicId = item.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);

        String hierarchyAwareMediaID = "rock|" + musicId;
        MediaMetadata trackCopy = new MediaMetadata.Builder(item)
                .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, hierarchyAwareMediaID)
                .build();
        MediaBrowser.MediaItem bItem = new MediaBrowser.MediaItem(trackCopy.getDescription(), MediaBrowser.MediaItem.FLAG_PLAYABLE);
        mediaItems.add(bItem);

        result.sendResult(mediaItems);
    }

    private final class MediaSessionCallback extends MediaSession.Callback {
        @Override
        public void onPlay() {
            Log.d(TAG, "play");
            mPlayer.start();
        }

        @Override
        public void onSkipToQueueItem(long queueId) {
            Log.d(TAG, "OnSkipToQueueItem:" + queueId);
        }

        @Override
        public void onSeekTo(long position) {
            Log.d(TAG, "onSeekTo:" + position);
        }

        @Override
        public void onPlayFromMediaId(String mediaId, Bundle extras) {
            Log.d(TAG, "playFromMediaId mediaId:" + mediaId + "  extras=" + extras);
            mPlayer.start();
        }

        @Override
        public void onPause() {
            Log.d(TAG, "pause.");
            mPlayer.start();
        }

        @Override
        public void onStop() {
            Log.d(TAG, "stop.");
            mPlayer.reset();
            initMedia();
        }

        @Override
        public void onSkipToNext() {
            Log.d(TAG, "skipToNext");
        }

        @Override
        public void onSkipToPrevious() {
            Log.d(TAG, "skipToPrevious");
        }

        @Override
        public void onCustomAction(String action, Bundle extras) {
            Log.i(TAG, "Unsupported action: " + action);
        }

        @Override
        public void onPlayFromSearch(String query, Bundle extras) {
            Log.d(TAG, "playFromSearch  query=" + query);
        }
    }
}















No comments: