瞭解 Car App Library 基礎知識

1. 事前準備

在本程式碼研究室中,您將瞭解如何使用車輛專用 Android App Library,建構已排除分心因素的 Android AutoAndroid Automotive OS 應用程式。您會先加入 Android Auto 的支援功能,接著只需完成極少的額外工作,就能建立該應用程式的變化版本,可在 Android Automotive OS 上執行。應用程式可同時在這兩個平台上執行後,您將另外建構一個畫面,以及部分基本互動功能!

未提供的內容

  • 如何打造適用於 Android Auto 和 Android Automotive OS 的媒體 (音訊) 應用程式相關指南。如要進一步瞭解如何建構這類應用程式,請參閱「打造車用媒體應用程式」一文。
  • 如何打造適用於 Android Auto 的訊息應用程式相關指南。如要進一步瞭解如何建構這類應用程式,請參閱「建構 Android Auto 訊息應用程式」。

軟硬體需求

建構項目

Android Auto

Android Automotive OS

顯示使用電腦版車用運算主機在 Android Auto 中執行應用程式的螢幕錄製畫面。

顯示在 Android Automotive OS 模擬器中執行應用程式的螢幕錄製畫面。

課程內容

  • Car App Library 的用戶端與主機架構如何運作。
  • 如何自行編寫 CarAppServiceSessionScreen 類別。
  • 如何在 Android Auto 和 Android Automotive OS 之間共用實作內容。
  • 如何使用電腦版車用運算主機在開發機器上執行 Android Auto。
  • 如何執行 Android Automotive OS 模擬器。

2. 做好準備

取得程式碼

  1. 您可以在 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

:app 模組依附 :common:data 模組。

:app 模組包含行動應用程式的 UI 和邏輯,:common:data 模組則包含 Place 模型資料類別和用於讀取 Place 模型的存放區。為簡化起見,這個存放區會從硬式編碼的清單讀取資料,但在實際應用程式中,則可從資料庫或後端伺服器輕鬆讀取資料。

:app 模組加入了一個 :common:data 模組的依附元件,因此可以讀取並顯示 Place 模型的清單。

3. 瞭解車輛專用 Android App Library

車輛專用 Android App Library 是一組 Jetpack 程式庫,方便開發人員用來建構車輛專用的應用程式。這個程式庫提供範本式架構,除了提供針對駕駛情境最佳化的使用者介面,也能配合車輛專用的各種硬體設定進行調整 (例如,輸入法、螢幕尺寸和顯示比例)。開發人員可以運用這些功能,輕鬆打造應用程式,並且相信應用程式會在搭載 Android Auto 和 Android Automotive OS 的各式車輛上順暢運作。

瞭解運作方式

使用 Car App Library 建構的應用程式不會直接在 Android Auto 或 Android Automotive OS 中執行,而是仰賴主機應用程式,代表這類應用程式與用戶端應用程式通訊,並轉譯用戶端的使用者介面。Android Auto 本身就是主機,Google Automotive App Host 則是內建 Google 服務、搭載 Android Automotive OS 車輛專用的主機。以下是建構應用程式時必須擴充的 Car App Library 主要類別:

CarAppService

CarAppService 是 Android Service 類別的子類別,可做為主機應用程式與用戶端應用程式 (例如您在本程式碼研究室中建構的應用程式) 通訊的進入點,其主要用途是建立與主機應用程式互動的 Session 例項。

Session

您可以將 Session 視為在車輛螢幕上執行的用戶端應用程式例項。這個類別與其他 Android 元件一樣擁有專屬生命週期,可用於在整個 Session 例項存在期間初始化及拆卸各種資源。CarAppServiceSession 之間是一對多關係。舉例來說,一個 CarAppService 可以有兩個 Session 例項,一個用於主要螢幕,另一個用於儀表板螢幕 (適用於支援儀表板螢幕的導航應用程式)。

Screen

Screen 例項負責產生由主機應用程式轉譯的使用者介面。這些使用者介面由多個 Template 類別表示,其中每個模型代表一個特定類型的版面配置,例如網格清單。每個 Session 管理一個 Screen 例項堆疊,這些例項會處理應用程式不同部分的使用者流程。ScreenSession 一樣,擁有可供掛鉤的專屬生命週期

這是說明 Car App Library 運作方式的圖表。左側是兩個標示為「Display」的方塊,中間的方塊標示為「Host」,右側的方塊標示為「CarAppService」。在 CarAppService 方塊中,有兩個各標示為「Session」的方塊。在第一個 Session 中,有三個互相堆疊的 Screen 方塊。在第二個 Session 中,有兩個互相堆疊的 Screen 方塊。每個 Display 與 Host 之間各有箭頭,Host 與 Session 之間也有箭頭,指出主機如何管理所有不同元件之間的通訊。

