デジスマチームでソフトウェアエンジニアをしている大和です。車で全国各地の温泉を巡るのが趣味です。今回は運転中の困りごとを解決するために開発したAndroid Auto向けアプリの開発過程を通して、開発方法について共有していきます。
この記事はデジスマチームブログリレーの9日目の記事です。
- 開発のきっかけ
- Android Autoについて
- Android Autoアプリ開発の基礎知識
- 開発環境の設定
- テスト方法
- プロジェクト作成とHello World
- 機能実装
- 完成したアプリと実車テスト
- まとめ
- We are Hiring!
開発のきっかけ
私はAndroid Auto対応のディスプレイがついた車に乗っており、Googleマップのナビゲーションを利用しています。遠出の際には高速道路を運転するのですが、GoogleマップではPA / SAをいい感じに確認する方法がなく、別のサイトでどれくらい先にPA / SAがあるのか確認して設定していました。この手順が毎回面倒なので、今回は自前でアプリを準備してみることにしました。
開発するアプリの内容
今回開発するアプリは以下を満たすものとします。
- PA / SAを一覧できる
- 現在地点からの距離を表示できる
- Googleマップのナビゲーションを設定できる
Android Autoについて
Android Autoとは、Android端末を各社のナビやナビの付いていないディスプレイと接続することで、対応しているアプリを車で利用できるシステムです。対応アプリ自体はスマートフォン側で実行されるのが特徴です。
自動車向けのシステムとしてAndroid Automotive OSというプラットフォームもあり、こちらは車のディスプレイ単体で利用できるものです。しかし私の車はAndroid Auto対応のためこちらは今回触れません。
Android Autoアプリ開発の基礎知識
制限
Android Auto向けのアプリは、運転中に操作されるものも含まれるため制限が含まれます。例えば次のようなものです。
- 基本的に自由なUIデザインは不可
- 操作は最小限ステップに制限 (5回まで)
他にもアプリが満たすべき条件が公開されています。
後述する実車テストの際には、一部の例外を除きGoogle Playストアからインストールする必要があるため、予めGoogle Play Consoleデベロッパーアカウントを登録しておく必要があります。
サポートされているアプリカテゴリ
Android Autoでは、運転の安全性を確保するため、開発できるアプリのカテゴリが制限されています。一例として次のようなものがあります。
- ナビゲーション: 地図アプリなどのルート案内
- POI (Point of Interest): 駐車場やガソリンスタンドなどのスポット操作
- メディア: 音楽やラジオ等のオーディオコンテンツ (動画は含まない)
- コミュニケーション: 通知やメッセージの読み上げ・音声入力
今回のPA / SAを探すアプリは POIカテゴリ に該当します。
Android for Cars アプリライブラリとテンプレート
Android Autoアプリは、Android for Cars アプリライブラリが提供するテンプレートを使って開発することが推奨されています。これにより、開発を簡単にするとともにUIの一貫性と安全性が保たれます。
利用できるテンプレートはカテゴリによって制限されており、例えば地図を表示するテンプレートを利用できるのはナビゲーションカテゴリやPOIカテゴリなどです。
開発環境の設定
Android Studio (Android SDK) の設定
Android Auto開発には、通常のAndroid開発と同様にAndroid Studioを利用します。開発自体はそのまま行えますが、テスト用にDHU (Desktop Head Unit) があると便利です。DHUのインストール手順は次のとおりです。
- SDK Managerで
SDK Toolsタブを開きAndroid Auto Desktop Head Unit Emulatorをインストールする - 実行権限をつける:
chmod +x path/to/install/extras/google/auto/desktop-head-unit- インストールされるディレクトリは
~/Library/Android/sdkなどです
- インストールされるディレクトリは
DHUを使用することで、Android端末とUSBケーブルがあればAndroid Autoアプリのテストが可能になります。

開発者モードの有効化
通常の開発者モードに加えて、Android Auto用の開発者モードを有効化する必要があります。開発者向けオプションを有効にするおよびAndroid Auto デベロッパーモードを参考にして有効化してください。
また、DHUを使ったテストの際にはデバッグ接続が必要なため、デバイスで USB デバッグを有効にするを参考に設定してください。
テスト方法
DHUでのテスト
通常のAndroidアプリの開発と同じ手順でAndroid Studio上でビルドしてAndroid端末にインストールすることで、DHU上でアプリを実行できます。

