构建 Android Auto 即时通讯应用

通讯类别即将推出
我们正在扩展“即时通讯”类别,以支持新功能,包括消息记录和通话体验

对许多驾驶员而言,通过消息保持联络非常重要。聊天应用可让用户知道是否需要接孩子,或者晚餐地点是否已更改。借助 Android 框架,即时通讯应用可以使用标准界面将其服务扩展到驾驶体验中,让驾驶员可以始终关注前方道路。

支持消息功能的应用可以扩展其消息通知,使 Android Auto 在运行时能够使用它们。这些通知显示在 Auto 中,让用户能够在一致且不易导致分心的界面中阅读和回复消息。如果使用 MessagingStyle API,您的消息通知可针对所有 Android 设备(包括 Android Auto)进行优化。此类优化包括专用于消息通知的界面、改进的动画,以及对内嵌图片的支持。

本指南将向您介绍如何扩展用于向用户显示消息和接收用户回复的应用(例如聊天应用),以将消息显示和回复接收工作交给 Auto 设备。如需获取相关设计指导,请参阅 Design for Driving 网站中的即时通讯应用

使用入门

如需为 Auto 设备提供通讯服务,您的应用必须在清单中声明其对 Android Auto 的支持,并且能够执行以下操作:

概念和对象

在开始设计应用之前,最好先了解 Android Auto 如何处理即时通讯。

一个通信块称为“一条消息”,由 MessagingStyle.Message 类表示。消息包含发送者、消息内容以及消息发送时间。

用户之间的通信称为“对话”,由 MessagingStyle 对象表示。对话(即 MessagingStyle)包含标题、若干消息,以及指出这是否为用户群组对话的信息。

为了向用户通知对话有更新(例如,有新消息),应用会向 Android 系统发布 Notification。此 Notification 使用 MessagingStyle 对象在通知栏中显示消息功能专用界面。Android 平台还会将此 Notification 传递给 Android Auto,然后系统会提取 MessagingStyle 并用其通过汽车的显示屏发布通知。

Android Auto 还要求应用将 Action 对象添加到 Notification,以便用户能够直接通过通知栏快速回复消息或将消息标记为已读。

总而言之,单个对话由一个 Notification 对象表示,该对象使用一个 MessagingStyle 对象设置样式。MessagingStyle 在一个或多个 MessagingStyle.Message 对象中包含该对话中的所有消息。此外,为了符合 Android Auto 规范,应用必须将“回复”及“标记为已读”Action 对象附加到 Notification

即时通讯流

本部分介绍您的应用与 Android Auto 之间的典型即时通讯流。

  1. 应用收到一条消息。
  2. 应用生成包含“回复”和“标记为已读”Action 对象的 MessagingStyle 通知。
  3. Android Auto 从 Android 系统收到“新通知”事件,并查找 MessagingStyle、“回复”Action 和“标记为已读”Action
  4. Android Auto 生成通知并显示于车载显示屏上。
  5. 如果用户点按车载显示屏上的通知,Android Auto 会触发“标记为已读”Action
    • 在后台,应用必须处理此“标记为已读”事件。
  6. 如果用户通过语音响应通知,Android Auto 会将用户响应转录包含到“回复”Action 中,然后触发相应操作。
    • 在后台,应用必须处理此回复事件。

初步假设

本页面不会为您提供有关创建整个即时通讯应用的指导。以下代码示例包含您的应用在开始支持使用 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)

声明 Android Auto 支持

当 Android Auto 收到来自即时通讯应用的通知时,会检查该应用是否声明了 Android Auto 支持。如需启用此支持,请在应用的清单中添加以下条目:

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

此清单条目引用了另一个 XML 文件,您需要通过以下路径创建该文件:YourAppProject/app/src/main/res/xml/automotive_app_desc.xml。在 automotive_app_desc.xml 中,声明您的应用支持的 Android Auto 功能。例如,如需声明支持通知,请添加以下代码:

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

如果您的应用可以设置为默认短信处理程序,请务必添加以下 <uses> 元素。否则,当用户将您的应用设置为默认短信处理程序时,系统会使用 Android Auto 内置的默认处理程序来处理传入的短信/彩信,这可能会导致重复通知。

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

导入 AndroidX 核心库

构建用于 Auto 设备的通知需要安装 AndroidX 核心库。请按照以下步骤将该库导入您的项目中:

  1. 在顶级 build.gradle 文件中,添加 Google Maven 制品库的依赖项,如以下示例所示:

Groovy

allprojects {
    repositories {
        google()
    }
}

Kotlin

allprojects {
    repositories {
        google()
    }
}
  1. 在应用模块的 build.gradle 文件中,添加 AndroidX Core 库依赖项,如以下示例所示:

Groovy

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

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

Kotlin

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

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

处理用户操作

