The Android Developer Challenge is back! Submit your idea before December 2.

Cómo compilar apps de mensajes para Android Auto

Mantenerse conectado a través de mensajes es importante para muchos conductores. Con las apps de chat, los usuarios pueden saber si deben ir a buscar a su hijo o si cambió el lugar de una cena. El marco de trabajo de Android permite que las apps de mensajes extiendan sus servicios a la experiencia de manejo utilizando una interfaz de usuario estándar que les permite a los conductores mantener sus ojos centrados en la carretera.

Las apps que admiten mensajes pueden extender las notificaciones correspondientes para permitir que Android Auto las consuma cuando se esté ejecutando. Estas notificaciones se muestran en Android Auto y les permiten a los usuarios leer y responder los mensajes en una interfaz coherente y con mínima distracción. Además, cuando usas la API MessagingStyle API, obtienes el beneficio de las notificaciones mensajes optimizadas para todos los dispositivos Android, incluido Android Auto. Estas optimizaciones incluyen la IU especializada para las notificaciones de mensajes, animaciones mejoradas y compatibilidad con imágenes intercaladas.

En esta lección, se supone que compilaste una app que muestra mensajes al usuario y recibe sus respuestas, como una app de chat. Se mostrará cómo extender tu app para entregar esos mensajes a un dispositivo con Android Auto para que se muestren y se respondan.

Cómo comenzar

Para habilitar tu app para que proporcione el servicio de mensajes para los dispositivos con Android Auto, esta debe tener la capacidad de realizar las siguientes tareas:

  1. Compilar y enviar objetos NotificationCompat.MessagingStyle que contengan objetos de respuesta y Action para marcar los mensajes como leídos
  2. Controlar las respuestas y marcar una conversación como leída con un Service
  3. Configurar tu manifiesto para indicar que la app es compatible con Android Auto

Conceptos y objetos

Antes de comenzar a diseñar tu app, es útil comprender cómo Android Auto controla los mensajes.

Una comunicación individual se denomina mensaje y se representa mediante la clase MessagingStyle.Message. Un mensaje contiene un remitente, el contenido del mensaje y la hora en la que se envió el mensaje.

La comunicación entre usuarios se denomina conversación y se representa mediante un objeto MessagingStyle. Una conversación (o MessagingStyle) contiene un título y los mensajes, y también indica si es de carácter grupal (es decir, si tiene más de un destinatario).

Para notificar a los usuarios sobre las actualizaciones de una conversación (como un nuevo mensaje), las apps publican una Notification en el sistema Android. Esta notificación usa el objeto MessagingStyle para mostrar la IU específica de los mensajes en el panel de notificaciones. La plataforma Android también pasa esta Notification a Android Auto, y MessagingStyle se extrae y usa para publicar una notificación en la pantalla del vehículo.

Las apps también pueden agregar objetos de Action a una Notification para permitir al usuario responder rápidamente a un mensaje o marcarlo como leído de forma directa desde el panel de notificaciones. Android Auto requiere los objetos Action para responder y marcar mensajes como leídos con el fin de administrar una conversación.

En resumen, una única conversación está representada por un único objeto Notification que tiene un único objeto MessagingStyle. MessagingStyle contiene todos los mensajes dentro de esa conversación con uno o más objetos MessagingStyle.Message. Finalmente, para que sea totalmente compatible con Android Auto, debes adjuntar una respuesta y una Action a fin de marcar el mensaje como leído a la Notification.

Flujo de mensajes

En esta sección, se describe un flujo de mensajes típico entre tu app y Android Auto.

  1. Tu app recibe un mensaje.
  2. Esta genera una notificación de MessagingStyle con una respuesta y una Action para marcar el mensaje como leído.
  3. Android Auto recibe un evento de "nueva notificación" del sistema Android y encuentra MessagingStyle, la Action de respuesta y la Action para marcar el mensaje como leído.
  4. Android Auto genera y muestra una notificación en el vehículo.
  5. Si un usuario presiona la notificación en la pantalla del vehículo, Android Auto activa la Action para marcar el mensaje como leído.
    • En segundo plano, tu app debe controlar este evento, que consiste en marcar el mensaje como leído.
  6. Si el usuario responde a la notificación por voz, Android Auto incluye una transcripción de la respuesta del usuario en la Action de respuesta y, luego, la activa.
    • En segundo plano, la app debe controlar este evento de respuesta.

Suposiciones preliminares

En esta página, no encontrarás una guía para crear una app de mensajes completa. Sin embargo, el ejemplo de código a continuación incluye algunos de los aspectos que tu app debería tener antes de comenzar a admitir mensajes en 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)
    

Declara la compatibilidad con Android Auto

Cuando Android Auto recibe una notificación de una app de mensajes, verifica que la app haya declarado su compatibilidad con Android Auto. Para habilitar esta compatibilidad, incluye la siguiente entrada en el manifiesto de tu app:

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

Esta entrada de manifiesto hace referencia a otro archivo XML que debes crear con la siguiente ruta: YourAppProject/app/src/main/res/xml/automotive_app_desc.xml, donde declaras las capacidades de Android Auto que admite tu app. Por ejemplo, para incluir compatibilidad con notificaciones, incluye lo siguiente en tu automotive_app_desc.xml:

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

