Sunday, February 22, 2015

Sending Messages to Android Auto

    In November of 2014, Google provided developers with two simulators, one for media apps and another for message notifications, to start updating and testing apps to work with the new Android Auto platform. In a previous post, I shared how to create a service that works with Android Auto in order to play media, and in this post I will go over how to interact with and send messages to the Android Auto notification screen. All source code for this tutorial can be found on GitHub. The previous post (linked above) has instructions for installing the Auto simulators, so I'm going to skip over that in this post for brevity.

Main message screen on Android Auto
    To start, we're going to want to get everything set up. We'll do this by first making sure our app is targeting SDK version 21 or higher in build.gradle. Next we'll move over to adding in a new xml file under /res/xml called automotive_app_desc.xml and add in the following content to let Android Auto know that this app supports notifications
<automotiveApp>
    <uses name="notification"/>
</automotiveApp>
    Next we're going to want to go into AndroidManifest.xml and add a metadata tag within the application node to direct the OS towards our previously created xml file.
<meta-data android:name="com.google.android.gms.car.application"
     android:resource="@xml/automotive_app_desc" />
    In addition to our metadata tag, we're going to add two broadcast receivers with intent-filters that look for specific actions for 'read' or 'reply' user actions. While I'm using broadcast receivers here, it should be noted that you could also create Services to handle the read and reply situations instead, which I'll talk about a little more when we start tying in these broadcast receivers in code.
<receiver android:name=".AutoMessageReadReceiver" android:exported="false">
    <intent-filter>
        <action android:name="com.ptrprograms.androidautomessenger.ACTION_MESSAGE_READ"/>
    </intent-filter>
</receiver>

<receiver android:name=".AutoMessageReplyReceiver" android:exported="false">
    <intent-filter>
        <action android:name="com.ptrprograms.androidautomessenger.ACTION_MESSAGE_REPLY"/>
    </intent-filter>
</receiver>
    Now that the general setup is done, we can jump into our Java code. The way Android Auto's messaging system works is an Android application connected to Auto runs all of the logic for when a notification should be constructed, then it sends that notification to the Auto dashboard. To keep things simple, I'm going to build out the notification in our application's MainActivity.java file in order to point out what needs to be done. The first part of this should seem familiar if you've dealt with Android notifications before (and if not, I've written other posts on creating notifications): we're going to create a general NotificationCompat.Builder object.
NotificationCompat.Builder notificationBuilder =
    new NotificationCompat.Builder( getApplicationContext() )
            .setSmallIcon( R.drawable.ic_launcher )
            .setLargeIcon( BitmapFactory.decodeResource( getResources(), R.drawable.ic_launcher ) )
            .setContentText( "content text" )
            .setWhen( Calendar.getInstance().get( Calendar.SECOND ) )
            .setContentTitle( "content title" );
    With the NotificationCompat.Builder created, we can start adding in the Android Auto portions of the code. To do this, we need to use the .extend() method of NotificationCompat.Builder to add functionality from NotificationCompat.CarExtender(), which also uses a builder pattern to add functionality to our app (more options are available than shown here, such as setting text color for the Auto notification, but I'll leave that off for now). Each message group on the Auto dashboard is called a Conversation, so we also need to create an UnreadConversation to add to our notification.
notificationBuilder.extend( new NotificationCompat.CarExtender()
        .setUnreadConversation( getUnreadConversation() ) );
   getUnreadConversation() handles creating the messages that we will display, the actions taken when the user has read them and if they reply
private NotificationCompat.CarExtender.UnreadConversation getUnreadConversation() {
    NotificationCompat.CarExtender.UnreadConversation.Builder unreadConversationBuilder =
            new NotificationCompat.CarExtender.UnreadConversation.Builder( UNREAD_CONVERSATION_BUILDER_NAME );

    unreadConversationBuilder
        .setReadPendingIntent( getMessageReadPendingIntent() )
        .setReplyAction( getMessageReplyPendingIntent(), getVoiceReplyRemoteInput() )
        .addMessage( "Message 1")
        .addMessage( "Message 2" )
        .addMessage( "Message 3" )
        .setLatestTimestamp( Calendar.getInstance().get( Calendar.SECOND ) );

    return unreadConversationBuilder.build();
}
    The important things to pay attention to here are getMessageReadPendingIntent(), getMessageReplyPendingIntent() and getVoiceReplyRemoteInput(). As mentioned before, I'm using a set of BroadcastReceivers to handle responding to when the user reads or replies to a message, but since we're using a system of PendingIntents here, we could just as easily create a set of intents that go to a service to deal with these actions. getMessageReadPendingIntent() is pretty straight forward, creating a pending intent with an action that will be caught by AutoMessageReadReceiver.java after the user has tapped on the message and it has been read aloud by the system.
private Intent getMessageReadIntent() {
    return new Intent()
        .addFlags( Intent.FLAG_INCLUDE_STOPPED_PACKAGES )
        .setAction( MESSAGE_READ_ACTION )
        .putExtra( MESSAGE_CONVERSATION_ID_KEY, 1 );
}

private PendingIntent getMessageReadPendingIntent() {
    return PendingIntent.getBroadcast( getApplicationContext(),
            1,
            getMessageReadIntent(),
            PendingIntent.FLAG_UPDATE_CURRENT );
}
    AutoMessageReadReceiver simply listens for our intent with the MESSAGE_READ_ACTION and dismisses the notification associated with the intent.
public class AutoMessageReadReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        int conversationId = intent.getIntExtra( MainActivity.MESSAGE_CONVERSATION_ID_KEY, -1 );
        Log.d( "Message", "id: " + conversationId );
        NotificationManagerCompat.from( context ).cancel( conversationId );
    }
}
Messages before they have been listened to
Message notification after all 'UnreadConversations' have been read
    The reply action is a little more interesting, though equally straight forward. We create another PendingIntent for a separate BroadcastReceiver, and also attach a RemoteInput for a voice reply with the setReplyAction method of our UnreadConversationBuilder.