您會在本程式碼研究室的「編寫 CarAppService」一節中編寫 CarAppServiceSessionScreen,因此無需擔心還未能運作的功能。

4. 建立初始設定

開始時,請設定包含 CarAppService 的模組並宣告其依附元件。

建立 car-app-service 模組

  1. 在「Project」視窗中選取 :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 檔案中,為 Car App Library 版本新增如下變數宣告,方便您在應用程式的每個模組中使用相同版本。

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 檔案中新增兩個依附元件。
  • androidx.car.app:app:這是 Car App Library 的主要構件,可提供用於建構應用程式的所有核心類別。這個程式庫還有另外三個組成構件:androidx.car.app:app-projected 用於 Android Auto 專屬功能,androidx.car.app:app-automotive 用於 Android Automotive OS 功能程式碼,以及 androidx.car.app:app-testing 用於單元測試可用的部分輔助程式。您會在程式碼研究室的後續章節用到 app-projectedapp-automotive
  • :common:data:這個資料模組與現有行動應用程式所用的資料模組相同,可讓相同的資料來源用於應用程式的所有版本。

build.gradle (Module :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. 首先,在 carappservice 套件的 :common:car-app-service 模組內建立名為 PlacesCarAppService.kt 的檔案。
  2. 在這個檔案內,建立可擴充 CarAppService 的類別並命名為 PlacesCarAppService

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 抽象類別會實作 onBindonUnbindService 方法,並防止後續流程覆寫這些方法,確保與主機應用程式的互通性。您只要實作 createHostValidatoronCreateSession 即可。

系統會在繫結 CarAppService 時參照您從 createHostValidator 傳回的 HostValidator,確保主機為可信任的裝置,並且在主機不符合定義的參數時使繫結失敗。在本程式碼研究室 (以及一般測試) 中,可以使用 ALLOW_ALL_HOSTS_VALIDATOR 輕鬆確保應用程式與主機連結,但不應用於正式版本。如果要進一步瞭解如何為正式版應用程式進行這項設定,請參閱 createHostValidator 的說明文件。

針對這個簡單的應用程式,onCreateSession 可以直接傳回 Session 的例項。如果是更複雜的應用程式,您可以在此初始化長效資源,例如應用程式在車輛上執行時所用的指標和記錄用戶端。

  1. 最後,您需要在 :common:car-app-service 模組的 AndroidManifest.xml 檔案中加入 PlacesCarAppService 的對應 <service> 元素,告知作業系統以及其他應用程式 (例如主機應用程式) PlacesCarAppService 存在的情形。

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>

請注意以下兩個重要事項:

  • <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 的檔案,用於容納擴充 ScreenMainScreen 類別。目前這個套件透過 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 中執行這個應用程式,還需要進行兩項設定。

新增 car-app-service 模組的依附元件

:app 模組的 build.gradle 檔案中新增以下內容:

build.gradle (Module :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 模組中編寫的程式碼,就能與 Car App Library 提供的其他元件 (例如系統提供的授權活動) 連結起來。

宣告 com.google.android.gms.car.application meta-data

  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> 元素,宣告應用程式使用 Car App Library 提供的範本。

automotive_app_desc.xml

<?xml version="1.0" encoding="utf-8"?>
<automotiveApp>
    <uses name="template"/>
</automotiveApp>
  1. :app 模組的 AndroidManifest.xml 檔案中,新增以下 <meta-data> 元素,並參照您剛剛建立的 automotive_app_desc.xml 檔案。

AndroidManifest.xml (:app)

<application ...>

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

    ...

</application>

Android Auto 會讀取這個檔案,瞭解應用程式支援哪些功能,在本例中是指使用 Car App Library 範本系統的功能。接著,Android Auto 會根據這項資訊處理相關行為,例如將應用程式新增至 Android Auto 啟動器,以及從通知開啟應用程式

選用:監聽投影變更

您有時會想知道使用者裝置是否已連線至車輛,方法是使用 CarConnection API,這個 API 提供的 LiveData 可用來觀測連線狀態。

  1. 如要使用 CarConnection API,請先在 :app 模組的 androidx.car.app:app 構件上新增依附元件。

build.gradle (Module :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」

現在畫面上會另外顯示一行文字,表示投影狀態:「Not projecting」

7. 使用電腦版車用運算主機 (DHU) 測試

完成 CarAppService 實作和 Android Auto 設定後,現在可以執行應用程式,看看效果如何。

  1. 在手機上安裝應用程式,然後按照安裝及執行 DHU 的指示操作

啟動並執行 DHU 後,您應該會在啟動器中看到應用程式圖示 (如果沒有,請再次檢查是否已按照上一節所述完成所有步驟,然後先從終端機退出 DHU 再重新啟動)。

  1. 從啟動器開啟應用程式

Android Auto 啟動器排列顯示所有應用程式,其中包括 Places 應用程式。

糟糕,當機了!

錯誤畫面顯示「Android Auto has encountered an unexpected error」訊息,畫面右上角有偵錯切換鈕。

  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 本身一樣,Car App Library 也有 API 級別的概念,因為主機和用戶端應用程式之間需要有契約,才能互相通訊。主機應用程式支援指定 API 級別及其相關功能 (基於回溯相容性,也支援更早級別的功能)。舉例來說,搭載 API 級別 2 以上的主機可以使用 SignInTemplate。但如果嘗試在僅支援 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 執行時看到手機上的狀態更新,如下所示:

顯示投影狀態的文字行現在標示為「Projecting」,因為手機已連線至 DHU。

8. 新增對 Android Automotive OS 的支援

啟動並執行 Android Auto 後,請再加把勁,新增 Android Automotive OS 的支援功能。

建立 :automotive 模組

  1. 如要建立包含應用程式 Android Automotive OS 版本專用程式碼,請在 Android Studio 依序開啟「」>「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 模組時提及,所有支援 Car App Library 應用程式的 Android Automotive OS 車輛,執行最低級別為 API 29。

「Create New Module」精靈列出本步驟設定的 Android Automotive OS 模組相關值。

  1. 按一下「Next」,在下一個畫面選取「No Activity」,最後點選「Finish」

「Create New Module」精靈的第二個頁面,顯示「No Activity」、「Media Service」和「Messaging Service」三個選項,目前已選取「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 (Module :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 模組。

設定資訊清單

  1. 首先,您需要宣告兩項功能 android.hardware.type.automotiveandroid.software.car.templates_host必要

android.hardware.type.automotive 這項系統功能會指出裝置本身為車輛 (詳見 FEATURE_AUTOMOTIVE)。只有將這項功能標示為必要的應用程式,才能提交到 Play 管理中心的 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 上使用相同的資源。請注意,<meta-data> 元素位於 <application> 元素內 (因此您必須變更 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>

以下是這個元素執行的所有動作:

  • android:name 列出 app-automotive 套件中 CarAppActivity 類別的完整名稱。
  • android:exported 設為 true,因為這個 Activity 必須可由本身以外的其他應用程式 (啟動器) 啟動。
  • android:launchMode 設為 singleTask,這樣就一次只能有一個 CarAppActivity 例項。
  • android:theme 設為 @android:style/Theme.DeviceDefault.NoActionBar,讓應用程式占據可用的全部螢幕空間。
  • 意圖篩選器指出這是應用程式的啟動器 Activity
  • <meta-data> 元素向系統指出,應用程式可配合使用者體驗限制使用,例如車輛行駛中的情況。

選用:從 :app 模組複製啟動器圖示

剛剛才建立 :automotive 模組會採用預設的綠色 Android 標誌圖示。

  • 您可以視需要複製 :app 模組中的 mipmap 資源目錄再貼到 :automotive 模組中,使用與行動應用程式相同的啟動器圖示!

9. 使用 Android Automotive OS 模擬器測試

安裝 Automotive with Play Store 系統映像檔

  1. 首先,在 Android Studio 中開啟 SDK Manager,然後選取「SDK Platforms」分頁標籤 (如果尚未選取)。在 SDK Manager 視窗的右下角,確認已勾選「Show package details」方塊。
  2. 安裝下列一或多個模擬器映像檔。映像檔只能在與本身架構 (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 虛擬裝置

  1. 開啟裝置管理工具後,選取視窗左側「Category」欄下方的「Automotive」。接著,從清單中選取「Automotive (1024p landscape)」裝置定義,然後點選「Next」

「Virtual Device Configuration」精靈顯示選取的硬體設定檔「Automotive (1024p landscape)」。

  1. 在下一頁中,選取上一個步驟的系統映像檔 (如果您選擇了 Android 11/API 30 映像檔,可能會在「x86 Images」分頁下方,不是在預設的「Recommended」分頁下方)。按一下「Next」,選取所需進階選項,最後點選「Finish」建立 AVD。

執行應用程式

  1. 使用 automotive 執行設定,在剛建立的模擬器上執行應用程式。

使用的

首次執行應用程式時,可能會出現如下畫面:

應用程式顯示的畫面標示著「System update required」,下方有「Check for updates」按鈕。

在此情況下,請按一下「Check for updates」按鈕,前往 Play 商店的 Google Automotive App Host 應用程式頁面,然後點選「Install」按鈕。如果您在按「Check for updates」按鈕時尚未登入,系統會帶您完成登入流程。登入後,您就可以再次開啟應用程式,點選這個按鈕返回 Play 商店頁面。

Play 商店的 Google Automotive App Host 頁面,右上角有「Install」按鈕。

  1. 最後,主機安裝完成後,從啟動器 (底部列中的九點格狀圖示) 再次開啟應用程式,畫面應顯示如下:

應用程式顯示基本的「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,再加到由 PlaceListMapTemplate 顯示的 ItemList 中。

  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,應用程式主機就會處理地圖轉譯作業!

新增詳細資料畫面

接下來,請新增詳細資料畫面,讓使用者能查看特定地點相關詳細資訊,並選擇要使用偏好的導航應用程式前往該地點,還是返回其他地點的清單。您可以使用 PaneTemplate 來顯示最多四列資訊,旁邊還可視需要設置動作按鈕。

  1. 首先,在 :common:car-app-service 模組中的 res 目錄上按一下滑鼠右鍵,依序點選「New」>「Vector Asset」,然後使用以下設定建立導覽圖示:
  • Asset type:Clip art
  • Clip art:navigation
  • Name:baseline_navigation_24
  • Size:24 dp x 24 dp
  • Color:#000000
  • Opacity:100%

「Asset Studio」精靈顯示本步驟所述的輸入內容

  1. 接著,在 screen 套件中的現有 MainScreen.kt 檔案旁,建立名為 DetailScreen.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 上其他應用程式互動的關鍵動作。

現在有兩種類型的畫面,該在兩者之間加上導覽功能了!Car App Library 中的 Navigation 採用推入與彈出的堆疊模型,非常適合用於行車中完成的簡易工作流程。

圖表展示應用程式內導覽功能與 Car App Library 共同運作的方式。左側是只有一個 MainScreen 的堆疊,與中央堆疊之間有標示為「Push DetailScreen」的箭頭,中央堆疊的現有 MainScreen 上方有 DetailScreen,中央堆疊和右側堆疊之間有標示為「Pop」的箭頭,右側堆疊與左側堆疊相同,只有一個 MainScreen。

  1. 如要從 MainScreen 的其中一個清單項目前往該項目的 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:24 dp x 24 dp
  • Color:#000000
  • Opacity:100%

「Asset Studio」精靈顯示本步驟所述的輸入內容

  1. 然後,在 DetailsScreen.kt 中為 PaneTemplate 建立 ActionStrip

ActionStrip UI 元件位於標頭列,與標題相對的位置,適合設定次要動作和第三動作。由於導覽是使用者在 DetailScreen 上採取的主要動作,將收藏或取消收藏的 Action 放在 ActionStrip 中,是建立畫面結構的絕佳方式。

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

...

這裡有兩個有趣的部分:

  • CarIcon 會根據項目的狀態改變色調。
  • 使用 setOnClickListener 來回應使用者的輸入內容,並切換收藏狀態。
  1. 別忘了要在 PaneTemplate.Builder 呼叫 setActionStrip,才能使用!

DetailScreen.kt

return PaneTemplate.Builder(...)
    ...
    .setActionStrip(actionStrip)
    .build()
  1. 接著,執行應用程式,看看會有什麼結果:

畫面上顯示 DetailScreen,使用者正在輕觸收藏圖示,但圖示並未如預期變色。

真有趣... 看來使用者一直在點選圖示,但 UI 並未更新。

這是因為 Car App Library 有特定的「重新整理」概念。為減少致使駕駛人分心的情形,對重新整理畫面內容設有某些限制 (隨顯示的範本而異),且必須由您的程式碼明確要求每次的重新整理,也就是呼叫 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. 恭喜

您已成功建構第一個 Car App Library 應用程式。現在就運用所學,套用到自己的應用程式吧!

如稍早提及,若是使用 Car App Library 建構,目前只有某些類別的應用程式可以提交到 Play 商店。如果您的應用程式是導航應用程式、搜尋點 (POI) 應用程式 (就像您在本程式碼實驗室實作的應用程式) 或物聯網 (IOT) 應用程式,您可以立即開始建構,並直接將應用程式發布到這兩個平台的實際環境。

我們會逐年增加新的應用程式類別,因此即使未能立即運用所學,仍歡迎您日後再回來查看,屆時您也許就能將應用程式擴充到車用範圍!

體驗功能

  • 安裝原始設備製造商 (OEM) 的模擬器 (例如 Polestar 2 模擬器),看看在 Android Automotive OS 上,OEM 自訂功能如何改變 Car App Library 應用程式的外觀與風格。請注意,有些 OEM 模擬器並不支援 Car App Library 應用程式。
  • 查看示範應用程式如何展示 Car App Library 的完整功能。

其他資訊

參考文件