自動車向けアプリ ライブラリの基礎を学ぶ

1. 始める前に

この Codelab では、自動車向け Android アプリ ライブラリを使用して、Android AutoAndroid Automotive OS 用の注意散漫防止の最適化済みアプリを作成する方法を学びます。まず、Android Auto のサポートを追加して、その後、Android Automotive OS 上で動作するアプリのバリエーションを、最小限の追加作業で作成します。両方のプラットフォームでアプリが動作したら、追加の画面や基本的なインタラクティビティを作成します。

対象外:

必要なもの

作成するアプリの概要

Android Auto

Android Automotive OS

デスクトップ ヘッドユニットを使用した、Android Auto 上で動作するアプリを表示する画面の録画。

Android Automotive OS エミュレータ上で動作するアプリを表示する画面の録画。

学習内容

  • 自動車向けアプリ ライブラリのクライアント - ホスト アーキテクチャの仕組み。
  • 独自の CarAppService クラス、Session クラス、Screen クラスの作成方法。
  • Android Auto と Android Automotive OS の両方で実装を共有する方法。
  • デスクトップ ヘッドユニットを使用して、開発マシン上で Android Auto を動作させる方法
  • Android Automotive OS エミュレータの実行方法

2. 設定する

コードを取得する

  1. この Codelab のコードは、car-codelabs GitHub リポジトリ内の car-app-library-fundamentals ディレクトリにあります。クローンを作成するには、次のコマンドを実行します。
git clone https://github.com/android/car-codelabs.git
  1. または、リポジトリを ZIP ファイルとしてダウンロードすることもできます。

プロジェクトを開く

  • Android Studio を起動し、car-app-library-fundamentals/start ディレクトリのみを選択してプロジェクトをインポートします。car-app-library-fundamentals/end ディレクトリにはソリューション コードが含まれています。不明な点がある場合や、プロジェクト全体を確認したいときは、いつでも参照できます。

コードを理解する

  • Android Studio でプロジェクトを開き、初期状態のコードを確認します。

アプリのスターター コードが、:app:common:data の 2 つのモジュールに分かれていることに注目してください。

:app モジュールは :common:data モジュールに依存しています。

:app モジュールには、モバイルアプリの UI とロジックが含まれています。:common:data モジュールには、Place モデルの読み取りに使用される、Place モデルのデータクラスリポジトリが含まれています。単純化のために、リポジトリはハードコードされたリストから読み取りますが、実際のアプリでは、データベースまたはバックエンド サーバーから簡単に読み取ることができます。

:app モジュールには、:common:data モジュールとの依存関係があるため、Place モデルの一覧の読み取りや表示ができます。

3. 自動車向け Android アプリ ライブラリについて学ぶ

自動車向け Android アプリ ライブラリは、Jetpack ライブラリのセットです。これにより、開発者が自動車内で使用するアプリを構築できるようになります。このライブラリは、テンプレート化されたフレームワークによって、ドライバー向けに最適化されたユーザー インターフェースを提供します。また、車内で使われるさまざまなハードウェア構成(入力方法、画面サイズ、アスペクト比など)への適合にも対処します。これを利用することで、開発者は簡単にアプリを作成できるようになります。作成されたアプリは、さまざまな車両で、Android Auto と Android Automotive OS のどちらが動作していても、問題なく動作します。

仕組みを学ぶ

自動車向けアプリ ライブラリを使って作成されたアプリは、Android Auto や Android Automotive OS 上で直接動作させることはできません。代わりにホストアプリが、クライアント アプリと通信し、クライアントのユーザー インターフェースを表示します。Android Auto はそれ自体がホストです。Google Automotive App Host は、Google 搭載の Android Automotive OS 車両で使用されるホストです。自動車向けアプリ ライブラリの主なクラスのうち、アプリを作成する際に拡張する必要があるものは次のとおりです。

CarAppService

CarAppService は、Android の Service クラスのサブクラスで、ホストアプリがクライアント アプリ(この Codelab で作成するアプリなど)と通信するためのエントリー ポイントとして動作します。ホストアプリがやり取りする Session インスタンスを作成するのが主な目的です。

Session

Session は、車両の画面上で動作する、クライアント アプリのインスタンスと考えることができます。他の Android コンポーネントと同じように、独自のライフサイクルがあり、Session インスタンスが存在している間に使用されるリソースの初期化や破棄ができます。CarAppServiceSession の間には、1 対多の関係があります。たとえば、1 つの CarAppService が 2 つの Session インスタンスをもつことがあります。1 つはプライマリ ディスプレイ用で、もう 1 つはクラスタ ディスプレイ用です。クラスタ ディスプレイはナビゲーション アプリ用で、クラスタ画面をサポートします。

Screen

Screen インスタンスは、ホストアプリが表示するユーザー インターフェースの生成を担います。ユーザー インターフェースは、グリッドリストのような特定のレイアウト タイプをモデル化した Template クラスで表されます。それぞれの Session は、アプリのさまざまな部分でユーザーフローを処理する Screen インスタンスのスタックを管理します。Session と同様に、Screen にも利用可能な独自のライフサイクルがあります。

