Criar aplicativos de mensagens para o Android Auto

Para muitos motoristas, mensagens são uma forma importante de se comunicar. Os apps de chat podem avisar os usuários se é preciso buscar uma criança ou se vão jantar em outro lugar. O framework do Android permite que os apps de mensagens estendam os serviços para quem está dirigindo, usando uma interface de usuário padrão, que permite que os motoristas possam prestar atenção na estrada.

Os apps que oferecem suporte a mensagens podem estender as notificações de mensagens para permitir o uso delas pelo Android Auto quando estiverem em execução. Essas notificações são mostradas no Auto e permitem que os usuários leiam e respondam às mensagens em uma interface consistente e com pouca distração. E quando você usa a API MessagingStyle, recebe notificações de mensagens otimizadas para todos os dispositivos Android, incluindo o Android Auto. As otimizações incluem uma interface especializada para notificações de mensagens, animações aprimoradas e suporte para imagens inline.

Este guia mostra como estender um app que mostra mensagens e recebe as respostas do usuário, como um app de chat, para entregar a mensagem e enviar a resposta com um dispositivo Auto. Para conferir orientações de design relacionadas, consulte Apps de mensagens no site de Design para direção.

Primeiros passos

Para oferecer serviço de mensagens a dispositivos Auto, o app precisa declarar suporte para o Android Auto no manifesto e fazer o seguinte:

  • Criar e enviar objetos NotificationCompat.MessagingStyle que contenham objetos Action para responder e marcar a mensagem como lida.
  • Processar as ações de responder e marcar uma conversa como lida com um Service.

Conceitos e objetos

Antes de começar a projetar seu app, é útil entender como o Android Auto processa as mensagens.

Um bloco individual de comunicação é chamado de mensagem e é representado pela classe MessagingStyle.Message. Uma mensagem tem um remetente, o conteúdo e a hora em que ela foi enviada.

A comunicação entre usuários é chamada de conversa e é representada por um objeto MessagingStyle. Uma conversa, ou MessagingStyle, contém um título, as mensagens e se a conversa está em um grupo de usuários.

Para notificar os usuários sobre atualizações em uma conversa, como uma nova mensagem, os apps enviam uma Notification ao sistema Android. Essa Notification usa o objeto MessagingStyle para mostrar a interface específica da mensagem na aba de notificações. A plataforma Android também transmite essa Notification para o Android Auto, e o MessagingStyle é extraído e usado para mostrar uma notificação na tela do carro.

O Android Auto também exige que os apps adicionem objetos Action a uma Notification para permitir que o usuário responda rapidamente a uma mensagem ou a marque como lida diretamente na aba de notificações.

Em resumo, uma única conversa é representada por um objeto Notification que é estilizado com um objeto MessagingStyle. O MessagingStyle contém todas as mensagens dessa conversa em um ou mais objetos MessagingStyle.Message. Além disso, para ser compatível com o Android Auto, o app precisa anexar objetos Action para responder e marcar a mensagem como lida à Notification.

Fluxo de mensagens

Esta seção descreve um fluxo de mensagens comum entre seu app e o Android Auto.

  1. Seu app recebe uma mensagem.
  2. Seu app gera uma notificação MessagingStyle com objetos Action para responder e marcar a mensagem como lida.
  3. O Android Auto recebe o evento "notificação nova" do sistema Android e encontra o MessagingStyle, a Action para responder e a Action para marcar a mensagem como lida.
  4. O Android Auto gera e mostra uma notificação no carro.
  5. Se o usuário tocar na notificação na tela do carro, o Android Auto vai acionar a Action para marcar a mensagem como lida.
    • Seu app precisa gerenciar esse evento de marcar como lida em segundo plano.
  6. Se o usuário responder à notificação por voz, o Android Auto vai incluir uma transcrição da resposta na Action de resposta e a acionará.
    • Seu app precisa gerenciar esse evento de resposta em segundo plano.

Hipóteses preliminares

Esta página não orienta você sobre como criar um app de mensagens inteiro. O exemplo de código abaixo inclui alguns elementos que seu app precisa ter antes de começar a oferecer suporte ao envio de mensagens pelo 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)

