为 Android Auto 构建消息应用

通信类别即将推出
消息类别正在扩展,以包括对新功能的支持,包括消息历史记录和通话体验

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

支持消息的应用可以扩展其消息通知,以便在 Android Auto 运行时让 Android Auto 使用它们。这些通知在 Auto 中显示,并允许用户在一致的、干扰较少的界面中阅读和回复消息。当您使用 MessagingStyle API 时,您将获得针对所有 Android 设备(包括 Android Auto)的优化消息通知。优化包括专为消息通知设计的 UI、改进的动画以及对内联图像的支持。

本指南将向您展示如何扩展向用户显示消息并接收用户回复的应用(例如聊天应用),以便将消息显示和回复接收传递到 Auto 设备。有关相关设计指南,请参阅设计驾驶网站上的 消息应用

开始使用

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

概念和对象

在开始设计您的应用之前,了解 Android Auto 如何处理消息非常有帮助。

单个通信块称为消息,由类 MessagingStyle.Message 表示。消息包含发送方、消息内容以及发送消息的时间。

用户之间的通信称为对话,由 MessagingStyle 对象表示。对话或 MessagingStyle 包含标题、消息以及对话是否在用户组之间进行。

为了通知用户对话的更新(例如新消息),应用会将 Notification 发布到 Android 系统。此 Notification 使用 MessagingStyle 对象在通知栏中显示特定于消息的 UI。Android 平台还会将此 Notification 传递到 Android Auto,并提取 MessagingStyle 并将其用于通过汽车的显示屏发布通知。

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

总而言之,单个对话由一个 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 用途的简单字符串。由于单个服务可以处理多种类型的意图,因此定义多个操作字符串比定义多个 IntentService 组件更容易。

本指南中的示例短信应用具有两种必需的操作类型:回复和标记为已读,如下面的代码示例所示。

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

创建服务

要创建处理这些 Action 对象的服务,您需要对话 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>

生成和处理意图

其他应用(包括 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 标准的通知。

创建操作

Action 对象可以使用 Notification 传递到其他应用,以触发原始应用中的方法。这就是 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
}

生成挂起意图时,使用两种方法:createReplyId()createMarkAsReadId()。这些方法充当每个 PendingIntent 的请求代码,Android 使用这些代码来控制现有的挂起意图。 create() 方法必须为每个对话返回唯一的 ID,但对同一对话的重复调用必须返回已生成的唯一 ID。

考虑两个对话 A 和 B 的示例:对话 A 的回复 ID 为 100,其标记为已读 ID 为 101。对话 B 的回复 ID 为 102,其标记为已读 ID 为 103。如果对话 A 更新,则回复和标记为已读 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 Issue Tracker 报告问题。请务必在问题模板中填写所有所需信息。

创建新问题

在提交新问题之前,请检查问题列表中是否已报告该问题。您可以通过点击跟踪器中问题旁边的星号来订阅和投票。有关更多信息,请参阅 订阅问题