建構 Android Auto 訊息應用程式

對許多駕駛人來說,透過訊息保持聯絡非常重要。使用者可以運用即時通訊應用程式,瞭解是否需要接送孩子,或是晚餐地點是否異動。而 Android 架構,則可讓訊息應用程式透過標準使用者介面將服務延伸至行車體驗,促使駕駛人專注於路況。

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

本指南會說明如何擴充能向使用者顯示訊息並接收回覆的應用程式 (如即時通訊應用程式),以便將訊息的顯示及回覆收據傳遞至 Auto 裝置。如需相關設計指南,請參閱 Design for Driving 網站的訊息應用程式

開始使用

如要為 Auto 裝置提供訊息服務,您必須在資訊清單中宣告應用程式支援 Android Auto,且應用程式要能執行以下操作:

概念和物件

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

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

使用者之間的通訊稱為「對話」,以 MessagingStyle 物件表示。對話 (即 MessagingStyle) 包含標題、訊息,以及是否為群組對話的說明。

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

Android Auto 也會要求應用程式將 Action 物件新增至 Notification,讓使用者直接從通知欄快速回覆訊息,或將訊息標示為已讀。

總而言之,單一對話是由 Notification 物件表示,該物件又以 MessagingStyle 物件做為樣式。MessagingStyle 在一或多個 MessagingStyle.Message 物件中含有該對話的所有訊息。而為了支援 Android Auto,應用程式必須將回覆和標示為已讀的 Action 物件附加到 Notification 中。

訊息傳送流程

本節將說明應用程式和 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。請在 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 核心程式庫依附元件,如以下範例所示:

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

處理使用者動作

訊息應用程式需要透過 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,此 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
}

接著在 MessagingServiceACTION_REPLY 切換子句中,擷取傳入回覆 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 切換子句不需要進一步的邏輯,如以下範例所示:

// 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 Messaging 相關問題

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

建立新問題

提交新問題之前,請確認該問題是否已回報至問題清單中。您可以在追蹤程式中按一下該問題的星號,訂閱該問題並投下一票。詳情請參閱訂閱問題一文。