自動車向けアプリ ライブラリの動作に関する説明図です。左側には Display と書かれたボックスが 2 つあります。中央には、Host と書かれたボックスが 1 つあります。右側には、CarAppService と書かれたボックスが 1 つあります。CarAppService ボックスの中にボックスが 2 つあり、それぞれに Session と書かれています。最初の Session ボックスの中に、Screen ボックスが重なった状態で 3 つあります。2 つめの Session ボックスの中に、Screen ボックスが重なった状態で 2 つあります。それぞれの Display と Host の間には矢印があります。同様に、Host とそれぞれの Session の間にも矢印があります。矢印は、ホストが異なるコンポーネントの間の通信をどのように管理するかを示しています。

CarAppServiceSessionScreen は、この Codelab の CarAppService を作成するのセクションで作成しますので、今はあまり理解できなくても心配要りません。

4. 初期設定を行う

ます、CarAppService を含むモジュールを設定して、依存関係を宣言します。

car-app-service モジュールを作成する

  1. プロジェクト ウィンドウで :common モジュールを選択し、右クリックして [New] > [Module] オプションを選択します。
  2. モジュール ウィザードが開かれるので、左側の一覧で [Android Library] テンプレートを選択します(これによって、このモジュールが依存関係として使えるようになります)。その後、次の値を設定します。
  • Module name: :common:car-app-service
  • Package name: com.example.places.carappservice
  • Minimum SDK: API 23: Android 6.0 (Marshmallow)

このステップに記載された値が設定済みの Create New Module ウィザード。

依存関係を設定する

  1. プロジェクト レベルの build.gradle ファイルに、自動車向けアプリ ライブラリのバージョンを表す、次の変数宣言を追加します。これにより、アプリ内のそれぞれのモジュールで、簡単に同じバージョンを使うことができます。

build.gradle(Project: Places)

buildscript {
    ext {
        // All versions can be found at https://developer.android.com/jetpack/androidx/releases/car-app
        car_app_library_version = '1.3.0-rc01'
        ...
    }
}
  1. 次に、:common:car-app-service モジュールの build.gradle ファイルに、2 つの依存関係を追加します。
  • androidx.car.app:app。これは、自動車向けアプリ ライブラリのプライマリ アーティファクトで、アプリのビルド時に使用される、すべてのコアクラスが含まれています。ライブラリを構成するアーティファクトには、ほかに 3 つあります。androidx.car.app:app-projected は Android Auto 特有の機能用、androidx.car.app:app-automotive は、Android Automotive OS 機能のコード用、androidx.car.app:app-testing は単体テストに役立つヘルパー用です。app-projectedapp-automotive を、この Codelab で後ほど使います。
  • :common:data。これは、既存のモバイルアプリで使われているのと同じデータ モジュールで、アプリ エクスペリエンスのすべてのバージョンで、同じデータソースが使えるようになります。

build.gradle(:common:car-app-service モジュール)

dependencies {
    ...
    implementation "androidx.car.app:app:$car_app_library_version"
    implementation project(":common:data")
    ...
}

この変更によって、アプリに含まれるモジュールの依存関係グラフは次のようになります。

:app モジュールと :common:car-app-service モジュールが、どちらも :common:data モジュールに依存しています。

これで依存関係の設定は完了です。次は CarAppService の作成に進みましょう。

5. CarAppService を作成する

  1. まず、:common:car-app-service モジュール内の carappservice パッケージに、PlacesCarAppService.kt という名前のファイルを作成してください。
  2. このファイルの中に、PlacesCarAppService という名前のクラス(CarAppService の拡張)を作成してください。

PlacesCarAppService.kt

class PlacesCarAppService : CarAppService() {

    override fun createHostValidator(): HostValidator {
        return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR
    }

    override fun onCreateSession(): Session {
        // PlacesSession will be an unresolved reference until the next step
        return PlacesSession()
    }
}

抽象クラスの CarAppService に、自分用の onBindonUnbind のような Service メソッドを実装します。これにより、メソッドがオーバーライドされるのを防ぎ、ホストアプリとの適切な相互運用性を確保します。やるべきことは、createHostValidatoronCreateSession の実装だけです。

createHostValidator の戻り値である HostValidator は、CarAppService がバインドされるときに、信頼できるホストであることを確認するために参照されます。ホストが設定されたパラメータに一致しない場合は、バインドされません。この Codelab(および一般的なテスト)では、ALLOW_ALL_HOSTS_VALIDATOR を使ってアプリの接続を簡単にしますが、製品版では使わないでください。製品版アプリでの設定方法の詳細については、createHostValidator に関するドキュメントをご覧ください。

これくらいシンプルなアプリであれば、onCreateSession は単に Session のインスタンスを返すだけでも良いです。より複雑なアプリの場合は、車両でアプリが動作している間使用される統計情報やロギング クライアントのような、有効期間が長いリソースの初期化をここで行うのも良いでしょう。

  1. 最後に、:common:car-app-service モジュールの AndroidManifest.xml ファイルに、PlacesCarAppService に対応する <service> エレメントを追加する必要があります。これにより、オペレーティング システム(およびホストのような他のアプリ)に存在を通知できるようになります。

AndroidManifest.xml(:common:car-app-service)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!--
        This AndroidManifest.xml will contain all of the elements that should be shared across the
        Android Auto and Automotive OS versions of the app, such as the CarAppService <service> element
    -->

    <application>
        <service
            android:name="com.example.places.carappservice.PlacesCarAppService"
            android:exported="true">
            <intent-filter>
                <action android:name="androidx.car.app.CarAppService" />
                <category android:name="androidx.car.app.category.POI" />
            </intent-filter>
        </service>
    </application>
</manifest>

