Android Auto 用メッセージ アプリを作成する

多くのドライバーにとって、運転中もメッセージのやりとりができるということは重要なポイントです。チャットアプリがあれば、子どものお迎えが必要になったときやレストランが変更になったときに、通知を受け取れます。Android フレームワークは、標準のユーザー インターフェースを使用してメッセージ アプリのサービスを拡張し、ドライバーが運転に集中できるドライビング エクスペリエンスを実現します。

メッセージ機能をサポートするアプリの場合、メッセージ通知機能を拡張することで、Android Auto の実行中に通知機能を使用できるようになります。こうした通知は Android Auto に表示されます。ユーザーは、一貫性のある気が散らないインターフェースでメッセージを読んだり、応答したりできます。また、MessagingStyle API を使用すると、Android Auto を含むすべての Android デバイス向けに最適化されたメッセージ通知を受け取ることができます。主な最適化としては、メッセージ通知に特化した UI、アニメーションの改善、インライン画像のサポートなどがあります。

このガイドでは、ユーザーに対しメッセージを表示してユーザーの返信を受け取るアプリ(チャットアプリなど)を拡張し、Auto デバイスでメッセージの表示と返信の受け取りを行う方法について説明します。関連する設計ガイダンスについては、運転モードの設計に関するメッセージ アプリをご覧ください。

始める

Auto デバイスにメッセージ サービスを提供するには、アプリのマニフェストで Android Auto のサポートを宣言し、次のことをできるようにする必要があります。

  • 返信と既読マーキングの Action オブジェクトを含む NotificationCompat.MessagingStyle オブジェクトを作成して送信する。
  • Service を使用して、返信を処理し、会話を既読としてマークする。

概念とオブジェクト

アプリの設計を開始する前に、Android Auto がメッセージを処理する仕組みを把握しておいてください。

個々の通信チャンクは「メッセージ」と呼ばれ、MessagingStyle.Message クラスで表現されます。メッセージには、送信者、メッセージ コンテンツ、送信時刻が格納されます。

ユーザー間の通信は「会話」と呼ばれ、MessagingStyle オブジェクトで表現されます。会話(MessagingStyle)には、タイトル、メッセージ、会話がユーザーのグループに属するかどうかに関する情報が格納されます。

会話の更新情報(新しいメッセージなど)をユーザーに通知するため、アプリは Android システムに Notification を送信します。この NotificationMessagingStyle オブジェクトを使用して、メッセージ機能固有の UI を通知シェードに表示します。Android プラットフォームも、この Notification を Android Auto に渡します。MessagingStyle が抽出され、車載ディスプレイに通知を送信する際に使用されます。

Android Auto では、アプリの NotificationAction オブジェクトを追加して、ユーザーが通知シェードから直接、メッセージに素早く返信したり、メッセージを既読にしたりできるようにする必要があります。

要約すると、1 つの会話は、MessagingStyle オブジェクトによってスタイル設定された Notification オブジェクトで表されます。MessagingStyle は、その会話内のすべてのメッセージを 1 つ以上の 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>

このマニフェスト エントリは、パス YourAppProject/app/src/main/res/xml/automotive_app_desc.xml で作成する必要がある別の XML ファイルを参照します。automotive_app_desc.xml で、アプリがサポートする Android Auto の機能を宣言します。たとえば通知のサポートを宣言するには、次の行を追加します。

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

アプリをデフォルトの SMS ハンドラとして設定できる場合は、必ず次の <uses> 要素を追加してください。そうしなかった場合、アプリがデフォルトの SMS ハンドラとして設定されると、Android Auto に組み込まれているデフォルトのハンドラが着信 SMS / MMS メッセージの処理に使用され、通知が重複する可能性があります。

<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.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 の場合、アプリは、返信と既読マーキングという 2 種類の Action オブジェクトを処理する必要があります。これは IntentService を使用して処理することをおすすめします。コストが高くなる可能性のある呼び出しをバックグラウンドで柔軟に処理でき、アプリのメインスレッドが開放されます。

インテント アクションを定義する

Intent アクションは、Intent の目的を明示するシンプルな文字列です。1 つのサービスで複数のタイプのインテントを処理できるため、複数の IntentService コンポーネントを定義するより、複数のアクション文字列を定義するほうが簡単です。

このガイドのメッセージ アプリ例には、次のコードサンプルに示すように、返信と既読マーキングという 2 種類の必須のアクションがあります。

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>

インテントを生成して処理する

IntentPendingIntent を通じて他のアプリに渡されるため、Android Auto を含め、MessagingService をトリガーする Intent を他のアプリが取得する方法はありません。この制限があるため、次の例に示すように、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 スイッチ句で、返信 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 に対応した通知の生成です。

アクションを作成する

Notification を使用して Action オブジェクトを他のアプリに渡すことで、元のアプリのメソッドをトリガーできます。このようにして、Android Auto は会話を既読にしたり会話に返信したりできます。

Action を作成するには、Intent から始めます。次の例は、「返信」Intent を作成する方法を示しています。

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

次に、この IntentPendingIntent でラップして、外部アプリで使用できるようにします。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 に関して 3 つの要件があることにご注意ください。

  • セマンティック アクションは Action.SEMANTIC_ACTION_REPLY に設定する必要があります。
  • Action の起動時にユーザー インターフェースを表示しないことを指示する必要があります。
  • ActionRemoteInput を 1 つ含める必要があります。

次のコードサンプルは、上記の要件に対応する返信 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 に 2 つの要件があります。

  • セマンティック アクションを 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() という 2 つのメソッドが使用されます。この 2 つのメソッドは、それぞれの PendingIntent のリクエスト コードとして機能し、Android で既存のペンディング インテントを制御するために使用されます。create() メソッドは、会話ごとに一意の ID を返す必要がありますが、同じ会話に対する呼び出しが繰り返される場合は、生成済みの一意の ID を返す必要があります。

A と B という 2 つの会話の例を考えてみましょう。会話 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
}

通知をパッケージ化してプッシュする

Action オブジェクトと MessagingStyle オブジェクトを生成した後、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 を使用して報告できます。 問題テンプレートに必要な情報をすべて記入してください。

新しい問題を報告する

新しい問題を報告する前に、その問題がすでに問題リスト内で報告されていないかご確認ください。Issue Tracker 内で各問題の横にあるスターアイコンをクリックすると、問題を登録して投票できます。詳細については、問題を登録する手順をご覧ください。