The Android Developer Challenge is back! Submit your idea before December 2.

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

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

メッセージングをサポートするアプリは、メッセージング通知を拡張して、Android Auto の実行中に 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 が抽出され、車載ディスプレイを介して通知を送信するために使用されます。

また、アプリで Action オブジェクトを Notification に追加すると、ユーザーはメッセージにすばやく返信したり、通知シェードから直接既読にしたりすることが可能になります。Android Auto は、会話を管理するために、既読マークおよび返信の Action オブジェクトを必要とします。

簡単に言うと、単一の会話は、単一の 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 コアライブラリをインポートする

Auto デバイスで使用する通知を作成するには、AndroidX コアライブラリが必要です。これは、次の手順でプロジェクトにインポートできます。

  1. 以下に示すように、最上位の build.gradle ファイルに Google の Maven リポジトリが含まれていることを確認します。

    allprojects {
            repositories {
                google()
            }
        }
        
  2. 以下に示すように、アプリ モジュールの build.gradle ファイルに AndroidX コアライブラリの依存関係を挿入します。

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

ユーザー操作を処理する

メッセージング アプリには、Action を介して会話の更新を処理する方法が必要です。Android Auto では、アプリが処理する必要がある Action オブジェクトには、返信と既読マークの 2 種類があります。これを処理するおすすめの方法は、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 と、このセクションで後述するリモート入力キーが必要です。次のコードサンプルは、そのようなサービスを作成します。

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 は、起動時にユーザー インターフェースを表示しないことを示す必要があります。
  • 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
    }
    

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

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

関連ドキュメント