ここでは次の 2 つに注目します。

  • <action> 要素によって、ホスト(およびランチャー)アプリが、このアプリを見つけられるようになります。
  • <category> 要素では、アプリのカテゴリを宣言します。これにより、満たすべきアプリの品質基準が決まります(詳細は後ほど説明します)。他に、androidx.car.app.category.NAVIGATIONandroidx.car.app.category.IOT の値になることがあります。

PlacesSession クラスを作成する

  • 新しく PlacesCarAppService.kt ファイルを作成し、次のコードを追加します。

PlacesCarAppService.kt

class PlacesSession : Session() {
    override fun onCreateScreen(intent: Intent): Screen {
        // MainScreen will be an unresolved reference until the next step
        return MainScreen(carContext)
    }
}

このようなシンプルなアプリでは、onCreateScreen でメイン画面を返すだけにもできます。しかし、このメソッドはパラメータとして Intent を持つため、機能がより豊富なアプリであればそのパラメータを読み取るとることで、画面のバックスタックの追加や条件付きロジックの使用が可能になります。

MainScreen クラスを作成する

次に、screen. という名前の新しいパッケージを作成します。

  1. com.example.places.carappservice パッケージを右クリックして、[New] > [Package] を選択します(パッケージのフルネームは com.example.places.carappservice.screen になります)。ここに、アプリのすべての Screen サブクラスを作成します。
  2. screen パッケージに、MainScreen.kt という名前のファイルを作成します。ここに、Screen の拡張である MainScreen クラスが含まれます。これで、PaneTemplate を使って、シンプルな Hello, world! というメッセージが表示できました。

MainScreen.kt

class MainScreen(carContext: CarContext) : Screen(carContext) {
    override fun onGetTemplate(): Template {
        val row = Row.Builder()
            .setTitle("Hello, world!")
            .build()
        
        val pane = Pane.Builder()
            .addRow(row)
            .build()

        return PaneTemplate.Builder(pane)
            .setHeaderAction(Action.APP_ICON)
            .build()
    }
}

6. Android Auto のサポートを追加する

これで、アプリが起動して動作するのに必要なロジックを、すべて実装しました。しかし、アプリを Android Auto で動作させるには、あと 2 つ設定を行う必要があります。

car-app-service モジュールに依存関係を追加する

:app モジュールの build.gradle ファイルに、次のような追加を行います。

build.gradle(:app モジュール)

dependencies {
    ...
    implementation project(path: ':common:car-app-service')
    ...
}

この変更によって、アプリに含まれるモジュールの依存関係グラフは次のようになります。

:app モジュールと :common:car-app-service モジュールが、どちらも :common:data モジュールに依存しています。:app モジュールは :common:car-app-service モジュールにも依存しています。

これにより、:common:car-app-service モジュールに作成したコードが、所定の権限付与アクティビティのような、自動車向けアプリ ライブラリに含まれる他のコンポーネントとバンドルされます。

com.google.android.gms.car.application メタデータを宣言する

  1. :common:car-app-service モジュールを右クリックして、[New] > [Android Resource File] オプションを選択し、次の値を上書きします。
  • File name: automotive_app_desc.xml
  • Resource type: XML
  • Root element: automotiveApp

このステップに記載された値が設定済みの New Resource File ウィザード。

  1. このファイルに次のような <uses> 要素を追加し、アプリが、自動車向けアプリ ライブラリで提供されたテンプレートを使用することを宣言します。

automotive_app_desc.xml

<?xml version="1.0" encoding="utf-8"?>
<automotiveApp>
    <uses name="template"/>
</automotiveApp>
  1. :app モジュールの AndroidManifest.xml ファイルに、作成した automotive_app_desc.xml ファイルを参照するための <meta-data> エレメントを追加します。

AndroidManifest.xml(:app)

<application ...>

    <meta-data
        android:name="com.google.android.gms.car.application"
        android:resource="@xml/automotive_app_desc" />

    ...

</application>

Android Auto がこのファイルを読み取って、アプリがどのような機能を持っているか判断します。今回の場合は、アプリが自動車向けアプリ ライブラリのテンプレート システムを使用することがわかります。この情報はその後、アプリを Android Auto ランチャーに登録する、通知からアプリを開くなどの動作を処理するために使用されます。

省略可: 投影の変更をリッスンする

ユーザーのデバイスが自動車に接続されているかどうかを知りたいことがあると思います。接続状態をモニタリングできる LiveData が含まれている CarConnection API を使用することで、これを実現できます。

  1. CarConnection API を使うには、まず、androidx.car.app:app アーティファクトの :app モジュールに依存関係を追加します。

build.gradle(:app モジュール)

dependencies {
    ...
    implementation "androidx.car.app:app:$car_app_library_version"
    ...
}
  1. デモ用に、現在の接続状況を表示する、次のようなシンプルなコンポーザブルを作成できます。実際のアプリでは、ロギングでこの状態をキャプチャし、投影中にスマートフォンの画面で機能を無効にするなどに使用します。

MainActivity.kt

@Composable
fun ProjectionState(carConnectionType: Int, modifier: Modifier = Modifier) {
    val text = when (carConnectionType) {
        CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> "Not projecting"
        CarConnection.CONNECTION_TYPE_NATIVE -> "Running on Android Automotive OS"
        CarConnection.CONNECTION_TYPE_PROJECTION -> "Projecting"
        else -> "Unknown connection type"
    }

    Text(
        text = text,
        style = MaterialTheme.typography.bodyMedium,
        modifier = modifier
    )
}
  1. これで、次のスニペットで示されているように、データの表示方法、読み取り方法、コンポーザブルに渡す方法がわかりました。

