建構 Android Auto 訊息應用程式

對許多駕駛人來說,透過訊息保持聯絡是相當重要的。Chat 應用程式會讓使用者知道他們是否需要接送孩童,或是晚餐地點有異動。Android 架構讓訊息應用程式可透過標準使用者介面將服務延伸至行車體驗,使駕駛人將注意力集中於道路上。

支援訊息功能的應用程式可將訊息通知延伸,讓 Android Auto 在執行時使用這些訊息。這些通知會顯示在自動通知分頁中,可讓使用者以一致、低干擾的介面閱讀及回覆訊息。此外,使用 MessagingStyle API 時,您可以善用所有 Android 裝置 (包括 Android Auto) 的最佳化訊息通知功能。這類最佳化包括訊息通知專屬的使用者介面、改良的動畫以及支援內嵌圖片。

本課程假設您建構的應用程式會向使用者顯示訊息,並接收使用者的回覆 (例如即時通訊應用程式)。本課程會說明如何擴充應用程式,將訊息傳遞至汽車裝置以供顯示及回覆。

立即開始

如要讓應用程式為汽車裝置提供訊息服務,應用程式必須符合下列條件:

  1. 建構並傳送包含回覆和標示為已讀取 Action 物件的 NotificationCompat.MessagingStyle 物件。
  2. 使用 Service 處理對話回覆,以及將對話標示為已讀取。
  3. 設定資訊清單,以指示應用程式支援 Android Auto。

概念及物件

開始設計應用程式前,建議您先瞭解 Android Auto 如何處理訊息。

個別通訊區塊稱為訊息,並以 MessagingStyle.Message 類別表示。訊息包含傳送者、訊息內容和訊息傳送時間。

使用者之間的通訊稱為對話,以 MessagingStyle 物件表示。對話 (或 MessagingStyle) 包含標題、訊息,以及對話是否屬於某個群組 (亦即該對話有多位收件者)。

為了通知使用者對話更新 (例如新訊息),應用程式會將 Notification 張貼至 Android 系統。這則「通知」使用 MessagingStyle 物件,在通知欄中顯示訊息專用的使用者介面。Android 平台也會將 Notification 傳遞至 Android Auto,系統會擷取 MessagingStyle 並用於透過汽車螢幕張貼通知。

應用程式也可以將 Action 物件新增至 Notification,讓使用者直接從通知欄快速回覆訊息或標示為已讀取。Android Auto 需要標示為已讀取和回覆 Action 物件,才能管理對話。

簡單來說,單一對話是由單一 Notification 物件呈現,呈現單一 MessagingStyle 物件的樣式。MessagingStyle 包含該對話中一個或多個 MessagingStyle.Message 物件的所有訊息。最後,為了完整支援 Android Auto,您必須在 Notification 中附加回覆,並將 Action 標示為已讀取。

訊息傳送流程

本節將說明應用程式和 Android Auto 之間的一般訊息傳送流程。

  1. 您的應用程式會收到訊息。
  2. 接著,您的應用程式會產生 MessagingStyle 通知,並提供回覆和標示為已讀取的 Action
  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,您可以在此路徑宣告應用程式支援哪些 Android Auto 功能。舉例來說,如要支援通知,請在 automotive_app_desc.xml 中加入下列內容:

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

如果您的應用程式需要支援處理簡訊、多媒體訊息和 RCS,您還必須提供下列資訊:

<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 核心程式庫的依附元件,如下所示:

    Groovy

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

    Kotlin

    dependencies {
       implementation("androidx.core:core:1.0.0")
    }
    

處理使用者動作

訊息應用程式需要透過 Action 處理對話更新。如果是 Android Auto,應用程式需要處理的兩種 Action 物件類型:回覆和標示為已讀取。建議的做法是使用 IntentServiceIntentService 可彈性處理背景潛在的昂貴呼叫,並釋出應用程式的主要執行緒。

定義意圖動作

Intent 動作 (勿與通知動作混淆) 是簡單的字串,可用於識別 Intent 的用途。由於單一服務可以處理多種類型的意圖,因此更容易定義多個 Intent.action 字串,而不用定義多個 IntentServices

我們的訊息應用程式範例有兩種動作:回覆與標示為已讀取,如以下程式碼範例所示。

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?) {
        // 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()
        }
    }
}

如要將此服務與您應用程式建立關聯,您也必須在應用程式資訊清單註冊這項服務,如下所示。

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

產生和處理「意圖」

其他應用程式無法取得觸發 MessagingServiceIntent,因為每個 Intent 均透過 PendingIntent 傳遞至外部應用程式。由於有此限制,您必須建立 RemoteInput 物件,讓其他應用程式能夠將「回覆」文字傳回至您的應用程式,如下所示。

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

現在,您已找到回覆 Intent 中的內容,請處理 MessagingServiceACTION_REPLY 切換子句的 TODO,擷取相關資訊,如下所示:

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

您可以透過類似的方式處理標示為已讀取的Intent (但不需要使用 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
}

以下程式碼範例說明如何處理 MessagingServiceACTION_MARK_AS_READ 切換子句的 TODO

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

設定回覆 Action 之前,請注意 Android Auto 針對回覆 Action 設有以下三個規定:

  • 語意動作必須設為 Action.SEMANTIC_ACTION_REPLY
  • Action 必須表示在觸發時不會顯示任何使用者介面。
  • Action 必須包含單一 RemoteInput

下列程式碼範例會設定回應 Action,同時遵守上述規定:

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

在標示為已讀的側邊,除了沒有 RemoteInput 之外,我們會執行相同的操作。因此,Android Auto 針對標示為已讀取的 Action 有 2 項規定:

  • 語意動作設為 Action.SEMANTIC_ACTION_MARK_AS_READ
  • 動作表示觸發時不會顯示任何使用者介面。
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
}

建立 MessagingStyle

MessagingStyle 是訊息資訊的運送者,是 Android Auto 用來朗讀對話中的每個訊息。首先,必須以 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()
    // ...

接著,您就可以建構 MessagingStyle 物件並提供部分對話的詳細資料。

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

最後,新增未讀訊息。

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

打包並推送通知

產生 ActionMessagingStyle 物件後,您現在可以建構並張貼 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)
}

另請參閱

回報 Android Auto Messaging 相關問題

如果您在開發 Android Auto 訊息應用程式的過程中遇到問題,可以使用 Google Issue Tracker 回報問題。請務必在問題範本中填寫所有必要資訊。

建立新問題

提交新問題之前,請確認問題清單中是否已回報此問題。您可以按一下追蹤問題中的星號圖示,訂閱問題並為問題投票。詳情請參閱「訂閱問題」一文。