Staying connected through messages is important to many drivers. Chat apps can let users know if a child needs to be picked up, or if a dinner location has been changed. The Android framework enables messaging apps to extend their services into the driving experience using a standard user interface that lets drivers keep their eyes on the road.
Apps that support messaging can extend their messaging notifications to allow Android Auto to consume them when Auto is running. These notifications are displayed in Auto and allow users to read and respond to messages in a consistent, low distraction, interface. Additionally, when using the MessagingStyle API, you get the benefit of optimized message notifications for all Android devices, including Android Auto. Such optimizations include UI that's specialized for message notifications, improved animations, and support for inline images.
This lesson assumes that you have built an app that displays messages to the user and receives the user's replies, such as a chat app. The lesson shows you how to extend your app to hand those messages off to an Auto device for display and replies.
Get started
To enable your app to provide messaging service for Auto devices, your app must be able to do the following:
- Build and send
NotificationCompat.MessagingStyle
objects that contain reply and mark-as-readAction
objects. - Handle replying and marking a conversation as read with a
Service
. - Configure your manifest to indicate the app supports Android Auto.
Concepts and objects
Before you start designing your app, it's helpful to understand how Android Auto handles messaging.
An individual chunk of communication is called a message and is represented by
the class MessagingStyle.Message
. A message contains a sender, the message
content, and the time the message was sent.
Communication between users is called a conversation and is represented by a
MessagingStyle
object. A conversation (or MessagingStyle
) contains a title,
the messages, and whether the conversation is amongst a group (that is, the
conversation has more than one other recipient).
To notify users of updates to a conversation (such as a new message), apps post
a Notification
to the Android system. This Notification uses the
MessagingStyle
object to display messaging-specific UI in the notification
shade. The Android platform also passes this Notification
to Android Auto, and
the MessagingStyle
is extracted and used to post a notification through the
car's display.
Apps can also add Action
objects to a Notification
to allow user to quickly
reply to a message or mark-as-read directly from the notification shade. Android
Auto requires the mark-as-read and reply Action
objects, in order to manage a
conversation.
In summary, a single conversation is represented by a single Notification
object that is styled with a single MessagingStyle
object. MessagingStyle
contains all the messages within that conversation with one or more
MessagingStyle.Message
objects. Finally, to be fully Android Auto compliant,
you must attach a reply and mark-as-read Action
to the Notification
.
Messaging flow
This section describes a typical messaging flow between your app and Android Auto.
- Your app receives a message.
- Your app then generates a
MessagingStyle
notification with a reply and mark-as-readAction
. - Android Auto receives “new notification” event from Android system and finds
MessagingStyle
, replyAction
, and mark-as-readAction
. - Android Auto generates and displays a notification in the car.
- If a user taps on the notification via the car's display, Android Auto
triggers the mark-as-read
Action
.- In the background, your app must handle this mark-as-read event.
- If the user responds to the notification via voice, Android Auto includes a transcription of the user's response into the reply
Action
and then triggers it.- In the background, your app must handle this reply event.
Preliminary assumptions
This page does not guide you on creating an entire messaging app. However, the code sample below includes some of the things your app should already have before you start to support messaging with Android Auto.
data class YourAppConversation(
val id: Int,
val title: String,
val recipients: MutableList<YourAppUser>,
val icon: Bitmap) {
companion object {
/** Fetches [YourAppConversation] by its [id]. */
fun getById(id: Int): YourAppConversation = // ...
}
/** Replies to this conversation with the given [message]. */
fun reply(message: String) {}
/** Marks this conversation as read. */
fun markAsRead() {}
/** Retrieves all unread messages from this conversation. */
fun getUnreadMessages(): List<YourAppMessage> { return /* ... */ }
}
data class YourAppUser(val id: Int, val name: String, val icon: Uri)
data class YourAppMessage(
val id: Int,
val sender: YourAppUser,
val body: String,
val timeReceived: Long)
Declare Android Auto support
When Android Auto receives a notification from a messaging app, it checks that the app has declared support for Android Auto. To enable this support, include the following entry in your app's manifest:
<application>
...
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc"/>
...
</application>
This manifest entry refers to another XML file that you should create with the
following path: YourAppProject/app/src/main/res/xml/automotive_app_desc.xml
,
where you declare what Android Auto capabilities your app supports. For example,
to include support for notifications, include the following in your
automotive_app_desc.xml
:
<automotiveApp>
<uses name="notification" />
</automotiveApp>
If your app requires support for handling SMS, MMS, and RCS, you must also include the following:
<automotiveApp>
...
<uses name="sms" />
</automotiveApp>
Import the AndroidX core library
Building notifications for use with Auto devices requires the AndroidX core library, which you can import into your project as follows:
- In the top-level
build.gradle
file, make sure you include Google's Maven repository, as shown below.
Groovy
allprojects { repositories { google() } }
Kotlin
allprojects { repositories { google() } }
In your app module's
build.gradle
file, include the AndroidX core library dependency, as shown below:Groovy
dependencies { implementation 'androidx.core:core:1.0.0' }
Kotlin
dependencies { implementation("androidx.core:core:1.0.0") }
Handle user actions
Your messaging app needs a way to handle updating a conversation through an
Action
. For Android Auto, there are two types of Action
objects your app
needs to handle: reply and mark-as-read. The recommended way to do this is
through the use of an IntentService
.
An IntentService
provides the flexibility of handling potentially expensive
calls in the background and frees an app's main thread.
Define intent actions
Intent
actions (not to be confused with notification actions) are simple
strings that identify what the Intent
is for. Because a single service can
handle multiple types of intents, it's easier to define multiple Intent.action
strings instead of defining multiple IntentServices
.
In our example messaging app, we have two types of actions: reply and mark-as-read, as declared in the following code sample.
private const val ACTION_REPLY = "com.example.REPLY"
private const val ACTION_MARK_AS_READ = "com.example.MARK_AS_READ"
Create the Service
To create a service that handles these Action
s, you need the conversation ID,
which is an arbitrary data structure defined by your app that identifies
the conversation, and a remote input key, which is discussed in further detail
later in this section. The following code sample creates such a service.
private const val EXTRA_CONVERSATION_ID_KEY = "conversation_id"
private const val REMOTE_INPUT_RESULT_KEY = "reply_input"
/**
* An [IntentService] that handles reply and mark-as-read actions for
* [YourAppConversation]s.
*/
class MessagingService : IntentService("MessagingService") {
override fun onHandleIntent(intent: Intent?) {
// Fetch our internal data
val conversationId = intent!!.getIntExtra(EXTRA_CONVERSATION_ID_KEY, -1)
// And search our database for that conversation
val conversation = YourAppConversation.getById(conversationId)
// Then handle the action that was requested in the intent. The TODOs
// are addressed in a section below.
when (intent.action) {
ACTION_REPLY -> TODO()
ACTION_MARK_AS_READ -> TODO()
}
}
}
To associate this service with your app, you need to also register the service in your app's manifest, as shown below.
<application>
<service android:name="com.example.MessagingService" />
...
</application>
Generate and handle Intents
There's no way for other apps to obtain the Intent
that triggers the
MessagingService
as each Intent
is passed to external apps through a
PendingIntent
. Because of this limitation, you need to create a RemoteInput
object to allow those other apps to provide the “reply” text back to your app,
as shown below.
/**
* Creates a [RemoteInput] which allows remote apps to provide a response string
* to the underlying [Intent] within a [PendingIntent].
*/
fun createReplyRemoteInput(context: Context): RemoteInput {
// RemoteInput.Builder accepts a single parameter: the key to use to store
// the response in.
return RemoteInput.Builder(REMOTE_INPUT_RESULT_KEY).build()
// Note that the RemoteInput has no knowledge of the conversation. This is
// because the data for the RemoteInput is bound to the reply Intent via
// static methods in the RemoteInput class.
}
/** Creates an [Intent] which handles replying to the given [appConversation]. */
fun createReplyIntent(
context: Context, appConversation: YourAppConversation): Intent {
// Create the intent backed by the MessagingService.
val intent = Intent(context, MessagingService::class.java)
// This action string lets the MessagingService know this is a "reply" request.
intent.action = ACTION_REPLY
// Provide the conversation id so we know what conversation this applies to.
intent.putExtra(EXTRA_CONVERSATION_ID_KEY, appConversation.id)
return intent
}
Now that you know what is going into the reply Intent
, address the
TODO
for the ACTION_REPLY
switch clause within the MessagingService
and extract that information, as shown below:
ACTION_REPLY -> {
// Extract reply response from the intent using the same key that the
// above RemoteInput used.
val results: Bundle = RemoteInput.getResultsFromIntent(intent)
val message = results.getString(REMOTE_INPUT_RESULT_KEY)
// Note this conversation object came from above in the MessagingService
conversation.reply(message)
}
You handle the mark-as-read Intent
in a similar way (however, it doesn't
require a RemoteInput
).
/** Creates an [Intent] which handles marking the [appConversation] as read. */
fun createMarkAsReadIntent(
context: Context, appConversation: YourAppConversation): Intent {
val intent = Intent(context, MessagingService::class.java)
intent.action = ACTION_MARK_AS_READ
intent.putExtra(EXTRA_CONVERSATION_ID_KEY, appConversation.id)
return intent
}
The code sample below addresses the TODO
for the ACTION_MARK_AS_READ
switch
clause within the MessagingService
:
// Marking as read has no other logic.
ACTION_MARK_AS_READ -> conversation.markAsRead()
Notify users of messages
With the messaging app conversation action handling done, we can move onto generating Android Auto compliant notifications.
Create actions
Action
s are objects that can be passed to other apps via a Notification
to
trigger methods in the original app. This is how Android Auto can mark a
conversation as read, and reply to them.
To create an Action
, start with an Intent
, as shown with the "reply" Intent
below:
fun createReplyAction(
context: Context, appConversation: YourAppConversation): Action {
val replyIntent: Intent = createReplyIntent(context, appConversation)
// ...
We then wrap this Intent
in a PendingIntent
which prepares it for external
app usage. A PendingIntent
locks down all access to the wrapped Intent
by
only exposing a select set of methods that allow a receiving app to fire the
Intent
or get the originating app's package name; but never allowing the
external app to access the underlying Intent
or data within.
// ...
val replyPendingIntent = PendingIntent.getService(
context,
createReplyId(appConversation), // Method explained later.
replyIntent,
PendingIntent.FLAG_UPDATE_CURRENT)
// ...
Before you set up the reply Action
, be aware that Android Auto has three
requirements for the reply Action
:
- The semantic action must be set to
Action.SEMANTIC_ACTION_REPLY
. - The
Action
must indicate that it will not show any user interface when fired. - The
Action
must contain a singleRemoteInput
.
The following code sample sets up the reply Action
while adressing the
requirements listed above:
// ...
val replyAction = Action.Builder(R.drawable.reply, "Reply", replyPendingIntent)
// Provide context to what firing Action does.
.setSemanticAction(Action.SEMANTIC_ACTION_REPLY)
// The action doesn't show any UI (it's a requirement for apps to
// not show UI for Android Auto).
.setShowsUserInterface(false)
// Don't forget the reply RemoteInput. Android Auto will use this to
// make a system call that will add the response string into
// the reply intent so it can be extracted by the messaging app.
.addRemoteInput(createReplyRemoteInput(context))
.build()
return replyAction
}
On the mark-as-read side, we do the same except there's no RemoteInput
.
Android Auto thus has 2 requirements for the mark-as-read Action
:
- The semantic action is set to
Action.SEMANTIC_ACTION_MARK_AS_READ
. - The action indicates that it will not show any user interface when fired.
fun createMarkAsReadAction(
context: Context, appConversation: YourAppConversation): Action {
val markAsReadIntent = createMarkAsReadIntent(context, appConversation)
val markAsReadPendingIntent = PendingIntent.getService(
context,
createMarkAsReadId(appConversation), // Method explained below.
markAsReadIntent,
PendingIntent.FLAG_UPDATE_CURRENT)
val markAsReadAction = Action.Builder(
R.drawable.mark_as_read, "Mark as Read", markAsReadPendingIntent)
.setSemanticAction(Action.SEMANTIC_ACTION_MARK_AS_READ)
.setShowsUserInterface(false)
.build()
return markAsReadAction
}
Create a MessagingStyle
MessagingStyle
is the carrier of the messaging information and is what Android
Auto uses to read aloud each message in a conversation. First, the user of the
device must be specified in the form of a Person
object.
fun createMessagingStyle(
context: Context, appConversation: YourAppConversation): MessagingStyle {
// Method defined by our messaging app.
val appDeviceUser: YourAppUser = getAppDeviceUser()
val devicePerson = Person.Builder()
// The display name (also the name that's read aloud in Android auto).
.setName(appDeviceUser.name)
// The icon to show in the notification shade in the System UI (outside
// of Android Auto).
.setIcon(appDeviceUser.icon)
// A unique key in case there are multiple people in this conversation with
// the same name.
.setKey(appDeviceUser.id)
.build()
// ...
You can then construct the MessagingStyle
object and provide some details
about the conversation.
// ...
val messagingStyle = MessagingStyle(devicePerson)
// Set the conversation title. Note that if your app's target version is lower
// than P, this will automatically mark this conversation as a group (to
// maintain backwards compatibility). Simply use #setGroupConversation after
// setting the conversation title to explicitly override this behavior. See
// the documentation for more information.
messagingStyle.setConversationTitle(appConversation.title)
// Group conversation means there is more than 1 recipient, so set it as such.
messagingStyle.setGroupConversation(appConversation.recipients.size > 1)
// ...
Lastly, add the unread messages.
// ...
for (appMessage in appConversation.getUnreadMessages()) {
// The sender is also represented via a Person object
val senderPerson = Person.Builder()
.setName(appMessage.sender.name)
.setIcon(appMessage.sender.icon)
.setKey(appMessage.sender.id)
.build()
// And the message is added. Note that more complex messages (ie. images)
// may be created and added by instantiating the MessagingStyle.Message
// class directly. See documentation for details.
messagingStyle.addMessage(
appMessage.body, appMessage.timeReceived, senderPerson)
}
return messagingStyle
}
Package and push the notification
After generating the Action
and MessagingStyle
objects, you can now
construct and post the Notification
.
fun notify(context: Context, appConversation: YourAppConversation) {
// Create our Actions and MessagingStyle from the methods defined above.
val replyAction = createReplyAction(context, appConversation)
val markAsReadAction = createMarkAsReadAction(context, appConversation)
val messagingStyle = createMessagingStyle(context, appConversation)
// Create the notification.
val notification = NotificationCompat.Builder(context, channel)
// A required field for the Android UI.
.setSmallIcon(R.drawable.notification_icon)
// Shown in Android Auto as the conversation image.
.setLargeIcon(appConversation.icon)
// Add MessagingStyle.
.setStyle(messagingStyle)
// Add reply action.
.addAction(replyAction)
// Let's say we don't want to show our mark-as-read Action in the
// notification shade. We can do that by adding it as invisible so it
// won't appear in Android UI, but still satisfy Android Auto's
// mark-as-read Action requirement. You're free to add both Actions as
// visible or invisible. This is just a stylistic choice.
.addInvisibleAction(markAsReadAction)
.build()
// Post the notification for the user to see.
val notificationManagerCompat = NotificationManagerCompat.from(context)
notificationManagerCompat.notify(appConversation.id, notification)
}
See also
Report an Android Auto Messaging issue
If you run into an issue while developing your messaging app for Android Auto, you can report it using the Google Issue Tracker. Be sure to fill out all the requested information in the issue template.
Before filing a new issue, please check if it is already reported in the issues list. You can subscribe and vote for issues by clicking the star for an issue in the tracker. For more information, see Subscribing to an Issue.