MainActivity.kt

setContent {
    val carConnectionType by CarConnection(this).type.observeAsState(initial = -1)
    PlacesTheme {
        // A surface container using the 'background' color from the theme
        Surface(
            modifier = Modifier.fillMaxSize(),
            color = MaterialTheme.colorScheme.background
        ) {
            Column {
                Text(
                    text = "Places",
                    style = MaterialTheme.typography.displayLarge,
                    modifier = Modifier.padding(8.dp)
                )
                ProjectionState(
                    carConnectionType = carConnectionType,
                    modifier = Modifier.padding(8.dp)
                )
                PlaceList(places = PlacesRepository().getPlaces())
            }
        }
    }
}
  1. アプリを実行すると、Not projecting と表示されるはずです。

投影状態を示す 1 行のテキストが画面に追加されていて、そこには「Not projecting」と表示されています。

7. デスクトップ ヘッドユニット(DHU)でテストする

CarAppService が実装され、Android Auto 設定も準備できたので、アプリを実行してどのように動作するか見てみましょう。

  1. スマートフォンにアプリをインストールし、指示に従って DHU のインストールと実行を行います

DHU が起動して動作すると、ランチャーにアプリのアイコンが表示されます(表示されない場合、前のセクションですべてのステップを正しく実施したか再確認し、端末で DHU を終了してから再起動してください)。

  1. ランチャーからアプリを開く

Android Auto ランチャーにアプリグリッドが表示されていて、そこに Places アプリが含まれています。

アプリがクラッシュしました。

「Android Auto で予期しないエラーが発生しました」というメッセージのエラーが画面に表示されています。画面の右上の角に、デバッグ切り替えスイッチがあります。

  1. アプリがクラッシュした理由を調べるには、右上の角にあるデバッグ アイコン(DHU で動作しているときのみ表示されます)を切り替えるか、Android Studio の Logcat を確認します。

前の図と同じエラー画面ですが、今度はデバッグが有効に切り替わっています。画面にスタック トレースが表示されています。

Error: [type: null, cause: null, debug msg: java.lang.IllegalArgumentException: Min API level not declared in manifest (androidx.car.app.minCarApiLevel)
        at androidx.car.app.AppInfo.retrieveMinCarAppApiLevel(AppInfo.java:143)
        at androidx.car.app.AppInfo.create(AppInfo.java:91)
        at androidx.car.app.CarAppService.getAppInfo(CarAppService.java:380)
        at androidx.car.app.CarAppBinder.getAppInfo(CarAppBinder.java:255)
        at androidx.car.app.ICarApp$Stub.onTransact(ICarApp.java:182)
        at android.os.Binder.execTransactInternal(Binder.java:1285)
        at android.os.Binder.execTransact(Binder.java:1244)
]

ログを見ると、アプリがサポートする最小 API レベルの宣言がマニフェストにないことがわかります。追加する前に、なぜ必要なのか考えてみましょう。

Android 自体と同じように、自動車向けアプリ ライブラリにも API レベルのコンセプトがあります。ホストとクライアントのアプリが通信するためには、コントラクトが必要だからです。ホストアプリは指定された API レベルと関連する機能(およびより低いレベルに対する下位互換性)をサポートします。たとえば、SignInTemplate は API レベル 2 以上で動作するホストで使用できます。しかし、API レベル 1 しかサポートしていないホストで使おうとすると、ホストはそのテンプレート タイプを知らないため、意味のあることは何もできません。

ホストをクライアントにバインドする処理では、サポートしている API レベルに共通部分がないと、正しくバインドできません。たとえば、ホストが API レベル 1 だけをサポートし、クライアント アプリは API レベル 2 の機能がないと動作しない場合(このマニフェスト宣言で示されているように)、このクライアント アプリはホスト上で正しく実行することはできないため、接続されるべきではありません。このため、必要な最小 API レベルは、クライアントがマニフェストで宣言しなければなりません。これにより、確実にそのレベルをサポートするホストだけをバインドできます。

  1. サポートする最小 API レベルを設定するには、:common:car-app-service モジュールの AndroidManfiest.xml ファイルに、次のような <meta-data> エレメントを追加します。

AndroidManifest.xml(:common:car-app-service)

<application>
    <meta-data
        android:name="androidx.car.app.minCarApiLevel"
        android:value="1" />
    <service android:name="com.example.places.carappservice.PlacesCarAppService" ...>
        ...
    </service>
</application>
  1. アプリを再度インストールして、DHU で立ち上げます。今度は次のように表示されるはずです。

アプリが基本的な「Hello, world」画面を表示しています。

完全に理解するために、minCarApiLevel を大きな値(たとえば 100)に設定して試してみるのも良いでしょう。ホストとクライアントに互換性がない場合にアプリを起動すると何が起こるか確認できます(ヒント: 値を設定していないときと同じようにクラッシュします)。

Android 自体と同じようにホストが要求されたレベルをサポートしているかについて実行時に確認する場合、宣言された最小レベルよりも大きいレベルの API に含まれる機能も利用できます。

省略可: 投影の変更をリッスンする

  • 前のステップで CarConnection リスナーを追加してあれば、DHU が動作しているとき、スマートフォン上で次のように状態が更新されます。

