多くのドライバーにとって、運転中もメッセージのやりとりができるということは重要なポイントです。チャットアプリがあれば、子どものお迎えが必要になったときやレストランが変更になったときに、通知を受け取ることができます。Android フレームワークでは、標準のユーザー インターフェースを使用してメッセージ アプリのサービスを拡張し、ドライバーが運転に集中できるユーザー エクスペリエンスを実現できます。
メッセージ機能をサポートするアプリの場合、メッセージ通知機能を拡張することで、Android Auto の実行中に通知機能を使用できるようになります。こうした通知は Android Auto に表示され、ユーザーは一貫性のある気が散らないインターフェースでメッセージを読み、応答することが可能です。さらに、MessagingStyle API を使用すると、Android Auto を備えたすべての Android デバイス向けに最適化されたメッセージ通知を利用するメリットが得られます。主な最適化としては、メッセージ通知に特化した UI、アニメーションの改善、インライン画像のサポートなどがあります。
このレッスンは、チャットアプリなど、ユーザーにメッセージを表示してユーザーの返信を受け取るアプリをすでに作成済みのデベロッパーを対象とし、そうしたアプリを拡張し、メッセージを Auto デバイスに渡して表示や返信を行う方法について説明します。
開始する
Auto デバイス向けにメッセージ サービスを提供できるアプリでは、次のことをできるようにする必要があります。
NotificationCompat.MessagingStyle
オブジェクト(返信および既読マーキングのAction
オブジェクトを含む)をビルドして送信する。Service
を使用して、返信を処理し、会話を既読としてマーキングする。- マニフェストを設定して、アプリが Android Auto に対応していることを示す。
概念とオブジェクト
アプリの設計を開始する前に、Android Auto がメッセージを処理する仕組みを把握しておいてください。
個々の通信チャンクは「メッセージ」と呼ばれ、MessagingStyle.Message
クラスで表現されます。メッセージには、送信者、メッセージ コンテンツ、送信時刻が格納されます。
ユーザー間の通信は「会話」と呼ばれ、MessagingStyle
オブジェクトで表現されます。会話(MessagingStyle
)には、タイトル、メッセージ、グループ会話かどうか(会話に複数の受信者が存在するか)に関する情報が格納されます。
会話の更新情報(新しいメッセージなど)をユーザーに通知するため、アプリは Android システムに Notification
を送信します。この通知は、MessagingStyle
オブジェクトを使用して、メッセージ機能固有の UI を通知シェードに表示します。Android プラットフォームも、この Notification
を Android Auto に渡します。MessagingStyle
が抽出され、車載ディスプレイに通知を送信する際に使用されます。
さらに、アプリで Notification
に Action
オブジェクトを追加して、メッセージにクイック返信する、通知シェードから直接既読にするという操作をユーザーができるようにすることも可能です。この場合、Android Auto は、会話を管理するために、既読マーキング用と返信用の Action
オブジェクトを必要とします。
要約すると、1 つの会話は、単一の MessagingStyle
オブジェクトによってスタイル設定された単一の Notification
オブジェクトで表されます。MessagingStyle
は、1 つまたは複数の MessagingStyle.Message
オブジェクトを使用して、会話内のすべてのメッセージを格納します。最後に、Android Auto に完全に準拠するには、返信および既読マーキングの Action
を Notification
にアタッチする必要があります。
メッセージ フロー
このセクションでは、アプリと Android Auto の間の一般的なメッセージング フローについて説明します。
- アプリがメッセージを受信します。
- アプリが、返信および既読マーキングの
Action
を含むMessagingStyle
通知を生成します。 - Android Auto が、Android システムから「新しい通知」イベントを受信して、
MessagingStyle
、返信Action
、既読マーキングAction
を見つけます。 - Android Auto が、車内用の通知を生成して表示します。
- ユーザーが車載ディスプレイで通知をタップした場合、Android Auto は、既読マーキング
Action
をトリガーします。- アプリはバックグラウンドでこの既読マーキング イベントを処理する必要があります。
- ユーザーが音声で通知に応答した場合、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 をプロジェクトにインポートする手順は次のとおりです。
- 以下に示すように、必ず最上位の
build.gradle
ファイルに Google の Maven リポジトリを組み込みます。
Groovy
allprojects { repositories { google() } }
Kotlin
allprojects { repositories { google() } }
以下に示すように、アプリ モジュールの
build.gradle
ファイルに AndroidX Core Library 依存関係を組み込みます。Groovy
dependencies { implementation 'androidx.core:core:1.0.0' }
Kotlin
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>
インテントを生成して処理する
各 Intent
は PendingIntent
を通じて外部アプリに渡されるため、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)
// ...
次に、この 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
に関して 3 つの要件があることにご注意ください。
- セマンティック アクションは
Action.SEMANTIC_ACTION_REPLY
に設定する必要があります。 Action
の起動時にユーザー インターフェースを表示しないことを指示する必要があります。Action
にRemoteInput
を 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)
}
関連ドキュメント
Android Auto のメッセージングに関する問題を報告する
Android Auto 用のメッセージング アプリの開発中に問題が発生した場合は、Google Issue Tracker を使用して報告できます。 問題テンプレートに必要な情報をすべて記入してください。
新しい問題を報告する前に、その問題がすでに問題リスト内で報告されていないかご確認ください。Issue Tracker 内で各問題の横にあるスターアイコンをクリックすると、問題を登録して投票できます。詳細については、問題を登録する手順をご覧ください。