为 Android Auto 构建消息应用

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

通过消息保持联系对许多驾驶员来说非常重要。聊天应用可以让用户知道孩子是否需要接送或晚餐地点是否已更改。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.13.1'

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

Kotlin

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

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

处理用户操作

短信应用需要一种方法来通过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报告问题。请务必在问题模板中填写所有请求的信息。

创建新问题

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