スマートフォンが DHU に接続されたため、投影状態を表すテキストが「Projecting」になりました。

8. Android Automotive OS のサポートを追加する

Android Auto を起動して動作させることができました。次に、Android Automotive OS も同じようにサポートしましょう。

:automotive モジュールを作成する

  1. アプリの Android Automotive OS 対応に必要なコードを含むモジュールを作成します。Android Studio で [File] > [New] > [New Module...] を開き、左側のテンプレート タイプ一覧から [Automotive] オプションを選択してください。その後、次の値を設定します。
  • Application/Library name: Places(メインアプリと同じにしてありますが、別の名前にすることもできます)
  • Module name: automotive
  • Package name: com.example.places.automotive
  • Language: Kotlin
  • Minimum SDK: API 29: Android 10.0 (Q) - :common:car-app-service モジュール作成時に説明したとおり、自動車向けアプリ ライブラリのアプリをサポートするすべての Android Automotive OS 車両は、API 29 以上で動作します。

Android Automotive OS モジュール用の Create New Module ウィザードに、このステップで記載された値が設定されています。

  1. [Next] をクリックします。次の画面で [No Activity] を選択し、最後に [Finish] をクリックします。

Create New Module ウィザードの 2 つめのページ。「No Activity」、「Media Service」、「Messaging Service」の 3 つのオプションが表示されています。「No Activity」オプションが選択されています。

依存関係を追加する

Android Auto のときと同じように、:common:car-app-service モジュールで依存関係を宣言する必要があります。これによって、実装を両方のプラットフォームで共有できるようになります。

さらに、androidx.car.app:app-automotive アーティファクトにも依存関係を追加します。androidx.car.app:app-projected アーティファクトは、Android Auto ではオプションでしたが、Android Automotive OS では、アプリの実行に CarAppActivity を使用するため必須です。

  1. 依存関係を追加するには、build.gradle ファイルを開き、次のコードを挿入します。

build.gradle(:automotive モジュール)

dependencies {
    ...
    implementation project(':common:car-app-service')
    implementation "androidx.car.app:app-automotive:$car_app_library_version"
    ...
}

この変更によって、アプリに含まれるモジュールの依存関係グラフは次のようになります。

:app モジュールと :common:car-app-service モジュールが、どちらも :common:data モジュールに依存しています。:app モジュールと :automotive モジュールが、:common:car-app-service module に依存しています。

マニフェストをセットアップする

  1. まず、android.hardware.type.automotiveandroid.software.car.templates_host の 2 つの機能が必須(required)であることを宣言します。

android.hardware.type.automotive はシステム機能で、デバイス自体が車両であることを示します(詳細は FEATURE_AUTOMOTIVE を参照)。この機能を必須としたアプリだけが、Google Play Console の Automotive OS トラックに送信できます(他のトラックに送信されるアプリは、この機能を必須にすることはできません)。android.software.car.templates_host は、テンプレート アプリの実行に必要なテンプレート ホストを持つ車両にだけあるシステム機能です。

AndroidManifest.xml(:automotive)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-feature
        android:name="android.hardware.type.automotive"
        android:required="true" />
    <uses-feature
        android:name="android.software.car.templates_host"
        android:required="true" />
    ...
</manifest>
  1. 次に、必須ではない機能をいくつか宣言する必要があります。

これは、アプリを Google 搭載車両の幅広いハードウェアに適合させるためです。たとえば、アプリが android.hardware.screen.portrait 機能を必須とする場合、ほとんどの車両で画面の向きは固定なので、横表示画面の車両には適合しません。そのため、この機能の android:required 属性を false に設定します。

AndroidManifest.xml(:automotive)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    ...
    <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" />
    <uses-feature
        android:name="android.hardware.camera"
        android:required="false" />
    ...
</manifest>
  1. 次に、Android Auto のときと同じように、automotive_app_desc.xml ファイルへの参照を追加する必要があります。

今回は、android:name 属性が以前と異なることに注意してください。com.google.android.gms.car.application ではなく com.android.automotive になります。以前と同じように、これで :common:car-app-service モジュールの automotive_app_desc.xml ファイルを参照するようになります。つまり、同じリソースが Android Auto と Android Automotive OS の両方で使われます。<application> 要素内の <meta-data> 要素に注意してください(application タグが自己完結しているのを変更する必要があります)。

AndroidManifest.xml(:automotive)

<application>
    ...
    <meta-data android:name="com.android.automotive"
        android:resource="@xml/automotive_app_desc"/>
    ...
</application>
  1. 最後に、ライブラリに含まれている CarAppActivity 用の <activity> 要素を追加する必要があります。

AndroidManifest.xml(:automotive)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    ...
    <application ...>
        ...
        <activity
            android:name="androidx.car.app.activity.CarAppActivity"
            android:exported="true"
            android:launchMode="singleTask"
            android:theme="@android:style/Theme.DeviceDefault.NoActionBar">

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <meta-data
                android:name="distractionOptimized"
                android:value="true" />
        </activity>
    </application>
</manifest>

これで、次のことが行われます。

  • app-automotive パッケージに含まれる CarAppActivity クラスの完全修飾されたクラス名が、android:name に一覧表示されます。
  • android:exportedtrue に設定されます。これは、この Activity が、それ自体(ランチャー)以外のアプリから起動できる必要があるためです。
  • android:launchModesingleTask に設定されます。これにより、同時に存在できる CarAppActivity は 1 つだけになります。
  • android:theme@android:style/Theme.DeviceDefault.NoActionBar に設定されます。これにより、アプリは使用可能な全画面表示スペースをすべて使うことができます。
  • インテント フィルタは、これがアプリのランチャー Activity であることを示します。
  • 車両の走行中など、UX 制限中にアプリを使用できることを OS に示す <meta-data> 要素があります。

