Google は、黒人コミュニティに対する人種平等の促進に取り組んでいます。取り組みを見る

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

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

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

このレッスンは、チャットアプリなど、ユーザーにメッセージを表示してユーザーの返信を受け取るアプリをすでに作成済みのデベロッパーを対象とし、そうしたアプリを拡張し、メッセージを Auto デバイスに渡して表示や返信を行う方法について説明します。

開始する

Auto デバイス向けにメッセージ サービスを提供できるアプリでは、次のことできるようにする必要があります。

  1. NotificationCompat.MessagingStyle オブジェクト(返信および既読マーキングの Action オブジェクトを含む)をビルドして送信する。
  2. Service を使用して、返信を処理し、会話を既読としてマーキングする。
  3. マニフェストを設定して、アプリが Android Auto に対応していることを示す。

概念とオブジェクト

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

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

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

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

さらに、アプリで NotificationAction オブジェクトを追加して、メッセージにクイック返信する、通知シェードから直接既読にするという操作をユーザーができるようにすることも可能です。この場合、Android Auto は、会話を管理するために、既読マーキング用と返信用の Action オブジェクトを必要とします。

要約すると、1 つの会話は、単一の MessagingStyle オブジェクトによってスタイル設定された単一の Notification オブジェクトで表されます。MessagingStyle は、1 つまたは複数の MessagingStyle.Message オブジェクトを使用して、会話内のすべてのメッセージを格納します。最後に、Android Auto に完全に準拠するには、返信および既読マーキングの ActionNotification にアタッチする必要があります。

メッセージ フロー

このセクションでは、アプリと 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 ファイルを参照します。このファイルでは、アプリがサポートする Android Auto 機能を宣言します。たとえば、通知のサポートを宣言するには、automotive_app_desc.xml に次の行を挿入します。

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

アプリが SMS、MMS、RCS の処理をサポートする必要がある場合は、次のような行も挿入する必要があります。

<automotiveApp>
        ...
        <uses name="sms" />
    </automotiveApp>
    

AndroidX Core Library をインポートする

Auto デバイスで使用する通知を作成するには、AndroidX Core Library が必要です。AndroidX Core Library をプロジェクトにインポートする手順は次のとおりです。

  1. 以下に示すように、必ず最上位の build.gradle ファイルに Google の Maven リポジトリを組み込みます。

    allprojects {
            repositories {
                google()
            }
        }
        
  2. 以下に示すように、アプリ モジュールの build.gradle ファイルに AndroidX Core Library 依存関係を組み込みます。

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

ユーザー アクションを処理する

メッセージ アプリには、Action による会話の更新を処理する仕組みが必要です。Android Auto の場合、アプリは、返信と既読マーキングという 2 種類の Action オブジェクトを処理する必要があります。推奨されているのは IntentService を使用する方法です。IntentService は、潜在的に高コストの呼び出しをバックグラウンドで柔軟に処理する機能を提供し、アプリのメインスレッドを解放します。

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

Intent アクション(通知アクションと混同しないでください)は、Intent の目的を指定するシンプルな文字列です。1 つのサービスで複数のタイプのインテントを処理できるため、複数の IntentServices を定義するより、複数の Intent.action 文字列を定義する方が簡単です。

ここで使用するメッセージ アプリの例の場合、返信と既読マーキングという 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?) {
            // 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>
    

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

IntentPendingIntent を通じて外部アプリに渡されるため、MessagingService をトリガーする Intent を他のアプリが取得する方法はありません。この制限があるため、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 に入る内容がわかったところで、次に MessagingService 内で ACTION_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
    }
    

MessagingService 内で ACTION_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)
        // ...
    

次に、この IntentPendingIntent 内にラップして、外部アプリで使用できるようにします。PendingIntent は、ラップした Intent へのアクセスをロックし、メソッドの一部のセット(受信側アプリが Intent アクションを起動するか、発信側アプリのパッケージ名を取得することを可能にするメソッド)だけをエクスポーズする以外は、内在する Intent やその内部のデータに外部アプリがアクセスすることを許可しません。

    // ...
        val replyPendingIntent = PendingIntent.getService(
            context,
            createReplyId(appConversation), // Method explained later.
            replyIntent,
            PendingIntent.FLAG_UPDATE_CURRENT)
        // ...
    

返信 Action をセットアップする前に、Android Auto には、返信 Action に関して 3 つの要件があることにご注意ください。

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

上記の要件を満たした返信 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
    }
    

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

Action オブジェクトと MessagingStyle オブジェクトを生成した後、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)
    }
    

関連ドキュメント