Si tu app requiere compatibilidad para controlar SMS, MMS y RCS, también debes incluir lo siguiente:

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

Importa la biblioteca principal de AndroidX

La compilación de notificaciones para uso con dispositivos de Android Auto requiere la biblioteca principal de AndroidX y puedes importarla a tu proyecto de la siguiente manera:

  1. En el archivo build.gradle de nivel superior, asegúrate de incluir el repositorio Maven de Google, como se muestra a continuación.

    allprojects {
            repositories {
                google()
            }
        }
        
  2. En el archivo build.gradle del módulo de tu app, incluye la dependencia de la biblioteca principal de AndroidX, como se muestra a continuación:

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

Controla las acciones del usuario

Tu app de mensajes debe tener un método de control de las actualizaciones de una conversación mediante una instancia de Action. Para Android Auto, hay dos tipos de objetos de Action que tu app debe controlar: responder y marcar el mensaje como leído. La forma recomendada de hacerlo es mediante el uso de una instancia de IntentService. IntentService proporciona la flexibilidad de controlar las llamadas que posiblemente sean costosas en segundo plano y libera el subproceso principal de una app.

Define las acciones de intent

Las acciones de Intent (que no se deben confundir con las acciones de notificación) son strings simples que identifican para qué sirve la instancia de Intent. Debido a que un solo servicio puede controlar múltiples tipos de intents, es más fácil definir múltiples cadenas de Intent.action, en lugar de definir múltiples IntentServices.

En nuestra app de mensajes de ejemplo, tenemos dos tipos de acciones: responder y marcar el mensaje como leído, como se declara en el siguiente ejemplo de código.

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

Crea el servicio

Para crear un servicio que controle estas Action, necesitas el ID de la conversación, que es una estructura de datos arbitraria definida por la app y que identifica la conversación, y una clave de entrada remota, que se analiza en detalle más adelante en esta sección. En el siguiente ejemplo de código crea ese servicio.

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 asociar este servicio con tu app, también debes registrar el servicio en el manifiesto de tu app, como se muestra a continuación.

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

Genera y controla las instancias de intent

No hay forma de que otras apps obtengan la instancia de Intent que activa MessagingService, ya que cada Intent se pasa a apps externas a través de un PendingIntent. Debido a esta limitación, debes crear un objeto RemoteInput para permitir que esas otras apps devuelvan el texto "de respuesta" a tu app, como se muestra a continuación.

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

Ahora que sabes lo que sucede en el Intent de respuesta, aborda TODO para la cláusula de cambio ACTION_REPLY dentro de MessagingService y extrae esa información de la siguiente manera:

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

El Intent para marcar el mensaje como leído se controla de una manera similar (sin embargo, no requiere una instancia de 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
    }
    

En el siguiente ejemplo de código, se aborda TODO para la cláusula de cambio ACTION_MARK_AS_READ dentro de MessagingService:

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

Notifica a los usuarios sobre los mensajes

Una vez listo el control de las acciones de conversación de la app de mensajes, podemos generar notificaciones que cumplan con Android Auto.

Crea acciones

Las instancias de Action son objetos que se pueden pasar a otras apps mediante una Notification para activar métodos en la app original. Así es como Android Auto puede marcar una conversación como leída y responder a ella.

Para crear una Action, comienza con un Intent, como se muestra en el siguiente Intent de respuesta a continuación:

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

Luego, unimos este Intent con un PendingIntent, que lo prepara para uso externo de la app. Un PendingIntent bloquea el acceso al Intent unido, ya que solo expone un conjunto selecto de métodos que permiten que la app receptora active el Intent, o bien obtenga el nombre del paquete de la app de origen, pero nunca permite que la app externa acceda al Intent subyacente ni a los datos que contiene.

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

Antes de configurar la Action de respuesta, ten en cuenta que Android Auto tiene tres requisitos para la Action de respuesta:

  • La acción semántica se debe establecer en Action.SEMANTIC_ACTION_REPLY.
  • La Action debe indicar que no mostrará ninguna interfaz de usuario cuando se active.
  • La Action debe contener una única instancia de RemoteInput.

En el siguiente ejemplo de código, se configura la Action de respuesta y se abordan los requisitos enumerados arriba:

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

En la acción para marcar una respuesta como leída, hacemos lo mismo, excepto que no hay ninguna instancia de RemoteInput. Por lo tanto, Android Auto tiene dos requisitos para la Action que marca el mensaje como leído:

  • La acción semántica se establece en Action.SEMANTIC_ACTION_MARK_AS_READ.
  • La acción indica que no mostrará ninguna interfaz de usuario cuando se active.
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
    }
    

Crea una instancia de MessagingStyle

MessagingStyle es el proveedor de la información de mensajes y lo que Android Auto usa para leer en voz alta cada uno de los mensajes de una conversación. Primero, se debe especificar el usuario del dispositivo en la forma de un 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()
        // ...
    

Luego, puedes crear el objeto MessagingStyle y proporcionar detalles sobre la conversación.

    // ...
        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 último, agrega los mensajes no leídos.

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

Empaqueta y envía la notificación

Después de generar los objetos Action y MessagingStyle, puedes crear y publicar la 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)
    }
    

Consulta también