DHUはデバッグ接続を有効化したAndroid端末をUSB接続後、次のコマンドを実行することで起動できます。
path/to/install/extras/google/auto/desktop-head-unit --usb
実車テスト
Google Play Consoleから内部テストとして配信することで、実車でもテスト可能です。内部テストについては一般的なAndroidアプリ開発の手順と同じであるため、詳しい手順はドキュメントを参照してください。
プロジェクト作成とHello World
開発の準備が整ったので、DHUに表示できるところまで整えます。
プロジェクト作成
Android StudioでNew Projectを選択してプロジェクトを作成します。通常のAndroidアプリがAndroid Autoをサポートする形になるので、作成したいアプリに応じてテンプレートを選択します。今回は解説範囲外ですがAndroidアプリとしても動作するように開発するので、Bottom Navigation Views Activityを選択します。

Android Auto対応の設定
最初にAndroid Auto対応のためライブラリを追加します。
app/build.gradle.kts :
dependencies {
:
implementation("androidx.car.app:app:1.7.0")
implementation("androidx.car.app:app-projected:1.7.0")
testImplementation("androidx.car.app:app-testing:1.7.0")
}
次にManifestの設定をします。
app/src/main/AndroidManifest.xml :
<manifest ...> <uses-permission android:name="androidx.car.app.MAP_TEMPLATES" /> <application ...> <meta-data android:name="com.google.android.gms.car.application" android:resource="@xml/automotive_app_desc"/> <meta-data android:name="androidx.car.app.minCarApiLevel" android:value="1" tools:ignore="MetadataTagInsideApplicationTag" /> <service android:name="com.example.aa_example.auto.AACarAppService" android:exported="true"> <intent-filter> <action android:name="androidx.car.app.CarAppService" /> <category android:name="androidx.car.app.category.POI" /> </intent-filter> </service> <activity ...>...</activity> </application> </manifest>
app/src/main/res/xml/automotive_app_desc.xml :
<?xml version="1.0" encoding="utf-8"?> <automotiveApp> <uses name="template" /> </automotiveApp>
CarAppServiceの実装
AndroidアプリのAndroid Auto対応は、manifestで定義したservice ( CarAppService を継承したclass) を実装することで行えます。ここでは画面に Hello World と表示するアプリを実装します。
前述したテンプレートのうち PlaceListMapTemplate を使って実装します。このテンプレートではスポットのリストを含めることで、地図の横にリストが表示される画面を作成できます。
app/src/main/java/com/example/aa_example/auto/AACarAppService.kt :
package com.example.aa_example.auto import android.content.Intent import androidx.car.app.CarAppService import androidx.car.app.Screen import androidx.car.app.Session import androidx.car.app.validation.HostValidator class AACarAppService : CarAppService() { override fun createHostValidator(): HostValidator { return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR } override fun onCreateSession(): Session { return object : Session() { override fun onCreateScreen(intent: Intent): Screen { return AAScreen(carContext) } } } }
app/src/main/java/com/exmaple/aa_example/auto/AAScreen.kt
package com.example.aa_example.auto import androidx.car.app.CarContext import androidx.car.app.Screen import androidx.car.app.model.Action import androidx.car.app.model.ItemList import androidx.car.app.model.PlaceListMapTemplate import androidx.car.app.model.Template class AAScreen(carContext: CarContext) : Screen(carContext) { override fun onGetTemplate(): Template { return PlaceListMapTemplate.Builder() .setTitle("Hello World") .setHeaderAction(Action.APP_ICON) .setItemList(ItemList.Builder().build()) .build() } }
ここまでの実装をDHUで確認してみると、次のように表示されます。