private Intent getMessageReplyIntent() {
    return new Intent()
            .addFlags( Intent.FLAG_INCLUDE_STOPPED_PACKAGES )
            .setAction( MESSAGE_REPLY_ACTION )
            .putExtra( MESSAGE_CONVERSATION_ID_KEY, 1 );
}

private PendingIntent getMessageReplyPendingIntent() {
    return PendingIntent.getBroadcast( getApplicationContext(),
            1,
            getMessageReplyIntent(),
            PendingIntent.FLAG_UPDATE_CURRENT );
}

private RemoteInput getVoiceReplyRemoteInput() {
    return new RemoteInput.Builder( VOICE_REPLY_KEY )
            .setLabel( "Reply" )
            .build();
}
    where our BroadcastReceiver, AutoMessageReplyReceiver.java, not only marks a message as read, but will also extract a voice reply provided by the user in order to use it within our app.
public class AutoMessageReplyReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText( context, "Message Received", Toast.LENGTH_LONG ).show();

        int conversationId = intent.getIntExtra( MainActivity.MESSAGE_CONVERSATION_ID_KEY, -1 );
        Log.d( "Message", "id: " + conversationId );
        NotificationManagerCompat.from(context).cancel( conversationId );

        String message = getMessageFromIntent( intent );
    }

    private String getMessageFromIntent( Intent intent ) {
        //Note that Android Auto does not currently allow voice responses in their simulator
        Bundle remoteInput = RemoteInput.getResultsFromIntent( intent );
        if( remoteInput != null && remoteInput.containsKey( "extra_voice_reply" ) ) {
            return remoteInput.getCharSequence( "extra_voice_reply" ).toString();
        }

        return null;
    }
}
Reply button pressed. Currently the Android Auto simulator does not allow for voice responses, so it sends an empty string value with the RemoteInput and posts a Toast with "Canned response sent".
    Once the notification is properly set up for displaying on Auto, we use NotificationManagerCompat to display it on our device and Auto from MainActivity.java
NotificationManagerCompat.from( this ).notify( 1, notificationBuilder.build() );
    Once the notification has been sent, it will be visible to the user on the Android Auto dashboard. While this system may be simple to work with, given a proper location or context aware app, it could be an invaluable feature for users as they travel. While there are still a few things missing from the official documentation that I'd like to see, such as how to hide the reply button, I'm definitely excited to see what else becomes available as Auto is released and matures, and hope this tutorial helps others create great apps.

No comments:

Post a Comment