省略可: :app モジュールからランチャー アイコンをコピーする

:automotive モジュールを作ったばかりなので、アイコンはデフォルトの緑の Android ロゴになっています。

  • ロゴを変更したい場合、:app モジュールから :automotive モジュールに、直接 mipmap リソースをコピーして貼り付けることで、モバイルアプリと同じランチャー アイコンを使うことができます。

9. Android Automotive OS エミュレータでテストする

Automotive with Play Store のシステム イメージをインストールする

  1. まず、Android Studio で SDK Manager を開き、まだ選択していない場合は [SDK Platforms] タブを選択します。SDK Manager ウィンドウの右下にある [Show package details] チェックボックスがオンになっていることを確認します。
  2. 次のエミュレータ イメージのうち 1 つ以上をインストールします。イメージは、同じアーキテクチャ(x86 / ARM)のマシンでのみ実行できます。
  • Android 12L > Automotive with Play Store Intel x86 Atom_64 System Image
  • Android 12L > Automotive with Play Store ARM 64 v8a System Image
  • Android 11 > Automotive with Play Store Intel x86 Atom_64 System Image
  • Android 10 > Automotive with Play Store Intel x86 Atom_64 System Image

Android Automotive OS の Android Virtual Device を作成する

  1. デバイス マネージャーを開き、ウィンドウの左側にある [Category] 列で [Automotive] を選択します。次に、デバイス定義の一覧から [Automotive (1024p landscape)] を選択し、[Next] をクリックします。

選択したハードウェア プロファイル「Automotive (1024p landscape)」が表示されている Virtual Device Configuration ウィザード。

  1. 次のページに移動したら、前のステップでインストールしたシステム イメージを選択します(Android 11/API 30 のイメージを選択する場合、デフォルトの Recommended タブではなく、x86 Images タブにあるかもしれません)。[Next] をクリックし、必要に応じて詳細オプションを選択したら、[Finish] をクリックして AVD を作成します。

アプリを実行する

  1. 前のステップで作成したエミュレータで、automotive 実行構成を使用してアプリを実行します。

実行

最初にアプリを実行すると、次のような画面が表示されるかもしれません。

アプリの画面に「System update required」というメッセージと、その下に「Check for updates」というボタンが表示されています。

その場合は、[Check for updates] ボタンをクリックすると、Google Play ストアのページに移動します。そこで、[Install] ボタンをクリックし、Google Automotive App Host アプリをインストールします。[Check for updates] ボタンをクリックしたときにログインしていなかった場合、ログインフローに移行します。ログイン後、アプリを再度開いてボタンをクリックすると、Google Play ストアのページに戻ります。

Google Automotive App Host の Google Play ストアのページ - 右上に「Install」ボタンがあります。

  1. 最後に、インストールされたホストで、ランチャー(下部にある 9 つのドットのグリッド アイコン)から再度アプリを開くと、次のように表示されます。

アプリが基本的な「Hello, world」画面を表示しています。

次のステップでは、:common:car-app-service モジュールに変更を加えて、場所の一覧を表示したり、ユーザーが他のアプリで選択した場所へのナビゲーションを開始したりできるようにします。

10. 地図と詳細画面を追加する

メイン画面に地図を追加する

  1. まず、MainScreen クラスに含まれている onGetTemplate メソッドのコードを次のものに置き換えます。

MainScreen.kt

override fun onGetTemplate(): Template {
    val placesRepository = PlacesRepository()
    val itemListBuilder = ItemList.Builder()
        .setNoItemsMessage("No places to show")

    placesRepository.getPlaces()
        .forEach {
            itemListBuilder.addItem(
                Row.Builder()
                    .setTitle(it.name)
                    // Each item in the list *must* have a DistanceSpan applied to either the title
                    // or one of the its lines of text (to help drivers make decisions)
                    .addText(SpannableString(" ").apply {
                        setSpan(
                            DistanceSpan.create(
                                Distance.create(Math.random() * 100, Distance.UNIT_KILOMETERS)
                            ), 0, 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE
                        )
                    })
                    .setOnClickListener { TODO() }
                    // Setting Metadata is optional, but is required to automatically show the
                    // item's location on the provided map
                    .setMetadata(
                        Metadata.Builder()
                            .setPlace(Place.Builder(CarLocation.create(it.latitude, it.longitude))
                                // Using the default PlaceMarker indicates that the host should
                                // decide how to style the pins it shows on the map/in the list
                                .setMarker(PlaceMarker.Builder().build())
                                .build())
                            .build()
                    ).build()
            )
        }

    return PlaceListMapTemplate.Builder()
        .setTitle("Places")
        .setItemList(itemListBuilder.build())
        .build()
}

このコードは、PlacesRepository から Place インスタンスの一覧を読み取ります。また、それぞれのインスタンスを Row に変換して ItemList に追加し、PlaceListMapTemplate で表示できるようにします。

  1. 再度アプリを(どちらか一方、または両方のプラットフォームで)実行して結果を見てみましょう。

Android Auto

Android Automotive OS

エラーのため他のスタック トレースが表示されています。

