Criar apps de mensagens para o Android Auto

Manter-se conectado por meio de mensagens é importante para muitos motoristas. Os apps de bate-papo podem informar aos usuários se uma criança precisa ser buscada ou se o local do jantar mudou. O framework do Android permite que os apps de mensagens estendam os serviços para quem está dirigindo ao usar uma interface de usuário padrão, que permite que os motoristas continuem prestando atenção no trânsito.

Os apps compatíveis com mensagens podem estender as notificações de mensagens para permitir que o Android Auto use-as quando estiver em execução. Essas notificações são exibidas no Auto e permitem que os usuários leiam e respondam às mensagens em uma interface consistente e com pouca distração. Além disso, ao usar a API MessagingStyle, você terá o benefício de notificações de mensagens otimizadas para todos os dispositivos Android, incluindo o Android Auto. Essas otimizações incluem IU especializada para notificações de mensagens, animações aprimoradas e compatibilidade com imagens in-line.

Esta lição presume que você criou um app que exibe mensagens para o usuário e recebe as respostas dele, como um aplicativo de bate-papo. A lição mostra como estender seu app para entregar essas mensagens a um dispositivo Auto para exibição e respostas.

Primeiros passos

Para que seu app ofereça um serviço de mensagens para dispositivos Auto, ele precisa ser capaz de:

  1. Criar e enviar objetos NotificationCompat.MessagingStyle que contenham objetos Action "reply" e "mark-as-read".
  2. Identificar as ações de responder e marcar uma conversa como lida com um Service.
  3. Configurar seu manifesto para indicar que o app é compatível com o Android Auto.

Conceitos e objetos

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

Um único bloco 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) tem um título, as mensagens e se a conversa é entre um grupo (ou seja, se tem mais de um destinatário).

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

Os apps também podem adicionar 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. O Android Auto requer os objetos Action "mark-as-read" e "reply" para gerenciar uma conversa.

Em outras palavras, uma única conversa é representada por um único objeto Notification estilizado com um único objeto MessagingStyle. O MessagingStyle contém todas as mensagens da conversa com um ou mais objetos MessagingStyle.Message. Por fim, para ser totalmente compatível com o Android Auto, é necessário anexar uma Action "reply" e "mark-as-read" a 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. O app gera uma notificação MessagingStyle com uma Action "reply" e "mark-as-read".
  3. O Android Auto recebe um evento “new notification” do sistema Android e encontra MessagingStyle, a Action "reply" e a Action "mark-as-read".
  4. O Android Auto gera e exibe uma notificação no carro.
  5. Se o usuário tocar na notificação na tela do carro, o Android Auto acionará a Action "mark-as-read".
    • Seu app precisa gerenciar esse evento "mark-as-read" em segundo plano.
  6. Se o usuário responder à notificação por voz, o Android Auto incluirá uma transcrição da resposta do usuário na Action "reply" e a acionará.
    • Seu app precisa gerenciar esse evento de resposta em segundo plano.

Hipóteses preliminares

Esta página não traz orientações sobre como criar um app de mensagens inteiro. No entanto, o exemplo de código abaixo inclui alguns dos elementos que seu app precisa ter antes de começar a oferecer compatibilidade com o 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 compatibilidade com o 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 essa compatibilidade, inclua a seguinte 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 precisa ser criado com o seguinte caminho: YourAppProject/app/src/main/res/xml/automotive_app_desc.xml, em que é declarado com quais recursos do Android Auto seu app é compatível. Por exemplo, para informar compatibilidade com notificações, inclua o seguinte no seu automotive_app_desc.xml:

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

Se seu app precisar de compatibilidade com o processamento de SMS, MMS e RCS, também é necessário incluir o seguinte:

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

Importar a biblioteca principal do AndroidX

A criação de notificações para uso com dispositivos Auto requer a biblioteca principal do AndroidX, que pode ser importada para seu projeto da seguinte maneira:

  1. No arquivo build.gradle de nível superior, inclua o repositório Maven do Google, como mostrado abaixo.

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

    dependencies {
            implementation 'androidx.core:core:1.0.0'
        }
        