Declarar suporte para Android Auto

Quando o Android Auto recebe uma notificação de um app de mensagens, ele verifica se o app declarou compatibilidade com o Android Auto. Para ativar esse suporte, inclua esta entrada no manifesto do seu app:

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

Essa entrada de manifesto se refere a outro arquivo XML que você precisa criar com este caminho: YourAppProject/app/src/main/res/xml/automotive_app_desc.xml. Em automotive_app_desc.xml, declare os recursos do Android Auto com suporte do app. Por exemplo, para declarar o suporte a notificações, inclua o seguinte:

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

Caso seu app possa ser definido como o gerenciador de SMS padrão, inclua o elemento <uses> abaixo. Se você não fizer isso, um gerenciador padrão integrado ao Android Auto será usado para processar mensagens SMS/MMS recebidas quando seu app estiver configurado como o gerenciador padrão, o que pode levar a notificações duplicadas.

<automotiveApp>
    ...
    <uses name="sms" />
</automotiveApp>

Importar a biblioteca principal do AndroidX

A criação de notificações para uso com dispositivos Auto exige a biblioteca principal do AndroidX. Importe a biblioteca para o projeto desta forma:

  1. No arquivo build.gradle de nível superior, inclua uma dependência no repositório Maven do Google, conforme mostrado no exemplo abaixo:

Groovy

allprojects {
    repositories {
        google()
    }
}

Kotlin

allprojects {
    repositories {
        google()
    }
}
  1. No arquivo build.gradle do módulo do seu app, inclua a dependência da biblioteca AndroidX Core, conforme mostrado no exemplo abaixo:

Groovy

dependencies {
    // If your app is written in Java
    implementation 'androidx.core:core:1.12.0'

    // If your app is written in Kotlin
    implementation 'androidx.core:core-ktx:1.12.0'
}

Kotlin

dependencies {
    // If your app is written in Java
    implementation("androidx.core:core:1.12.0")

    // If your app is written in Kotlin
    implementation("androidx.core:core-ktx:1.12.0")
}

Processar ações do usuário

Seu app de mensagens precisa de uma maneira de processar atualizações em uma conversa por uma Action. Para o Android Auto, existem dois tipos de objetos Action que o app precisa processar: responder e marcar como lida. Recomendamos processá-los usando uma IntentService, que oferece a flexibilidade de processar chamadas potencialmente caras em segundo plano, liberando a linha de execução principal.

Definir ações de intents

As ações Intent são strings simples que identificam para que serve a Intent. Como um único serviço pode processar vários tipos de intents, é mais fácil definir várias strings em vez de definir vários componentes IntentService.

O app de mensagens de exemplo deste guia tem os dois tipos obrigatórios de ação: responder e marcar a mensagem como lida, conforme mostrado no exemplo de código abaixo.

private const val ACTION_REPLY = "com.example.REPLY"
private const val ACTION_MARK_AS_READ = "com.example.MARK_AS_READ"

Criar o serviço

Para criar um serviço que gerencie esses objetos Action, você precisa do ID da conversa, que é uma estrutura de dados arbitrária definida pelo seu app que identifica a conversa. Você também precisa de uma chave de entrada remota, discutida em detalhes mais adiante nesta seção. O exemplo de código abaixo cria um serviço para processar as ações necessárias:

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?) {
        // Fetches internal data.
        val conversationId = intent!!.getIntExtra(EXTRA_CONVERSATION_ID_KEY, -1)

        // Searches the database for that conversation.
        val conversation = YourAppConversation.getById(conversationId)

        // Handles the action that was requested in the intent. The TODOs
        // are addressed in a later section.
        when (intent.action) {
            ACTION_REPLY -> TODO()
            ACTION_MARK_AS_READ -> TODO()
        }
    }
}

Para associar esse serviço ao seu app, você também precisa registrá-lo no manifesto do app, como mostrado neste exemplo:

<application>
    <service android:name="com.example.MessagingService" />
    ...
</application>

Gerar e processar intents

