Cómo compilar apps de mensajería para Android Auto

Mantenerse al tanto de las últimas novedades 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 framework de Android permite que las apps de mensajería extiendan sus servicios a la experiencia de manejo con una interfaz de usuario estándar que les permite a los conductores mantener sus ojos en la ruta.

Las apps que admiten mensajes pueden extender las notificaciones correspondientes para permitir que Android Auto las procese 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 de MessagingStyle, recibes notificaciones de mensajes optimizadas para todos los dispositivos Android, incluido Android Auto. Las optimizaciones incluyen una IU especializada para las notificaciones de mensajes, animaciones mejoradas y compatibilidad con imágenes intercaladas.

En esta guía, se muestra cómo extender una app que muestra mensajes al usuario y recibe sus respuestas, como una app de chat, para enviar el mensaje y responderlo en un dispositivo Auto. Si deseas obtener orientación relacionada con el diseño, consulta la sección sobre apps de mensajería en el sitio de Design for Driving.

Comenzar

Para proporcionar un servicio de mensajería para dispositivos Auto, tu app debe declarar su compatibilidad con Android Auto en el manifiesto y poder hacer lo siguiente:

  • Compilar y enviar objetos NotificationCompat.MessagingStyle que contengan objetos Action de respuesta y marcado como leído
  • Controlar cómo responder una conversación y marcarla como leída con un Service

Conceptos y objetos

Antes de comenzar a diseñar tu app, resulta útil comprender cómo controla la mensajería Android Auto.

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

La comunicación entre los usuarios se denomina conversación y la representa un objeto MessagingStyle. Una conversación, o MessagingStyle, contiene un título, los mensajes y si la conversación es entre un grupo de usuarios.

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

Android Auto también requiere que las apps agreguen objetos Action a una Notification para permitir que el usuario responda rápidamente un mensaje o lo marque como leído directo desde el panel de notificaciones.

En resumen, una sola conversación está representada por un objeto Notification que tiene el estilo de un objeto MessagingStyle. El MessagingStyle contiene todos los mensajes que están dentro de esa conversación en uno o más objetos MessagingStyle.Message. Además, para que sea compatible con Android Auto, una app debe adjuntar a la Notification objetos Action de respuesta y marcado como leído.

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. Tu app genera una notificación MessagingStyle con objetos Action de respuesta y marcado como leído.
  3. Android Auto recibe el evento "notificación nueva" del sistema Android y encuentra el MessagingStyle, la Action de respuesta y la Action de marcado como leído.
  4. Android Auto genera y muestra una notificación en el vehículo.
  5. Si el usuario presiona la notificación en la pantalla del vehículo, Android Auto activa la Action de marcado 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 con la voz, Android Auto coloca una transcripción de la respuesta del usuario en el Action de respuesta y, luego, la activa.
    • En segundo plano, tu app debe controlar este evento de respuesta.

Suposiciones preliminares

En esta página, no encontrarás una guía para crear una app de mensajería completa. En la siguiente muestra de código, se incluyen algunos de los elementos que necesita tu app 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)

Cómo declarar 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 esa 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 en formato XML que debes crear con la siguiente ruta de acceso: YourAppProject/app/src/main/res/xml/automotive_app_desc.xml. En automotive_app_desc.xml, debes declarar las capacidades de Android Auto que admite tu app. Por ejemplo, para declarar la compatibilidad con notificaciones, incluye lo siguiente:

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

Si tu app se puede configurar como el controlador de SMS predeterminado, asegúrate de incluir el siguiente elemento <uses>. Si no lo haces, se usará un controlador predeterminado integrado en Android Auto para controlar los mensajes SMS/MMS entrantes cuando la app esté configurada como el controlador de SMS predeterminado, lo que puede generar la duplicación de notificaciones.

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

Cómo importar la biblioteca principal de AndroidX

La compilación de notificaciones para uso con dispositivos Auto requiere la biblioteca principal de AndroidX. Importa la biblioteca a tu proyecto de la siguiente manera:

  1. En el archivo build.gradle de nivel superior, incluye una dependencia en el repositorio de Maven de Google, como se muestra en el siguiente ejemplo:

Groovy

allprojects {
    repositories {
        google()
    }
}

Kotlin

allprojects {
    repositories {
        google()
    }
}
  1. En el archivo build.gradle del módulo de tu app, incluye la dependencia de la biblioteca de AndroidX Core, como se muestra en el siguiente ejemplo:

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

Cómo controlar las acciones del usuario

Tu app de mensajería debe tener un método de control de las actualizaciones de una conversación mediante una Action. Para Android Auto, hay dos tipos de objetos Action que tu app deberá controlar: responder y marcar el mensaje como leído. Recomendamos manejarlos con un IntentService, que proporciona la flexibilidad para controlar llamadas potencialmente costosas en segundo plano, lo que libera el subproceso principal de la app.