Gerenciar ações do usuário

Seu app de mensagens precisa de uma maneira de processar atualizações em uma conversa por meio de uma Action. Para o Android Auto, existem dois tipos de objetos Action que o app precisa processar: "reply" e "mark-as-read". A maneira recomendada de fazer isso é usando um IntentService. Um IntentService fornece a flexibilidade de gerenciar chamadas potencialmente caras em segundo plano e libera a linha de execução principal de um app.

Definir ações de intents

Ações de Intent (que não podem ser confundidas com ações de notificação) são strings simples que identificam para que serve o Intent. Como um único serviço pode gerenciar vários tipos de intents, é mais fácil definir várias strings Intent.action em vez de definir vários IntentServices.

Em nosso exemplo de app de mensagens, temos dois tipos de ações: "reply" e "mark-as-read", conforme declarado no seguinte exemplo de código.

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 gerencia essas Action, você precisa do código da conversa, que é uma estrutura de dados arbitrária definida pelo seu app que identifica a conversa, e de uma chave de entrada remota, discutida em mais detalhes posteriormente nesta seção. O exemplo de código a seguir cria esse tipo de serviço.

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()
            }
        }
    }
    

Para associar esse serviço ao seu app, registre o serviço no manifesto do app, conforme mostrado abaixo.

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

Gerar e processar intents

Não há como outros apps receberem o Intent que aciona o MessagingService, porque cada Intent é passado para apps externos por meio de um PendingIntent. Devido a essa limitação, é necessário criar um objeto RemoteInput para permitir que esses outros apps enviem o texto "reply" de volta ao seu app, conforme mostrado abaixo.

/**
     * 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
    }
    

Agora que você sabe o que está entrando no Intent "reply", direcione o TODO da cláusula de chave ACTION_REPLY no MessagingService e extraia essas informações, conforme mostrado abaixo:

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)
    }
    

Gerencie o Intent "mark-as-read" de maneira semelhante (com a diferença de que ele não requer uma 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
    }
    

O exemplo de código abaixo mostra o TODO da cláusula de chave ACTION_MARK_AS_READ no MessagingService:

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

Notificar usuários de mensagens

Depois de lidar com a ação de conversa do app de mensagens, podemos prosseguir e gerar notificações compatíveis com o Android Auto.

Criar ações

Actions são objetos que podem ser transmitidos para outros apps por meio de uma Notification para acionar métodos no app original. É assim que o Android Auto pode marcar uma conversa como lida e responder a ela.

Para criar uma Action, comece com um Intent, conforme mostrado no Intent "reply" abaixo:

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

Em seguida, esse PendingIntent é empacotado em um Intent, que o prepara para uso por apps externos. Um PendingIntent bloqueia todo o acesso ao Intent empacotado, expondo apenas um conjunto selecionado de métodos que permitem que um app receptor acione o Intent ou receba o nome do pacote do app de origem, mas nunca permitindo que o app externo acesse o Intent ou os dados subjacentes.

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

Antes de configurar a Action "reply", saiba que 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 um único RemoteInput.

O exemplo de código a seguir configura a Action "reply" seguindo os requisitos listados acima:

    // ...
        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
    }
    

Na Action "mark-as-read", fazemos o mesmo, mas não há RemoteInput. O Android Auto, portanto, tem dois requisitos para a Action "mark-as-read":

  • 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.
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
    }
    

Criar um MessagingStyle

O MessagingStyle é a operadora das informações das mensagens, que é usada 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.

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()
        // ...
    

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

    // ...
        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)
        // ...
    

Por fim, adicione as mensagens não lidas.

    // ...
        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
    }
    

Empacotar e enviar a notificação

Depois de gerar os objetos Action e MessagingStyle, construa e publique a 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)
    }
    

Veja também