アプリを開いても、クラッシュしてランチャーに戻ってしまいます。

別のエラーが発生しました – 権限が足りないようです。

java.lang.SecurityException: The car app does not have a required permission: androidx.car.app.MAP_TEMPLATES
        at android.os.Parcel.createExceptionOrNull(Parcel.java:2373)
        at android.os.Parcel.createException(Parcel.java:2357)
        at android.os.Parcel.readException(Parcel.java:2340)
        at android.os.Parcel.readException(Parcel.java:2282)
        ...
  1. エラーを修正するために、:common:car-app-service モジュールのマニフェストに、次のような <uses-permission> 要素を追加します。

この権限は、PlaceListMapTemplate を使用するすべてのアプリで宣言する必要があります。宣言がない場合、先ほどのようにアプリがクラッシュします。カテゴリの宣言androidx.car.app.category.POI を指定したアプリのみが、このテンプレートを使用でき、この権限も有効になることに注意してください。

AndroidManifest.xml(:common:car-app-service)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="androidx.car.app.MAP_TEMPLATES" />
    ...
</manifest>

権限を付与してからアプリを実行すると、それぞれのプラットフォームで次のように表示されます。

Android Auto

Android Automotive OS

画面の左側に場所一覧が表示され、その背後に、画面の残りのスペースを埋めるように地図が表示されています。地図には、場所に対応するピンが表示されています。

画面の左側に場所一覧が表示され、その背後に、画面の残りのスペースを埋めるように地図が表示されています。地図には、場所に対応するピンが表示されています。

必要な Metadata を提供すると、アプリのホストが地図の描画を行います。

詳細画面を追加する

次に、詳細画面を追加します。これによりユーザーは、特定の場所の詳細情報を見られるようになり、その場所へ好みのナビゲーション アプリでナビを開始するか、他の場所一覧に戻るかを選択できるようになります。これは、オプション操作ボタンの横に情報を 4 行まで表示できる PaneTemplate を使用することで実現できます。

  1. まず、:common:car-app-service モジュールの res ディレクトリを右クリックした後、[New] > [Vector Asset] をクリックし、次の設定を使ってナビゲーション アイコンを作成します。
  • Asset type: Clip art
  • Clip art: navigation
  • Name: baseline_navigation_24
  • Size: 24dp by 24dp
  • Color: #000000
  • Opacity: 100%

Asset Studio ウィザードにこのステップに記載された入力値が表示されています。

  1. 続いて、screen パッケージで、DetailScreen.kt という名前のファイルを(既存の MainScreen.kt ファイルの横に)作成し、次のコードを追加します。

DetailScreen.kt

class DetailScreen(carContext: CarContext, private val placeId: Int) : Screen(carContext) {

    override fun onGetTemplate(): Template {
        val place = PlacesRepository().getPlace(placeId)
            ?: return MessageTemplate.Builder("Place not found")
                .setHeaderAction(Action.BACK)
                .build()

        val navigateAction = Action.Builder()
            .setTitle("Navigate")
            .setIcon(
                CarIcon.Builder(
                    IconCompat.createWithResource(
                        carContext,
                        R.drawable.baseline_navigation_24
                    )
                ).build()
            )
            // Only certain intent actions are supported by `startCarApp`. Check its documentation
            // for all of the details. To open another app that can handle navigating to a location
            // you must use the CarContext.ACTION_NAVIGATE action and not Intent.ACTION_VIEW like
            // you might on a phone.
            .setOnClickListener {  carContext.startCarApp(place.toIntent(CarContext.ACTION_NAVIGATE)) }
            .build()

        return PaneTemplate.Builder(
            Pane.Builder()
                .addAction(navigateAction)
                .addRow(
                    Row.Builder()
                        .setTitle("Coordinates")
                        .addText("${place.latitude}, ${place.longitude}")
                        .build()
                ).addRow(
                    Row.Builder()
                        .setTitle("Description")
                        .addText(place.description)
                        .build()
                ).build()
        )
            .setTitle(place.name)
            .setHeaderAction(Action.BACK)
            .build()
    }
}

navigateAction の作成方法には特に注意が必要です。OnClickListener での startCarApp の呼び出しは、Android Auto や Android Automotive OS 上の他のアプリとの通信の鍵となります。

2 種類の画面が表示されましたので、その間の移動を追加しましょう。自動車向けアプリ ライブラリを使用したナビゲーションでは、プッシュとポップのスタックモデルを使用します。これは、運転中の作業に向いているシンプルなタスクフローに最適です。

自動車向けアプリ ライブラリを使ったアプリ内移動の、動作方法を表す図。左側に、MainScreen だけが含まれるスタックがあります。左側の図と中央の図の間に矢印があり、「Push DetailScreen」というラベルが付けられています。中央のスタックには、既存の MainScreen の上に DetailScreen が含まれています。中央のスタックと右側のスタックの間に矢印があり、「Pop」というラベルが付けられています。右側のスタックは左側のスタックと同じで、MainScreen だけが含まれています。

  1. MainScreen 上のリストアイテムの 1 つから、そのアイテムの DetailScreen に移動するには、次のコードを追加します。

MainScreen.kt

Row.Builder()
    ...
    .setOnClickListener { screenManager.push(DetailScreen(carContext, it.id)) }
    ...