Cómo definir las acciones de intent

Las acciones de Intent son cadenas simples que identifican para qué sirve la clase Intent. Debido a que un solo servicio puede controlar múltiples tipos de intents, es más fácil definir múltiples cadenas de acción en lugar de definir múltiples componentes de IntentService.

La app de mensajería de ejemplo en esta guía tiene los dos tipos de acciones requeridas: responder y marcar el mensaje como leído, como se muestra en la siguiente muestra de código.

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

Cómo crear el servicio

Para crear un servicio que controle estos objetos 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. También necesitas una clave de entrada remota, que se analiza en detalle más adelante en esta sección. En la siguiente muestra de código, se crea un servicio para manejar las acciones requeridas:

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 asociar este servicio con tu app, también debes registrarlo en el manifiesto de tu app, como se muestra en el siguiente ejemplo:

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

Cómo generar y controlar intents

No hay forma de que otras apps, incluida Android Auto, obtengan el Intent que activa el MessagingService, ya que los objetos Intent se pasan a otras apps con un PendingIntent. Debido a esta limitación, debes crear un objeto RemoteInput para permitir que otras apps proporcionen el texto de respuesta a tu app, como se muestra en el siguiente ejemplo:

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

En la cláusula de cambio ACTION_REPLY dentro de MessagingService, extrae la información que se incluye en el Intent de respuesta, como se muestra en el siguiente ejemplo:

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

El Intent de marcado como leído se controla de una manera similar. Sin embargo, no es necesario usar un RemoteInput, como se muestra en el siguiente ejemplo:

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

La cláusula de cambio ACTION_MARK_AS_READ dentro de MessagingService no requiere más lógica, como se muestra en el siguiente ejemplo:

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

Cómo notificar a los usuarios sobre los mensajes

Una vez que se complete el manejo de las acciones de conversación, el siguiente paso es generar notificaciones compatibles con Android Auto.

Cómo crear acciones

Los objetos Action se pueden pasar a otras apps con una Notification para activar métodos en la app original. Así es como Android Auto puede marcar una conversación como leída o responderla.

Para crear una Action, comienza con un Intent. En el siguiente ejemplo, se muestra cómo crear un Intent de "respuesta":

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

Luego, une este Intent en un PendingIntent que lo prepara para el uso de una app externa. Un PendingIntent bloquea todo el acceso al Intent unido, ya que solo expone un conjunto seleccionado de métodos que permiten que la app receptora active el Intent y obtenga el nombre del paquete de la app de origen. La app externa nunca puede acceder al Intent subyacente ni a los datos que contenga.

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

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 debe configurarse como Action.SEMANTIC_ACTION_REPLY.
  • La Action debe indicar que no mostrará ninguna interfaz de usuario cuando se active.
  • La Action debe contener un solo RemoteInput.

En la siguiente muestra de código, se configura una Action de respuesta que aborda los requisitos enumerados arriba:

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

La acción de marcar como leído es similar, excepto que no hay RemoteInput. Por lo tanto, Android Auto tiene dos requisitos para la Action de marcado 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.

En la siguiente muestra de código, se configura una Action de marcado como leído, que cumple con estos 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
}

Cuando se generan los intents pendientes, se usan dos métodos: createReplyId() y createMarkAsReadId(). Estos métodos sirven como códigos de solicitud para cada PendingIntent, que Android utiliza en el control de intents pendientes existentes. Los métodos create() deben mostrar IDs únicos para cada conversación, pero las llamadas repetidas de la misma conversación deben mostrar el ID único que ya se generó.

Considera un ejemplo con dos conversaciones, A y B: el ID de respuesta de la conversación A es 100 y su ID de marcado como leído es 101. El ID de respuesta de la conversación B es 102 y su ID de marcado como leído es 103. Si se actualiza la conversación A, los IDs de respuesta y marcado como leído siguen siendo 100 y 101. Para obtener más información, consulta PendingIntent.FLAG_UPDATE_CURRENT.

Cómo crear una instancia de MessagingStyle

MessagingStyle es el proveedor de la información de mensajería 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, como se muestra en el siguiente ejemplo:

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

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

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

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

Cómo empaquetar y enviar la notificación

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

Recursos adicionales

Informa un problema en las apps de mensajería para Android Auto

Si tienes un problema mientras desarrollas tu app de mensajería para Android Auto, puedes informarlo con la herramienta de seguimiento de errores de Google. Asegúrate de llenar toda la información solicitada en la plantilla de problemas.

Crear un error nuevo

Antes de informar un problema nuevo, verifica si ya se informó en la lista de problemas. Para suscribirte a un problema o votarlo, haz clic en el ícono de estrella que aparece en la herramienta de seguimiento. Si deseas obtener más información, consulta Cómo suscribirte a un problema.