1. 始める前に
対象外:
- Android Auto および Android Automotive OS 向けのメディアアプリ(音楽、ラジオ、ポッドキャストなどのオーディオ アプリ)を作成する方法については説明していません。これらのアプリの作成方法について詳しくは、自動車向けメディアアプリを作成するをご覧ください。
必要なもの
- Android Studio プレビュー版。この Codelab で使用する一部の Android Automotive OS エミュレータは、Android Studio プレビュー版でのみ利用できます。まだ Android Studio プレビュー版をインストールしていない場合でも、ダウンロードしている間に安定版を使用して Codelab を開始することはできます。
- Kotlin に関する基本的な経験。
- Android 仮想デバイスを作成して Android Emulator で実行した経験。
- Jetpack Compose に関する基本的な知識。
- 副作用についての理解。
- ウィンドウ インセットに関する基本的な知識。
作成するアプリの概要
この Codelab では、既存の動画ストリーミング モバイルアプリ Road Reels を Android Automotive OS に移行する方法について学びます。
スマートフォンで実行されている初期状態版のアプリ。 | Android Automotive OS エミュレータで実行されている完成版のアプリ。ディスプレイ カットアウトが追加されている。 |
学習内容
- Android Automotive OS エミュレータの使用方法
- Android Automotive OS ビルドの作成に必要な変更
- モバイル向けアプリ開発時の一般的な想定を、Android Automotive OS で実行するアプリに適用できない場合の対処方法
- 自動車用アプリのさまざまな品質レベル
- メディア セッションを使用して、他のソースからアプリの再生を操作できるようにする方法
- システム UI とウィンドウ インセットが Android Automotive OS デバイスとモバイル デバイスでどう異なるか
2. 設定する
コードを取得する
- この Codelab のコードは、
car-codelabs
GitHub リポジトリ内のbuild-a-parked-app
ディレクトリにあります。クローンを作成するには、次のコマンドを実行します。
git clone https://github.com/android/car-codelabs.git
- または、リポジトリを ZIP ファイルとしてダウンロードすることもできます。
プロジェクトを開く
- Android Studio を起動し、
build-a-parked-app/start
ディレクトリのみを選択してプロジェクトをインポートします。build-a-parked-app/end
ディレクトリにはソリューション コードが含まれています。不明な点がある場合や、プロジェクト全体を確認したいときは、いつでも参照できます。
コードを理解する
- Android Studio でプロジェクトを開いたら初期状態のコードを確認します。
3. Android Automotive OS 用の駐車時向けアプリについて理解する
駐車時向けアプリは、Android Automotive OS でサポートされているアプリカテゴリのサブセットです。執筆時点では、動画ストリーミング アプリ、ウェブブラウザ、ゲームからなります。電気自動車の普及が進んでおり、車の充電中にドライバーや同乗者がアプリを利用する機会が増えることを考えると、これらのアプリと Google 搭載のハードウェアを備えた自動車の相性は非常に良いと言えます。
自動車に搭載されている画面は、多くの点で他の大画面デバイス(タブレット、折りたたみ式デバイスなど)と似ています。同程度のサイズ、解像度、アスペクト比のタッチスクリーンが、縦向きまたは横向きで設置されています(画面の向きが固定されている点はタブレットと異なります)。また、ネットワーク接続が頻繁に切り替わるコネクテッド デバイス、という点も似ています。以上の点を考慮すると、大画面向けに最適化されているアプリであれば、最小限の追加作業で自動車向けの優れたユーザー エクスペリエンスを実現できそうです。
大画面アプリと同様、自動車用アプリにも品質レベルがあります。
- レベル 3 - 自動車対応: 大画面に対応しており、駐車時に使用できるアプリです。自動車向けに最適化された機能は搭載していませんが、他の大画面 Android デバイスと同じようにアプリを使用できます。これらの要件を満たすモバイルアプリは、自動車対応モバイルアプリ プログラムを通じて自動車に配信できます。
- レベル 2 - 自動車向けに最適化: 自動車のセンター コンソール ディスプレイで優れたエクスペリエンスを提供できるアプリです。このレベルの要件を満たすには、アプリのカテゴリに応じて自動車専用の変更を加え、運転モードや駐車モードで使用できる機能を含める必要があります。
- レベル 1 - 自動車向けに差別化: 自動車のさまざまなハードウェアで動作するように構築されており、運転モードと駐車モードでエクスペリエンスを調整できるアプリです。センター コンソール、計器類、追加の画面(たとえば高級車に搭載されているパノラマ ディスプレイ)など、車内のさまざまな画面に合わせて設計された最適なユーザー エクスペリエンスを提供します。
4. Android Automotive OS エミュレータでアプリを実行する
Automotive with Play Store のシステム イメージをインストールする
- まず、Android Studio で SDK Manager を開き、まだ選択していない場合は [SDK Platforms] タブを選択します。SDK Manager ウィンドウの右下にある [Show package details] チェックボックスがオンになっていることを確認します。
- Generic System Image の追加の一覧から、いずれかの Automotive with Play Store エミュレータ イメージをインストールします。イメージは、同じアーキテクチャ(x86 / ARM)のマシンでのみ実行できます。
Android Automotive OS Android Virtual Device を作成する
- デバイス マネージャーを開き、ウィンドウの左側にある [Category] 列で [Automotive] を選択します。次に、バンドルされたハードウェア プロファイルの一覧から Automotive (1024p landscape) を選択し、[Next] をクリックします。
- 次のページに移動したら、前の手順で作成したシステム イメージを選択します。[Next] をクリックし、必要に応じて詳細オプションを選択したら、[Finish] をクリックして AVD を作成します。注: API 30 のイメージを選択した場合は、[Recommended] タブ以外のタブに表示されることがあります。
アプリを実行する
前の手順で作成したエミュレータで、既存の app
実行構成を使用してアプリを実行します。アプリを操作してさまざまな画面を表示し、スマートフォンまたはタブレットのエミュレータで実行した場合の動作と比較してみてください。
5. Android Automotive OS ビルドを作成する
アプリはこのままでも動作しますが、Android Automotive OS で適切に動作するアプリにするため、また Google Play ストアへの公開要件を満たすためには、いくつかの変更を加える必要があります。これらの変更の中には、アプリのモバイル バージョンに含めても意味がないものもあります。そこで、まずは Android Automotive OS ビルド バリアントを作成します。
フォーム ファクタのフレーバー ディメンションを追加する
まず、build.gradle.kts
ファイルの flavorDimensions
を変更して、ビルドの対象とするフォーム ファクタのフレーバー ディメンションを追加します。次に、それぞれのフォーム ファクタ(mobile
および automotive
)に productFlavors
ブロックとフレーバーを追加します。
詳細については、プロダクト フレーバーを設定するをご覧ください。
build.gradle.kts(Module :app)
android {
...
flavorDimensions += "formFactor"
productFlavors {
create("mobile") {
// Inform Android Studio to use this flavor as the default (e.g. in the Build Variants tool window)
isDefault = true
// Since there is only one flavor dimension, this is optional
dimension = "formFactor"
}
create("automotive") {
// Since there is only one flavor dimension, this is optional
dimension = "formFactor"
// Adding a suffix makes it easier to differentiate builds (e.g. in the Play Console)
versionNameSuffix = "-automotive"
}
}
...
}
build.gradle.kts
ファイルを更新すると、ファイル上部のバナーに「Gradle files have changed since last project sync. A project sync may be necessary for the IDE to work properly.」(プロジェクトの最後の同期以降に Gradle ファイルが変更されています。IDE が正しく動作するためには、プロジェクトの同期が必要になる場合があります。)と表示されます。Android Studio がこれらのビルド構成の変更をインポートできるようにするため、このバナーの [Sync Now] ボタンをクリックします。
次に、[Build] > [Select Build Variant...] のメニュー項目から [Build Variants] ツール ウィンドウを開き、automotiveDebug
バリアントを選択します。これにより、automotive
ソースセットのファイルが [Project] ウィンドウに表示され、Android Studio でアプリを実行する際にこのビルド バリアントが使用されるようになります。
Android Automotive OS のマニフェストを作成する
次に、automotive
ソースセットの AndroidManifest.xml
ファイルを作成します。このファイルには、Android Automotive OS アプリに必要な要素が含まれています。
- [Project] ウィンドウで、
app
モジュールを右クリックします。表示されたプルダウンから、[New] > [Other] > [Android Manifest File] を選択します。 - 表示された [New Android Component] ウィンドウで、新しいファイルの [Target Source Set] として
automotive
を選択します。[Finish] をクリックしてファイルを作成します。
- 作成した
AndroidManifest.xml
ファイル(パスapp/src/automotive/AndroidManifest.xml
内)に、次のコードを追加します。
AndroidManifest.xml(automotive)
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- https://developer.android.com/training/cars/parked#required-features -->
<uses-feature
android:name="android.hardware.type.automotive"
android:required="true" />
<uses-feature
android:name="android.hardware.wifi"
android:required="false" />
<uses-feature
android:name="android.hardware.screen.portrait"
android:required="false" />
<uses-feature
android:name="android.hardware.screen.landscape"
android:required="false" />
</manifest>
1 つ目の宣言は、ビルド アーティファクトを Google Play Console の Android Automotive OS トラックにアップロードするために必要なものです。この機能は、Google Play で android.hardware.type.automotive
機能(自動車など)を備えたデバイスにアプリを配信するためだけに使用します。
それ以外の宣言は、自動車のさまざまなハードウェア構成にアプリをインストールするために必要なものです。詳しくは、必須の Android Automotive OS の機能をご覧ください。
アプリを動画アプリとしてマークする
追加する必要がある最後のメタデータは automotive_app_desc.xml
ファイルです。このファイルは、自動車向け Android のコンテキスト内でアプリのカテゴリを宣言するために使用します。ここで宣言するカテゴリは、Google Play Console で選択したアプリのカテゴリとは別物です。
app
モジュールを右クリックして [New] > [Android Resource File] を選択し、以下の値を入力してから [OK] をクリックします。
- [File name]:
automotive_app_desc.xml
- [Resource type]:
XML
- [Root element]:
automotiveApp
- [Source set]:
automotive
- [Directory name]:
xml
- このファイル内に次の
<uses>
要素を追加して、アプリが動画アプリであることを宣言します。
automotive_app_desc.xml
<?xml version="1.0" encoding="utf-8"?>
<automotiveApp xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses
name="video"
tools:ignore="InvalidUsesTagAttribute" />
</automotiveApp>
automotive
ソースセットのAndroidManifest.xml
ファイル(<uses-feature>
要素を追加したファイル)に、空の<application>
要素を追加します。その中に次の<meta-data>
要素を追加して、作成したautomotive_app_desc.xml
ファイルを参照します。
AndroidManifest.xml(automotive)
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
...
<application>
<meta-data
android:name="com.android.automotive"
android:resource="@xml/automotive_app_desc" />
</application>
</manifest>
これで、アプリの Android Automotive OS ビルドの作成に必要な変更がすべて完了しました。
6. Android Automotive OS の品質要件を満たす: 操作性
Android Automotive OS のビルド バリアントを作成したら、そのアプリを自動車で安全に使用できるかどうかを確認する必要があります。
ナビゲーション アフォーダンスを追加する
Android Automotive OS エミュレータでアプリを実行したとき、詳細画面からメイン画面、またはプレーヤー画面から詳細画面に戻れないことに気付いたでしょうか。他のフォーム ファクタでは、「戻る」ナビゲーションを有効にするための [戻る] ボタンやタッチ操作が必要になりますが、Android Automotive OS デバイスにはそのような要件はありません。そのため、アプリの UI でナビゲーション アフォーダンスを提供して、ユーザーがアプリ内の画面で立ち往生しないようナビゲートする必要があります。この要件は、品質に関するガイドライン AN-1 として規定されています。
詳細画面からメイン画面への「戻る」ナビゲーションをサポートするには、次のように詳細画面の CenterAlignedTopAppBar
に navigationIcon
パラメータを追加します。
RoadReelsApp.kt
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
...
navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null
)
}
}
プレーヤー画面からメイン画面への「戻る」ナビゲーションをサポートするための手順は次のとおりです。
TopControls
コンポーザブルに変更を加え、onClose
というコールバック パラメータを取れるようにするとともに、クリック時にそれを呼び出すIconButton
を追加します。
PlayerControls.kt
@Composable
fun TopControls(
title: String?,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Box(modifier) {
IconButton(
modifier = Modifier
.align(Alignment.TopStart),
onClick = onClose
) {
Icon(
Icons.TwoTone.Close,
contentDescription = "Close player",
tint = Color.White
)
}
if (title != null) { ... }
}
}
PlayerControls
コンポーザブルに変更を加え、onClose
コールバック パラメータを取ってTopControls
に渡せるようにします。
PlayerControls.kt
fun PlayerControls(
visible: Boolean,
playerState: PlayerState,
onClose: () -> Unit,
onPlayPause: () -> Unit,
onSeek: (seekToMillis: Long) -> Unit,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut()
) {
Box(modifier = modifier.background(Color.Black.copy(alpha = .5f))) {
TopControls(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.screen_edge_padding))
.align(Alignment.TopCenter),
title = playerState.mediaMetadata.title?.toString(),
onClose = onClose
)
...
}
}
}
- 次に
PlayerScreen
コンポーザブルに変更を加え、同じパラメータを取ってPlayerControls
に渡せるようにします。
PlayerScreen.kt
@Composable
fun PlayerScreen(
onClose: () -> Unit,
modifier: Modifier = Modifier,
) {
...
PlayerControls(
modifier = Modifier
.fillMaxSize(),
visible = isShowingControls,
playerState = playerState,
onClose = onClose,
onPlayPause = { if (playerState.isPlaying) player.pause() else player.play() },
onSeek = { player.seekTo(it) }
)
}
- 最後に
RoadReelsNavHost
で、PlayerScreen
に渡す実装を指定します。
RoadReelsNavHost.kt
composable(route = Screen.Player.name) {
PlayerScreen(onClose = { navController.popBackStack() })
}
これで、ユーザーが立ち往生することなく画面間を移動できるようになりました。また、これにより他のフォーム ファクタでもユーザー エクスペリエンスが向上する可能性があります。たとえば、細長いスマートフォンの画面上部を手で持って操作しているときに、デバイスを持ち替えなくてもアプリを簡単に操作できるようにすることも可能です。
画面の向きのサポートに対応する
ほとんどのモバイル デバイスとは異なり、自動車の場合は画面の向きが固定されているのが一般的です。画面を回転させることができないため、横向きまたは縦向きのいずれか一方のみがサポートされます。そのため、アプリでも両方の向きがサポートされていることを想定すべきではありません。
Android Automotive OS のマニフェストを作成するでは、android.hardware.screen.portrait
機能と android.hardware.screen.landscape
機能用に、2 つの <uses-feature>
要素を追加してrequired
属性を false
に設定しました。これにより、どちらかの画面の向きに暗黙的に依存することが原因で、アプリが自動車に配信されなくなることを回避できます。ただし、これらのマニフェスト要素はアプリの配信方法にのみ影響し、アプリの動作を変更するものではありません。
現時点でこのアプリには、動画プレーヤーを開いたときにアクティビティの向きを自動的に横向きに設定する便利な機能があります。この機能により、スマートフォンが横向きになっていない場合でも、ユーザーがデバイスを操作して向きを変更する必要はありません。
しかしながら、現在市販されている自動車にはデバイスが縦向きに固定されているものもあり、その場合はちらつきループが発生したりレターボックス表示になったりする可能性があります。
この問題を解決するには、デバイスでサポートされている画面の向きに基づくチェックを追加します。
- 実装を簡単にするため、まず
Extensions.kt
に以下を追加します。
Extensions.kt
import android.content.Context
import android.content.pm.PackageManager
...
enum class SupportedOrientation {
Landscape,
Portrait,
}
fun Context.supportedOrientations(): List<SupportedOrientation> {
return when (Pair(
packageManager.hasSystemFeature(PackageManager.FEATURE_SCREEN_LANDSCAPE),
packageManager.hasSystemFeature(PackageManager.FEATURE_SCREEN_PORTRAIT)
)) {
Pair(true, false) -> listOf(SupportedOrientation.Landscape)
Pair(false, true) -> listOf(SupportedOrientation.Portrait)
// For backwards compat, if neither feature is declared, both can be assumed to be supported
else -> listOf(SupportedOrientation.Landscape, SupportedOrientation.Portrait)
}
}
- 次に、リクエストされた向きに設定するための呼び出しを無効にします。同様の問題は、モバイル デバイスのマルチ ウィンドウ モードでも発生する可能性があります。この場合も、向きが動的に設定されないようにチェックを追加できます。
PlayerScreen.kt
import com.example.android.cars.roadreels.SupportedOrientation
import com.example.android.cars.roadreels.supportedOrientations
...
LaunchedEffect(Unit) {
...
// Only automatically set the orientation to landscape if the device supports landscape.
// On devices that are portrait only, the activity may enter a compat mode and won't get to
// use the full window available if so. The same applies if the app's window is portrait
// in multi-window mode.
if (context.supportedOrientations().contains(SupportedOrientation.Landscape)
&& !context.isInMultiWindowMode
) {
context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
}
...
}
[チェックを追加する前]: Polestar 2 エミュレータのプレーヤー画面でちらつきループが発生している(アクティビティによって | [チェックを追加する前]: Polestar 2 エミュレータのプレーヤー画面がレターボックス表示になっている(アクティビティによって | [チェックを追加した後]: Polestar 2 エミュレータのプレーヤー画面がレターボックス表示になっていない |
このアプリではこれ以外の場所で画面の向きを設定していないため、これでレターボックス表示を回避できます。アプリ内に、横向きまたは縦向き限定の screenOrientation
属性や setRequestedOrientation
呼び出し(センサー、反転、それぞれのユーザー バリアントを含む)がないか確認し、必要に応じて削除または無効化してレターボックス表示を制限してください。詳しくは、デバイスの互換モードをご覧ください。
システムバーが制御可能かどうかによって対応する
ここまでの変更により、ちらつきループやレターボックス表示は回避できましたが、「システムバーは常に非表示にできる」という想定が当てはまらなくなっています。自動車に対するユーザーニーズはスマートフォンやタブレットとは異なるため、空調などの車両制御を常に画面に表示できるよう、OEM 向けにアプリがシステムバーを非表示にできないようにするオプションが用意されています。
そのため、システム バーを非表示にできると想定してアプリを没入モードでレンダリングすると、システムバーの背後にレンダリングされてしまう可能性があります。前の手順でも確認できるように、アプリがレターボックス表示にならない場合、プレーヤーの上下のコントロールは表示されなくなってしまいます。上に示した例では、プレーヤーの閉じるボタンが表示されておらず、シークバーも使用できないためアプリを操作できなくなっています。
最も簡単な解決方法は、次のように systemBars
ウィンドウ インセットのパディングをプレーヤーに適用する方法です。
PlayerScreen.kt
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
...
Box(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.systemBars)
) {
PlayerView(...)
PlayerControls(...)
}
ただし、この方法が理想的とは言えません。システムバーがフェードアウトするときに、他の UI 要素が飛び跳ねるように移動してしまうからです。
ユーザー エクスペリエンスを改善するには、どのインセットが操作可能かを把握し、操作できないインセットにのみパディングを適用するようアプリを更新します。
- アプリ内の他の画面でもウィンドウ インセットが必要になる可能性を考え、操作可能なインセットを
CompositionLocal
として渡しておくとよいでしょう。com.example.android.cars.roadreels
パッケージに新しいファイルLocalControllableInsets.kt
を作成し、次のコードを追加します。
LocalControllableInsets.kt
import androidx.compose.runtime.compositionLocalOf
// Assume that no insets can be controlled by default
const val DEFAULT_CONTROLLABLE_INSETS = 0
val LocalControllableInsets = compositionLocalOf { DEFAULT_CONTROLLABLE_INSETS }
- 変化をリッスンできるよう
OnControllableInsetsChangedListener
を設定します。
MainActivity.kt
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat.OnControllableInsetsChangedListener
...
class MainActivity : ComponentActivity() {
private lateinit var onControllableInsetsChangedListener: OnControllableInsetsChangedListener
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
var controllableInsetsTypeMask by remember { mutableIntStateOf(DEFAULT_CONTROLLABLE_INSETS) }
onControllableInsetsChangedListener =
OnControllableInsetsChangedListener { _, typeMask ->
if (controllableInsetsTypeMask != typeMask) {
controllableInsetsTypeMask = typeMask
}
}
WindowCompat.getInsetsController(window, window.decorView)
.addOnControllableInsetsChangedListener(onControllableInsetsChangedListener)
RoadReelsTheme {
RoadReelsApp(calculateWindowSizeClass(this))
}
}
}
override fun onDestroy() {
super.onDestroy()
WindowCompat.getInsetsController(window, window.decorView)
.removeOnControllableInsetsChangedListener(onControllableInsetsChangedListener)
}
}
- テーマとアプリのコンポーザブルを含む最上位レベルの
CompositionLocalProvider
を追加し、値をLocalControllableInsets
にバインドします。
MainActivity.kt
import androidx.compose.runtime.CompositionLocalProvider
...
CompositionLocalProvider(LocalControllableInsets provides controllableInsetsTypeMask) {
RoadReelsTheme {
RoadReelsApp(calculateWindowSizeClass(this))
}
}
- プレーヤーで現在の値を読み取り、その値を使用してパディングに使用するインセットを特定します。
PlayerScreen.kt
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.union
import androidx.compose.ui.unit.dp
import com.example.android.cars.roadreels.LocalControllableInsets
...
val controllableInsetsTypeMask = LocalControllableInsets.current
// When the system bars can be hidden, ignore them when applying padding to the player and
// controls so they don't jump around as the system bars disappear. If they can't be hidden
// include them so nothing renders behind the system bars
var windowInsetsForPadding = WindowInsets(0.dp)
if (controllableInsetsTypeMask.and(WindowInsetsCompat.Type.statusBars()) == 0) {
windowInsetsForPadding = windowInsetsForPadding.union(WindowInsets.statusBars)
}
if (controllableInsetsTypeMask.and(WindowInsetsCompat.Type.navigationBars()) == 0) {
windowInsetsForPadding = windowInsetsForPadding.union(WindowInsets.navigationBars)
}
Box(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(windowInsetsForPadding)
) {
PlayerView(...)
PlayerControls(...)
}
システムバーを非表示にできる場合でも、コンテンツが飛び跳ねるように移動することはない | システムバーを非表示にできない場合は、コンテンツがそのまま表示される |
これでかなり改善されました。システムバーを非表示にしたときにコンテンツが飛び跳ねるように移動することはありません。また、システムバーを制御できない自動車でもコントロールは完全に表示されます。
7. Android Automotive OS の品質要件を満たす: ドライバーの注意散漫
最後に、自動車とそれ以外のフォーム ファクタとの大きな違いとして、「自動車は運転するもの」という点を指摘しておく必要があります。したがって、運転中にドライバーの注意を逸らさないようにすることが非常に重要です。Android Automotive OS 用の駐車時向けアプリでは、運転を開始するときに必ず再生を一時停止する必要があります。運転を開始するとシステム オーバーレイが表示され、オーバーレイされるアプリの onPause
ライフサイクル イベントが呼び出されます。この呼び出しの際に、アプリでの再生を一時停止する必要があります。
運転をシミュレーションする
エミュレータでプレーヤー ビューに移動し、コンテンツの再生を開始します。次に、運転をシミュレーションするの手順に沿って操作します。この段階では、アプリの UI がシステムによってオーバーレイされても再生は中断されません。この動作は、自動車向けアプリの品質に関するガイドライン DD-2 に違反しています。
運転を開始したら再生を一時停止する
androidx.lifecycle:lifecycle-runtime-compose
アーティファクトに対する依存関係を追加します。このアーティファクトには、ライフサイクル イベントでコードを実行するために使用するLifecycleEventEffect
が含まれています。
libs.version.toml
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
Build.gradle.kts(Module :app)
implementation(libs.androidx.lifecycle.runtime.compose)
- プロジェクトを同期して依存関係がダウンロードされたら、再生を一時停止するため、
ON_PAUSE
イベントで実行するLifecycleEventEffect
を追加します。
PlayerScreen.kt
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
...
@Composable
fun PlayerScreen(...) {
...
LifecycleEventEffect(Lifecycle.Event.ON_PAUSE) {
player.pause()
}
LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
player.play()
}
...
}
修正を実装したら、先ほどと同じ手順で再生中の運転をシミュレーションし、再生が停止することを確認します。これで、DD-2 の要件を満たすことができました。
8. 遠隔ディスプレイ エミュレータでアプリをテストする
自動車で新たに採用され始めているのが 2 画面構成です。たとえば、メイン画面をセンター コンソールに配置し、サブ画面をダッシュボード上部のフロントガラス近くに配置するような構成です。アプリをメイン画面とサブ画面の間で移動させることで、ドライバーや同乗者にさまざまなオプションを提供できます。
自動車の遠隔ディスプレイのイメージをインストールする
- まず、Android Studio で SDK Manager を開き、まだ選択していない場合は [SDK Platforms] タブを選択します。SDK Manager ウィンドウの右下にある [Show package details] チェックボックスがオンになっていることを確認します。
- お使いパソコンのアーキテクチャ(x86 / ARM)に対応する Automotive Distant Display with Google APIs エミュレータのイメージをインストールします。
Android Automotive OS Android Virtual Device を作成する
- デバイス マネージャーを開き、ウィンドウの左側にある [Category] 列で [Automotive] を選択します。次に、一覧から Automotive Distant Display のバンドルされたハードウェア プロファイル を選択し、[Next] をクリックします。
- 次のページに移動したら、前の手順で作成したシステム イメージを選択します。[Next] をクリックし、必要に応じて詳細オプションを選択したら、[Finish] をクリックして AVD を作成します。
アプリを実行する
前の手順で作成したエミュレータで、既存の app
実行構成を使用してアプリを実行します。遠隔ディスプレイ エミュレータを使用してテストするの手順に沿って、アプリを遠隔ディスプレイに移動したりメイン画面に戻したりします。アプリの移動と操作は、メイン / 詳細画面とプレーヤー画面の両方でテストしてください。
9. 遠隔ディスプレイのアプリ エクスペリエンスを改善する
遠隔ディスプレイでアプリを使用した際に、次の 2 点に気付いたでしょうか。
- 遠隔ディスプレイとの間でアプリを移動すると再生が最初に戻る。
- アプリが遠隔ディスプレイに表示されている間は、再生状態を変更するなどのアプリ操作を行えない。
アプリの継続性を改善する
再生が最初に戻る問題は、構成変更に伴ってアクティビティが再作成されることが原因です。アプリは Compose を使用して作成されており、構成変更がサイズ関連であるため、サイズベースの構成変更に関するアクティビティの再作成を制限することで、Compose で構成変更が処理されるように簡単に修正できます。これにより、アクティビティの再作成による再生停止や再読み込みが発生しなくなり、ディスプレイ間をシームレスに移動できるようになります。
AndroidManifest.xml
<activity
android:name="com.example.android.cars.roadreels.MainActivity"
...
android:configChanges="screenSize|smallestScreenSize|orientation|screenLayout|density">
...
</activity>
再生コントロールを実装する
遠隔ディスプレイに移動したときにアプリを操作できない問題を解決するには MediaSession
を実装します。MediaSession は、音声や動画のプレーヤーを操作するための汎用的な手段を提供します。詳しくは、MediaSession を使用して再生を制御およびアドバタイズするをご覧ください。
androidx.media3:media3-session
アーティファクトへの依存関係を追加します。
libs.version.toml
androidx-media3-mediasession = { group = "androidx.media3", name = "media3-session", version.ref = "media3" }
build.gradle.kts (Module :app)
implementation(libs.androidx.media3.mediasession)
- ビルダーを使用して
MediaSession
を作成します。
PlayerScreen.kt
import androidx.media3.session.MediaSession
@Composable
fun PlayerScreen(...) {
...
val mediaSession = remember(context, player) {
MediaSession.Builder(context, player).build()
}
...
}
- 次に、
Player
コンポーザブルのDisposableEffect
のonDispose
ブロックに別の行を追加して、Player
がコンポジション ツリーを離れるときにMediaSession
をリリースします。
PlayerScreen.kt
DisposableEffect(Unit) {
onDispose {
mediaSession.release()
player.release()
...
}
}
- 最後に、プレーヤー画面で
adb shell cmd media_session dispatch
コマンドを使用してメディア コントロールをテストします。
# To play content
adb shell cmd media_session dispatch play
# To pause content
adb shell cmd media_session dispatch pause
# To toggle the playing state
adb shell cmd media_session dispatch play-pause
以上により、遠隔ディスプレイを備えた自動車でのアプリの動作がかなり改善されます。加えて、他のフォーム ファクタでの動作も改善されます。画面を回転できるデバイスや、ユーザーがアプリのウィンドウ サイズを変更できるデバイスにも、アプリがシームレスに対応できるようになります。
さらには、メディア セッションの統合により、自動車のハードウェアやソフトウェアのコントロールだけでなく、他のソース(Google アシスタントのクエリ、ヘッドフォンの一時停止ボタンなど)からもアプリの再生を操作できるようになり、さまざまなフォーム ファクタでアプリを操作するオプションを提供できるようになります。
10. さまざまなシステム構成でアプリをテストする
アプリがメインディスプレイと遠隔ディスプレイで適切に動作することが確認できたら、最後にさまざまなシステムバー構成とディスプレイ カットアウトがアプリでどのように処理されるかを確認します。ウィンドウ インセットとディスプレイ カットアウトを使用するでも説明していますが、Android Automotive OS デバイスの構成には、モバイル フォーム ファクタに適用される一般的な想定が当てはまない場合があります。
ここでは、実行時に構成可能なエミュレータをダウンロードし、左側にシステムバーが表示されるように構成されたエミュレータでアプリをテストします。
Android Automotive with Google APIs イメージをインストールする
- まず、Android Studio で SDK Manager を開き、まだ選択していない場合は [SDK Platforms] タブを選択します。SDK Manager ウィンドウの右下にある [Show package details] チェックボックスがオンになっていることを確認します。
- お使いのパソコンのアーキテクチャ(x86 / ARM)に対応する API 33 Android Automotive with Google APIs エミュレータのイメージをインストールします。
Android Automotive OS Android Virtual Device を作成する
- デバイス マネージャーを開き、ウィンドウの左側にある [Category] 列で [Automotive] を選択します。次に、バンドルされたハードウェア プロファイルの一覧から Automotive (1080p landscape) を選択し、[Next] をクリックします。
- 次のページに移動したら、前の手順で作成したシステム イメージを選択します。[Next] をクリックし、必要に応じて詳細オプションを選択したら、[Finish] をクリックして AVD を作成します。
サイド システムバーを構成する
構成可能なエミュレータを使用してテストするで説明したように、自動車のさまざまなシステム構成をエミュレートできるよう幅広いオプションが用意されています。
この Codelab では、com.android.systemui.rro.left
を使用してさまざまなシステムバー構成をテストします。これを有効にするには次のコマンドを実行します。
adb shell cmd overlay enable --user 0 com.android.systemui.rro.left
このアプリでは、Scaffold
の contentWindowInsets
として systemBars
修飾子を使用しているため、コンテンツはすでにシステムバーのセーフエリア内に描画されています。システムバーが画面の上下にのみ表示されると想定しているアプリがどう表示されるかを確認するには、パラメータを次のように変更します。
RoadReelsApp.kt
contentWindowInsets = if (route?.equals(Screen.Player.name) == true) WindowInsets(0.dp) else WindowInsets.systemBars.only(WindowInsetsSides.Vertical)
この場合、一覧画面と詳細画面がシステムバーの背後にレンダリングされてしまいます。ただし、ここまでの変更により、その後システムバーが制御できなくなってもプレーヤー画面は問題なく表示されます。
次のセクションに進む前に、windowContentPadding
パラメータに加えた変更を元に戻してください。
11. ディスプレイ カットアウトを使用する
最後に、モバイル デバイスの場合とは大きく異なる、一部の自動車のディスプレイ カットアウトにも対応できるようにします。モバイル デバイスの場合はノッチやピンホールカメラのためのカットアウトが一般的ですが、Android Automotive OS を搭載した自動車には、非矩形の画面(輪郭が曲線になっているもの)が採用されている場合もあります。
このようなディスプレイ カットアウトでのアプリの動作を確認するには、まず次のコマンドを実行してディスプレイ カットアウトを有効にします。
adb shell cmd overlay enable --user 0 com.android.internal.display.cutout.emulation.free_form
アプリの動作を詳細にテストするため、最後のセクションで使用した左側のシステムバーも有効にします(無効になっている場合のみ)。
adb shell cmd overlay enable --user 0 com.android.systemui.rro.left
この時点では、ディスプレイ カットアウトに合わせたレンダリングはされていません(カットアウトの正確な形状はまだ不明ですが、次のステップで明らかになります)。この動作でもまったく問題ありません。アプリのエクスペリエンスとしては、カットアウトに合わせてレンダリングされているのに正しく表示されていないほうが問題です。
ディスプレイ カットアウトに合わせてレンダリングする
ユーザーに最大限の没入感を提供するには、ディスプレイ カットアウトに合わせてレンダリングして画面領域をより有効に活用します。
- ディスプレイ カットアウトに合わせてレンダリングするには、
integers.xml
ファイルを作成して自動車固有のオーバーライドを確保します。そのためには、UI mode 修飾子の値として Car Dock を選択します(この名前は Android Auto で使用されていたものですが、Android Automotive OS でも引き続き使用します)。また、LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
は Android R で導入されたため、Android Version 修飾子を追加し、値として 30 を選択します。詳しくは、代替リソースを使用するをご覧ください。
- 作成したファイル(
res/values-car-v30/integers.xml
)に次のコードを追加します。
integers.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="windowLayoutInDisplayCutoutMode">3</integer>
</resources>
整数値 3
は LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
に対応します。res/values/integers.xml
のデフォルト値 0
(LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
に対応)をオーバーライドします。この整数値は、enableEdgeToEdge()
によって設定されたモードをオーバーライドするため、すでに MainActivity.kt
で参照されています。この属性について詳しくは、リファレンス ドキュメントをご覧ください。
アプリを実行すると、コンテンツがカットアウトまで広がって表示され没入感があります。しかし、上部のアプリバーとコンテンツの一部が、ディスプレイ カットアウトによって隠れてしまっています。システムバーが上下にのみ表示されることを想定しているアプリと同様の問題です。
上部のアプリバーを修正する
上部のアプリバーを修正するには、次の windowInsets
パラメータを CenterAlignedTopAppBar
コンポーザブルに追加します。
RoadReelsApp.kt
import androidx.compose.foundation.layout.safeDrawing
...
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)
safeDrawing
は、systemBars
で上部のアプリバーのみを配置するデフォルトの windowInsets
パラメータを改良したもので、displayCutout
と systemBars
の 2 つのインセットで構成されています。
なお、上部のアプリバーはウィンドウの上部に配置されるため、safeDrawing
インセットの下部のコンポーネントは含めないようにしてください。不要なパディングが追加される恐れがあります。
メイン画面を修正する
メイン画面と詳細画面のコンテンツを修正する方法の 1 つとして、Scaffold
の contentWindowInsets
に systemBars
ではなく safeDrawing
を使用する方法もあります。ただし、この方法だとディスプレイ カットアウトによってコンテンツが途切れてしまい、アプリの没入感は著しく低下します。これでは、アプリがディスプレイ カットアウトに合わせてレンダリングされていない場合と大差ありません。
画面内の各コンポーネントのインセットを処理する方法であれば、より没入感のあるユーザー インターフェースを実現できます。
Scaffold
のcontentWindowInsets
に変更を加え、PlayerScreen
の場合だけでなく常に 0dp になるようにします。これにより、各画面や画面内のコンポーネントのインセット関連の動作が決まります。
RoadReelsApp.kt
Scaffold(
...,
contentWindowInsets = WindowInsets(0.dp)
) { ... }
- 行ヘッダー
Text
コンポーザブルのwindowInsetsPadding
に変更を加え、safeDrawing
インセットの水平コンポーネントを使用するように設定します。これらのインセットの上部コンポーネントは、上部のアプリバーによって処理されます。下部コンポーネントについては後で処理します。
MainScreen.kt
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding
...
LazyColumn(
contentPadding = PaddingValues(bottom = dimensionResource(R.dimen.screen_edge_padding))
) {
items(NUM_ROWS) { rowIndex: Int ->
Text(
"Row $rowIndex",
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier
.padding(
horizontal = dimensionResource(R.dimen.screen_edge_padding),
vertical = dimensionResource(R.dimen.row_header_vertical_padding)
)
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal))
)
...
}
LazyRow
のcontentPadding
パラメータを削除します。次に、対応するsafeDrawing
コンポーネントの幅のSpacer
を各LazyRow
の先頭と末尾に追加して、すべてのサムネイルが完全に表示されるようにします。widthIn
修飾子を使用して、これらのスペーサーの幅がコンテンツのパディングの幅以上になるようにします。これらの要素がないと、行の最初または最後までスワイプしたときに、端のアイテムがシステムバーやディスプレイ カットアウトに隠れてしまうことがあります。
MainScreen.kt
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsEndWidth
import androidx.compose.foundation.layout.windowInsetsStartWidth
...
LazyRow(
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.list_item_spacing)),
) {
item {
Spacer(
Modifier
.windowInsetsStartWidth(WindowInsets.safeDrawing)
.widthIn(min = dimensionResource(R.dimen.screen_edge_padding))
)
}
items(NUM_ITEMS_PER_ROW) { ... }
item {
Spacer(
Modifier
.windowInsetsEndWidth(WindowInsets.safeDrawing)
.widthIn(min = dimensionResource(R.dimen.screen_edge_padding))
)
}
}
- 最後に、画面の下部にシステムバーやディスプレイ カットアウトのインセットがある場合に備え、
LazyColumn
の末尾にSpacer
を追加します。このようなスペーサーは、LazyColumn
の上部には必要ありません。この処理は上部のアプリバーが行います。アプリが上部ではなく下部のアプリバーを使用している場合は、windowInsetsTopHeight
修飾子を使用してリストの先頭にSpacer
を追加します。アプリが上部と下部の両方のアプリバーを使用している場合は、どちらのスペーサーも必要ありません。
MainScreen.kt
import androidx.compose.foundation.layout.windowInsetsBottomHeight
...
LazyColumn(...){
items(NUM_ROWS) { ... }
item {
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
}
}
これで、上部のアプリバーが完全に表示され、行の最後までスクロールしてもすべてのサムネイルが完全に表示されるようになりました。
詳細画面を修正する
詳細画面はそこまで深刻ではありませんが、やはりコンテンツが切れています。
詳細画面にはスクロール可能なコンテンツはないため、最上位の Box
に windowInsetsPadding
修飾子を追加するだけで修正できます。
DetailScreen.kt
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding
...
Box(
modifier = modifier
.padding(dimensionResource(R.dimen.screen_edge_padding))
.windowInsetsPadding(WindowInsets.safeDrawing)
) { ... }
プレーヤー画面を修正する
PlayerScreen
には、すでに Android Automotive OS の品質要件を満たす: 操作性で、システムバーの一部またはすべてのウィンドウ インセットにパディングを適用しています。しかし、アプリをディスプレイ カットアウトに合わせてレンダリングする場合、コンテンツが確実に隠れないようにするにはパディングだけでは不十分です。モバイル デバイスの場合は、ディスプレイ カットアウトがシステムバー内に収まらないことはほぼありません。一方、自動車の場合はディスプレイ カットアウトがシステムバーを大きくはみ出る可能性があり、上記の想定は適用できません。
これを修正するには、windowInsetsForPadding
変数の初期値を、次のようにゼロ値から displayCutout
に変更します。
PlayerScreen.kt
import androidx.compose.foundation.layout.displayCutout
...
var windowInsetsForPadding = WindowInsets(WindowInsets.displayCutout)
これで、使いやすさを維持しながら画面を最大限に活用できるようになりました。
アプリをモバイル デバイスで実行した場合も、没入感がさらに高まっているはずです。リストアイテムは、ナビゲーション バーの背後も含め、画面の端までレンダリングされます。
12. 完了
これで、初めての駐車時向けアプリを移行して最適化することができました。ここで学んだことを、ぜひ実際のアプリに応用してください。
試してみたいこと
- ディメンション リソースの一部をオーバーライドして、自動車で実行するときの要素のサイズを大きくする
- 構成可能なエミュレータのさまざまな構成を試す
- 利用可能な OEM エミュレータのイメージを使用してアプリをテストする
参考資料
- Android Automotive OS 用の駐車時向けアプリを作成する
- Android Automotive OS 用の動画アプリを作成する
- Android Automotive OS 用のゲームを作成する
- Android Automotive OS 用のブラウザを作成する
- 自動車向け Android アプリの品質に関するページでは、優れたユーザー エクスペリエンスを実現し、Google Play ストアの審査に合格するために、アプリが満たす必要がある基準について説明しています。必ずアプリのカテゴリでフィルタしてください。