您的即时通讯应用需要一种通过 Action 处理对话更新的方式。对于 Android Auto,您的应用需要处理两种 Action 对象:“回复”和“标记为已读”。我们建议您使用 IntentService 来处理这些对象,这样即可灵活地在后台处理可能十分占用资源的调用,从而释放应用的主线程。

定义 intent 操作

Intent 操作是简单的字符串,用于标识 Intent 的用途。由于一个服务可以处理多种类型的 intent,因此定义多个操作字符串要比定义多个 IntentService 组件容易。

本指南的示例即时通讯应用具有两种必需的操作:“回复”和“标记为已读”,如以下代码示例所示。

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

创建服务

如需创建用于处理这些 Action 对象的服务,您需要对话 ID,此 ID 是由您的应用定义的任意数据结构,用来标识对话。此外,您还需要一个远程输入键,我们将在本节后面对其进行详细介绍。以下代码示例可以创建用于处理所需操作的服务:

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

如需将此服务与您的应用相关联,您还需要在应用清单中注册此服务,如以下示例所示:

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

生成和处理 intent

其他应用(包括 Android Auto)无法获取触发 MessagingServiceIntent,因为 Intent 是通过 PendingIntent 传递给其他应用的。由于存在此限制,因此您需要创建 RemoteInput 对象,使其他应用能够将回复文本提供给您的应用,如以下示例所示:

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

MessagingService 内的 ACTION_REPLY switch 子句中,提取传入“回复”Intent 的信息,如以下示例所示:

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

“标记为已读”Intent 采用类似的处理方式,但不需要 RemoteInput,如以下示例所示:

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

MessagingService 中的 ACTION_MARK_AS_READ switch 子句无需其他逻辑,如以下示例所示:

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

向用户提供消息通知

对话操作处理完成后,下一步是生成符合 Android Auto 规范的通知。

创建操作

可以使用 NotificationAction 对象传递给其他应用,从而在原始应用中触发方法。通过这种方式,Android Auto 可以将对话标记为已读或回复对话。

如需创建 Action,请先创建 Intent。以下示例展示了如何创建“回复”Intent

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

然后,将此 Intent 封装在 PendingIntent 中,以便为外部应用使用做好准备。PendingIntent 通过以下方式限制对所封装 Intent 的所有访问:仅公开一组选定的方法,让接收方应用能够触发 Intent 或获取源应用的软件包名称。外部应用无法访问底层 Intent 或其中的数据。

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

在设置“回复”Action 时,请注意 Android Auto 对“回复”Action 有三项要求:

  • 语义操作必须设置为 Action.SEMANTIC_ACTION_REPLY
  • Action 必须指明在触发时不会显示任何界面。
  • Action 必须包含单个 RemoteInput

以下代码示例设置了一个满足上述要求的“回复”Action

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

“标记为已读”操作采用类似的处理方式,不过无需 RemoteInput。因此,Android Auto 对“标记为已读”Action 有两项要求:

  • 语义操作设置为 Action.SEMANTIC_ACTION_MARK_AS_READ
  • 该操作指明在触发时不会显示任何界面。

以下代码示例设置了一个满足上述要求的“标记为已读”Action

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
}

生成待处理 intent 时使用了两种方法:createReplyId()createMarkAsReadId()。这两种方法相当于每个 PendingIntent 的请求代码,Android 用它们控制现有的待处理 intent。create() 方法必须针对每个对话返回唯一 ID,但对同一对话的重复调用必须返回已生成的唯一 ID。

举例而言,假设有两个对话 A 和 B:对话 A 的“回复”ID 为 100,其“标记为已读”ID 为 101。对话 B 的“回复”ID 为 102,其“标记为已读”ID 为 103。如果对话 A 已更新,则“回复”ID 和“标记为已读”ID 仍分别为 100 和 101。如需了解详情,请参阅 PendingIntent.FLAG_UPDATE_CURRENT

创建 MessagingStyle

MessagingStyle 是即时通讯信息的载体,供 Android Auto 朗读对话中的每条消息。

首先,必须以 Person 对象的形式指定设备的用户,如以下示例所示:

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

然后,您可以构建 MessagingStyle 对象并提供一些关于对话的详细信息。

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

最后,添加未读消息。

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

打包和推送通知

生成 ActionMessagingStyle 对象后,您可以构建并发布 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)
}

其他资源

报告 Android Auto 即时通讯问题

如果您在开发 Android Auto 即时通讯应用时遇到问题,可以使用 Google 问题跟踪器报告该问题。请务必在问题模板中填写所有必填信息。

创建新问题

在提交新问题之前,请先查看问题列表,确认该问题是否已报告过。您可以在跟踪器中点击某个问题的星标,订阅该问题并为其投票。如需了解详情,请参阅订阅问题