DetailScreen から MainScreen へ戻る動作はすでに処理されています。これは、DetailScreen に表示される PaneTemplate が作成されるときに、setHeaderAction(Action.BACK) が呼び出されるためです。ユーザーがヘッダー アクションをクリックすると、ホストがスタックから現在の画面をポップして取り除きます。ただし、この動作は必要に応じてアプリでオーバーライドできます。

  1. アプリを実行して DetailScreen とアプリ内移動が動作するのを確認しましょう。

11. 画面上のコンテンツを更新する

ユーザーの画面操作に応じて、画面上の要素の状態を変更したいことが、よくあると思います。この実現方法を示すために、DetailScreen に表示された場所を、お気に入りに追加したり、お気に入りから削除したりする機能を作成します。

  1. まず、状態を保持するローカル変数 isFavorite を追加します。実際のアプリではデータレイヤーの一部として保存されるべきですが、デモ目的であれば、ローカル変数で十分です。

DetailScreen.kt

class DetailScreen(carContext: CarContext, private val placeId: Int) : Screen(carContext) {
    private var isFavorite = false
    ...
}
  1. 次に、:common:car-app-service モジュールの res ディレクトリを右クリックした後、[New] > [Vector Asset] をクリックし、次の設定を使用してお気に入りアイコンを作成します。
  • Asset type: Clip art
  • Name: baseline_favorite_24
  • Clip art: favorite
  • Size: 24dp by 24dp
  • Color: #000000
  • Opacity: 100%

Asset Studio ウィザードにこのステップに記載された入力値が表示されています。

  1. その後 DetailsScreen.kt で、PaneTemplate 用の ActionStrip を作成します。

ActionStrip UI コンポーネントはヘッダー行のタイトルと反対側にあり、2 番目、3 番目の操作に適しています。ナビゲーションが DetailScreen 上でのメイン操作なので、お気に入りに追加、お気に入りから削除の ActionActionStrip に置くのは、とても良い画面構成です。

DetailScreen.kt

val navigateAction = ...

val actionStrip = ActionStrip.Builder()
    .addAction(
        Action.Builder()
            .setIcon(
                CarIcon.Builder(
                    IconCompat.createWithResource(
                        carContext,
                        R.drawable.baseline_favorite_24
                    )
                ).setTint(
                    if (isFavorite) CarColor.RED else CarColor.createCustom(
                        Color.LTGRAY,
                        Color.DKGRAY
                    )
                ).build()
            )
            .setOnClickListener {
                isFavorite = !isFavorite
            }.build()
    )
    .build()

...

ここでは、次の 2 つに注目してください。

  • CarIcon は、アイテムの状態によって色が変わります。
  • setOnClickListener は、ユーザーの入力に応じてお気に入りの状態を変更するために使用します。
  1. 使用するためには、PaneTemplate.BuildersetActionStrip を必ず呼び出してください。

DetailScreen.kt

return PaneTemplate.Builder(...)
    ...
    .setActionStrip(actionStrip)
    .build()
  1. アプリを実行して動作を見てみましょう。

DetailScreen が表示されています。ユーザーがお気に入りアイコンをタップしますが、アイコンの色が期待どおりには変わりません。

クリックはされているようですが、UI が更新されません。

これは、自動車向けアプリ ライブラリには、更新のコンセプトがあるためです。ドライバーの注意散漫を抑えるために、画面上のコンテンツ更新には一定の制限があります(表示されているテンプレートによって異なります)。また、コードで Screen クラスの invalidate メソッドを明示的に呼び出して要求したときにのみ更新されます。onGetTemplate で参照されている状態を更新するだけでは、UI は更新されません。

  1. この問題を修正するには、OnClickListener を次のように変更します。

DetailScreen.kt

.setOnClickListener {
    isFavorite = !isFavorite
    // Request that `onGetTemplate` be called again so that updates to the
    // screen's state can be picked up
    invalidate()
}
  1. 再度アプリを実行して、クリックするごとにハート型アイコンの色が変わることを確認しましょう。

DetailScreen が表示されています。ユーザーがお気に入りアイコンをタップし、今度は期待どおりに色が変わります。

これで、Android Auto と Android Automotive OS のどちらにも統合可能な基本アプリが作成できました。

12. 完了

初めての自動車向けアプリ ライブラリを使用したアプリが無事に作成できました。次は、学んだことを振り返り、自分のアプリに適用してみましょう。

前にも説明したとおり、現時点では、自動車向けアプリ ライブラリを使用して作成されたアプリのうち、特定のカテゴリのものだけを、Google Play ストアに送信できます。作成したアプリがナビゲーション アプリ、(この Codelab で作成したような)スポット(POI)アプリ、モノのインターネット(IOT)アプリであれば、すぐに開発を始めて、両方のプラットフォームで製品版をリリースできます。

新しいアプリのカテゴリは毎年追加されています。そのため、学んだことを今すぐ生かすことができなくても、後でまた確認してください。そのときには、アプリが車で使えるようになっているかもしれません。

試してみたいこと

  • OEM のエミュレータ(Polestar 2 エミュレータなど)をインストールして、OEM のカスタマイズによって、Android Automotive OS の自動車向けアプリ ライブラリで作成したアプリのデザインが、どのように変わるか確認してみましょう。すべての OEM エミュレータが、自動車向けアプリ ライブラリのアプリをサポートしているわけではありませんのでご注意ください。
  • サンプルアプリを紹介するでは、自動車向けアプリ ライブラリのすべての機能を紹介していますのでご覧ください。

参考資料

リファレンス ドキュメント