Não é possível para outros apps, incluindo o Android Auto, acessar a Intent que aciona o MessagingService, porque as Intents são transmitidas para outros apps usando uma PendingIntent. Devido a essa limitação, é necessário criar um objeto RemoteInput para permitir que outros apps forneçam o texto de resposta ao app, conforme mostrado neste exemplo:

/**
 * Creates a [RemoteInput] that lets remote apps 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 using
    // static methods in the RemoteInput class.
}

/** Creates an [Intent] that handles replying to the given [appConversation]. */
fun createReplyIntent(
        context: Context, appConversation: YourAppConversation): Intent {
    // Creates the intent backed by the MessagingService.
    val intent = Intent(context, MessagingService::class.java)

    // Lets the MessagingService know this is a reply request.
    intent.action = ACTION_REPLY

    // Provides the ID of the conversation that the reply applies to.
    intent.putExtra(EXTRA_CONVERSATION_ID_KEY, appConversation.id)

    return intent
}

Na cláusula de chave ACTION_REPLY dentro do MessagingService, extraia as informações da Intent "responder", conforme mostrado no exemplo abaixo:

ACTION_REPLY -> {
    // Extracts reply response from the intent using the same key that the
    // RemoteInput uses.
    val results: Bundle = RemoteInput.getResultsFromIntent(intent)
    val message = results.getString(REMOTE_INPUT_RESULT_KEY)

    // This conversation object comes from the MessagingService.
    conversation.reply(message)
}

Gerencie a Intent "marcar mensagem como lida" de maneira semelhante. No entanto, ela não exige uma RemoteInput, conforme mostrado neste exemplo:

/** Creates an [Intent] that 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
}

A cláusula de chave ACTION_MARK_AS_READ dentro do MessagingService não exige mais lógica, como mostrado neste exemplo:

// Marking as read has no other logic.
ACTION_MARK_AS_READ -> conversation.markAsRead()

Notificar usuários de mensagens

Depois que o processamento da ação de conversa for concluído, a próxima etapa será gerar notificações compatíveis com o Android Auto.

Criar ações

Objetos Action podem ser transmitidos para outros apps usando uma Notification para acionar métodos no app original. É assim que o Android Auto pode marcar uma conversa como lida ou responder a ela.

Para criar uma Action, comece com uma Intent. O exemplo abaixo mostra como criar uma Intent de "resposta":

fun createReplyAction(
        context: Context, appConversation: YourAppConversation): Action {
    val replyIntent: Intent = createReplyIntent(context, appConversation)
    // ...

Em seguida, envolvemos essa Intent em uma PendingIntent que a prepara para o uso externo do app. Uma PendingIntent bloqueia todo o acesso à Intent encapsulada expondo apenas um conjunto selecionado de métodos que permitem que o app receptor dispare a Intent ou receba o nome do pacote do app de origem. O app externo nunca pode acessar a Intent ou os dados dela.

    // ...
    val replyPendingIntent = PendingIntent.getService(
        context,
        createReplyId(appConversation), // Method explained later.
        replyIntent,
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE)
    // ...

Antes de configurar a Action de resposta, o Android Auto tem três requisitos para essa Action:

  • A ação semântica precisa ser configurada como Action.SEMANTIC_ACTION_REPLY.
  • A Action precisa indicar que não mostrará nenhuma interface do usuário quando acionada.
  • A Action precisa conter uma única RemoteInput.

O exemplo de código abaixo configura uma Action de resposta que atende aos requisitos listados acima:

    // ...
    val replyAction = Action.Builder(R.drawable.reply, "Reply", replyPendingIntent)
        // Provides context to what firing the Action does.
        .setSemanticAction(Action.SEMANTIC_ACTION_REPLY)

        // The action doesn't show any UI, as required by 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
}

O processamento da ação "marcar como lida" é semelhante, exceto pelo fato de não haver uma RemoteInput. O Android Auto tem dois requisitos para a Action de "marcar como lida":

  • A ação semântica precisa ser definida como Action.SEMANTIC_ACTION_MARK_AS_READ.
  • A ação indica que não mostrará nenhuma interface do usuário quando acionada.

O exemplo de código abaixo configura uma Action "marcar como lida" que atende a esses requisitos:

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  or PendingIntent.FLAG_IMMUTABLE)
    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
}

Ao gerar as intents pendentes, dois métodos são usados: createReplyId() e createMarkAsReadId(). Esses métodos servem como códigos de solicitação para cada PendingIntent, que são usados pelo Android para controlar as intents pendentes já existentes. Os métodos create() precisam retornar IDs exclusivos para cada conversa, mas chamadas repetidas para a mesma conversa precisam retornar o ID exclusivo já gerado.

Considere um exemplo com duas conversas: A e B: o ID de resposta da conversa A é 100, e o ID de marcar como lida é 101. O ID de resposta da conversa B é 102, e o ID de marcar como lida é 103. Se a conversa A for atualizada, os IDs de resposta e de marcar como lida ainda serão 100 e 101. Para saber mais, consulte PendingIntent.FLAG_UPDATE_CURRENT.

Criar um MessagingStyle

O MessagingStyle contém as informações das mensagens e é usado pelo Android Auto para ler mensagens de uma conversa em voz alta.

Primeiro, o usuário do dispositivo precisa ser especificado na forma de um objeto Person, conforme mostrado neste exemplo:

fun createMessagingStyle(
        context: Context, appConversation: YourAppConversation): MessagingStyle {
    // Method defined by the 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()
    // ...

Em seguida, construa o objeto MessagingStyle e forneça alguns detalhes sobre a conversa.

    // ...
    val messagingStyle = MessagingStyle(devicePerson)

    // Sets the conversation title. If the app's target version is lower
    // than P, this will automatically mark the conversation as a group (to
    // maintain backward compatibility). 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)
    // ...

Por fim, adicione as mensagens não lidas.

    // ...
    for (appMessage in appConversation.getUnreadMessages()) {
        // The sender is also represented using a Person object.
        val senderPerson = Person.Builder()
            .setName(appMessage.sender.name)
            .setIcon(appMessage.sender.icon)
            .setKey(appMessage.sender.id)
            .build()

        // Adds the message. More complex messages, like images,
        // can be created and added by instantiating the MessagingStyle.Message
        // class directly. See documentation for details.
        messagingStyle.addMessage(
                appMessage.body, appMessage.timeReceived, senderPerson)
    }

    return messagingStyle
}

Empacotar e enviar a notificação

Depois de gerar os objetos Action e MessagingStyle, você pode construir e postar a Notification.

fun notify(context: Context, appConversation: YourAppConversation) {
    // Creates the actions and MessagingStyle.
    val replyAction = createReplyAction(context, appConversation)
    val markAsReadAction = createMarkAsReadAction(context, appConversation)
    val messagingStyle = createMessagingStyle(context, appConversation)

    // Creates the notification.
    val notification = NotificationCompat.Builder(context, channel)
        // A required field for the Android UI.
        .setSmallIcon(R.drawable.notification_icon)

        // Shows in Android Auto as the conversation image.
        .setLargeIcon(appConversation.icon)

        // Adds MessagingStyle.
        .setStyle(messagingStyle)

        // Adds reply action.
        .addAction(replyAction)

        // Makes the mark-as-read action invisible, so it doesn't appear
        // in the Android UI but the app satisfies Android Auto's
        // mark-as-read Action requirement. Both required actions can be made
        // visible or invisible; it is a stylistic choice.
        .addInvisibleAction(markAsReadAction)

        .build()

    // Posts the notification for the user to see.
    val notificationManagerCompat = NotificationManagerCompat.from(context)
    notificationManagerCompat.notify(appConversation.id, notification)
}

Outros recursos

Informar um problema com mensagens no Android Auto

Se você encontrar um problema ao desenvolver seu app de mensagens para o Android Auto, informe-o usando o Google Issue Tracker. Preencha todas as informações solicitadas no modelo de problema.

Informar novo problema

Antes de informar um novo problema, verifique se ele já foi comunicado na lista. Inscreva-se e vote nos problemas clicando na estrela de um deles na lista de problemas. Para ver mais informações, consulte Como se inscrever em um problema.