機能実装
DHU上で動作する雛形を準備できたので、欲しい機能を実装します。
権限追加
今回のPOIアプリでは現在地の情報を利用するため、次の権限を追加します。
app/src/main/AndroidManifest.xml :
<manifest ...> : <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> : </manifest>
スポット表示
アプリの機能を実装していきます。まずはPA / SAの情報を持つdata classを準備します。緯度経度を渡すことで直線距離を計算するmethodも実装しておきます。
app/src/main/java/com/example/aa_example/model/PlaceType.kt :
package com.example.aa_example.model enum class PlaceType( val label: String, ) { PA("PA"), SA("SA"), }
app/src/main/java/com/example/aa_example/model/PlaceInfo.kt :
package com.example.aa_example.model import android.location.Location data class PlaceInfo( val type: PlaceType, val name: String, val latitude: Double, val longitude: Double, ) { fun distance(currentLat: Double, currentLng: Double): Double { val results = FloatArray(size = 1) Location.distanceBetween( currentLat, currentLng, latitude, longitude, results, ) return results.first() / 1000.0 } }
次にPA / SAのデータを準備します。今回はサンプルなのでベタ書きしたファイルを準備します。
app/src/main/java/com/example/aa_example/db/places.kt :
package com.example.aa_example.db import com.example.aa_example.model.PlaceInfo import com.example.aa_example.model.PlaceType val places = listOf( PlaceInfo( type = PlaceType.SA, name = "守谷SA", latitude = 35.9397, longitude = 139.9685, ), PlaceInfo( type = PlaceType.PA, name = "谷田部東PA", latitude = 36.0389, longitude = 140.1378, ), )
準備ができたので、表示部分を実装していきます。画面の表示は Screen を継承したclassで行われるため、実装を更新して ItemList にPA / SA情報を渡すようにします。
--- a/app/src/main/java/com/example/aa_example/auto/AAScreen.kt +++ b/app/src/main/java/com/example/aa_example/auto/AAScreen.kt @@ -1,18 +1,68 @@ package com.example.aa_example.auto +import android.text.SpannableString +import android.text.Spanned import androidx.car.app.CarContext import androidx.car.app.Screen import androidx.car.app.model.Action +import androidx.car.app.model.CarLocation +import androidx.car.app.model.Distance +import androidx.car.app.model.DistanceSpan import androidx.car.app.model.ItemList +import androidx.car.app.model.Metadata +import androidx.car.app.model.Place import androidx.car.app.model.PlaceListMapTemplate +import androidx.car.app.model.PlaceMarker +import androidx.car.app.model.Row import androidx.car.app.model.Template +import com.example.aa_example.db.places class AAScreen(carContext: CarContext) : Screen(carContext) { + + // フォールバック用の位置情報 + private val fallbackLocation = 35.6762 to 139.6503 + + private fun getCurrentLocation(): Pair<Double, Double> { + return fallbackLocation + } + override fun onGetTemplate(): Template { + val currentLocation = getCurrentLocation() + + val itemListBuilder = ItemList.Builder() + places.forEach { place -> + val text = SpannableString(" ").apply { + val distance = Distance.create( + place.distance(currentLocation.first, currentLocation.second), + Distance.UNIT_KILOMETERS, + ) + setSpan( + DistanceSpan.create(distance), + 0, + 1, + Spanned.SPAN_INCLUSIVE_INCLUSIVE, + ) + } + val row = Row.Builder() + .setMetadata( + Metadata.Builder() + .setPlace( + Place.Builder(CarLocation.create(place.latitude, place.longitude)) + .setMarker(PlaceMarker.Builder().setLabel(place.type.label).build()) + .build() + ) + .build() + ) + .setTitle(place.name) + .addText(text) + .build() + itemListBuilder.addItem(row) + } + return PlaceListMapTemplate.Builder() - .setTitle("Hello World") + .setTitle("PA / SA") .setHeaderAction(Action.APP_ICON) - .setItemList(ItemList.Builder().build()) + .setItemList(itemListBuilder.build()) .build() } }
これによりPA / SA情報がリストで表示されるようになりました。

残りの実装をしていきます。まず、現在の位置情報を取得できるようにします。
Screen からアクセス可能な CarContext 経由で様々な情報を取得可能で、位置情報もここから取得します。
--- a/app/src/main/java/com/example/aa_example/auto/AAScreen.kt +++ b/app/src/main/java/com/example/aa_example/auto/AAScreen.kt @@ -1,8 +1,11 @@ package com.example.aa_example.auto +import android.content.Context +import android.location.LocationManager import android.text.SpannableString import android.text.Spanned import androidx.car.app.CarContext +import androidx.car.app.CarToast import androidx.car.app.Screen import androidx.car.app.model.Action import androidx.car.app.model.CarLocation @@ -19,11 +22,27 @@ import com.example.aa_example.db.places class AAScreen(carContext: CarContext) : Screen(carContext) { + private val locationManager: LocationManager = carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager + // フォールバック用の位置情報 private val fallbackLocation = 35.6762 to 139.6503 private fun getCurrentLocation(): Pair<Double, Double> { - return fallbackLocation + return try { + val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) + if (location == null) { + CarToast.makeText(carContext, "位置情報の取得に失敗しました。", CarToast.LENGTH_SHORT).show() + return fallbackLocation + } + + location.latitude to location.longitude + } catch (e: SecurityException) { + CarToast.makeText(carContext, "位置情報の権限がありません。", CarToast.LENGTH_SHORT).show() + fallbackLocation + } catch (e: Exception) { + CarToast.makeText(carContext, "位置情報の取得に失敗しました。", CarToast.LENGTH_SHORT).show() + fallbackLocation + } } override fun onGetTemplate(): Template {
最後にリストからGoogleマップへ遷移できるようにします。
--- a/app/src/main/java/com/example/aa_example/auto/AAScreen.kt +++ b/app/src/main/java/com/example/aa_example/auto/AAScreen.kt @@ -1,6 +1,7 @@ package com.example.aa_example.auto import android.content.Context +import android.content.Intent import android.location.LocationManager import android.text.SpannableString import android.text.Spanned @@ -18,7 +19,9 @@ import androidx.car.app.model.PlaceListMapTemplate import androidx.car.app.model.PlaceMarker import androidx.car.app.model.Row import androidx.car.app.model.Template +import androidx.core.net.toUri import com.example.aa_example.db.places +import com.example.aa_example.model.PlaceInfo class AAScreen(carContext: CarContext) : Screen(carContext) { @@ -45,6 +48,18 @@ class AAScreen(carContext: CarContext) : Screen(carContext) { } } + private fun startNavigation(carContext: CarContext, place: PlaceInfo) { + try { + val uri = "geo:${place.latitude},${place.longitude}?q=${place.name}&mode=d".toUri() + val intent = Intent(CarContext.ACTION_NAVIGATE, uri) + + carContext.startCarApp(intent) + CarToast.makeText(carContext, "${place.name}への案内を開始します", CarToast.LENGTH_SHORT).show() + } catch (e: Exception) { + CarToast.makeText(carContext, "地図アプリを開けませんでした", CarToast.LENGTH_SHORT).show() + } + } + override fun onGetTemplate(): Template { val currentLocation = getCurrentLocation() @@ -74,6 +89,7 @@ class AAScreen(carContext: CarContext) : Screen(carContext) { ) .setTitle(place.name) .addText(text) + .setOnClickListener { startNavigation(carContext = carContext, place = place) } .build() itemListBuilder.addItem(row) }
ここまでの実装で欲しい機能を全て実装できました。
完成したアプリと実車テスト
データの追加などの追加実装をした後、実車でテストしてみました。Google Play Consoleで内部テストを有効にしてアプリをインストール後、車のディスプレイに接続して試した結果が次の動画です。

また、おまけとしてAndroid上でも通常のアプリとして動作するようにしました。Android上でGoogleマップのナビを設定すると目的地がAndroid Auto側でも引き継がれるので、スマートフォンを操作可能なときはこちらからも目的地を設定できるようになりました。

今後の課題
まだ実装できていない点についてメモしておきます。
- 上り / 下りなどのフィルター実装 → どのようなUIにできるか確認中
- 高速道路上にいる判定 → 自前で実装する必要がある
- PA / SAの経路上の距離 → 自前で実装する必要がある
- リリースの目処 → 2023年11月に基準が厳しくなったので、テスターを集める必要がある
まとめ
Android Auto向けアプリ開発では、通常のAndroid開発とは異なる制約があります。しかし提供されているテンプレートを利用することで、少ない実装でアプリ開発が可能です。私はAndroidアプリ開発経験はありませんでしたが、実装する部分で混乱するところは少なく、実車上でテストするところまでできました。Androidアプリを公開されている方は追加実装することでAndroid Autoに対応できるため、紹介した手順と公式ドキュメントを参考にして実装してみてはいかがでしょうか。
なお、今回紹介した部分のソースコードはGitHub上で公開しています。
We are Hiring!
エムスリーでは一緒にプロダクト開発をするエンジニアを絶賛募集中です。
デジスマの仕事に興味をお持ちいただけたならぜひ詳細をご確認ください。