使用車輛專用 Android App Library

車輛專用 Android App Library 可讓您將導航、搜尋點 (POI) 和物聯網 (IoT) 應用程式帶入車輛。這項服務提供一組範本,可滿足駕駛人分心程度標準,並處理各種車輛螢幕因素和輸入模式等細節。

本指南將概略介紹程式庫的主要功能和概念,並逐步引導您設定基本應用程式。

事前準備

  1. 詳閱「Design for Driving」頁面,瞭解車輛應用程式程式庫的相關資訊。
  2. 請參閱下文中的重要詞彙和概念
  3. 熟悉 Android Auto 系統 UIAndroid Automotive OS 設計
  4. 查看版本資訊
  5. 查看範例

重要詞彙與概念

模型和範本
使用者介面以模型物件的圖表表示,這些物件可根據所屬範本的規定,以不同方式排列。範本是模型的子集,可在這些圖表中充當根節點。模型包含要以文字和圖片形式向使用者顯示的資訊,以及用於設定這類資訊視覺外觀的屬性,例如文字顏色或圖片大小。主機會將模型轉換為視圖,這些視圖旨在符合駕駛人分心程度標準,並處理各種車輛螢幕因素和輸入模式等細節。
舉辦派對
主機是後端元件,可實作程式庫 API 提供的功能,讓應用程式能在車輛中執行。主機的職責範圍廣泛,從探索應用程式和管理其生命週期,到將模型轉換為檢視畫面,以及通知應用程式使用者互動情形。在行動裝置上,這個主機是由 Android Auto 實作。在 Android Automotive OS 上,這個主機會以系統應用程式的形式安裝。
範本限制
不同範本會在模型內容中強制執行限制。舉例來說,清單範本會限制向使用者顯示的項目數量。範本在連結方式上也有限制,因此無法形成任務的流程。舉例來說,應用程式只能將最多五個範本推送至畫面堆疊。詳情請參閱「範本限制」。
Screen
Screen 是程式庫提供的類別,應用程式會實作此類別,以便管理向使用者顯示的使用者介面。Screen 具有生命週期,並提供機制,讓應用程式在螢幕可見時傳送範本以供顯示。Screen 例項也可以推送至 Screen 堆疊,並從中彈出,確保它們遵循範本流程限制
CarAppService
CarAppService 是抽象的 Service 類別,應用程式必須實作並匯出此類別,才能由主機發現及管理。應用程式的 CarAppService 負責驗證主機連線是否可信任,方法是使用 createHostValidator,然後使用 onCreateSession 為每個連線提供 Session 例項。
Session

Session 是抽象類別,您的應用程式必須使用 CarAppService.onCreateSession 實作並傳回。這項元素可用於在車輛螢幕上顯示資訊。它具有生命週期,可告知車輛螢幕上應用程式的目前狀態,例如應用程式是否顯示或隱藏。

啟動 Session 時 (例如首次啟動應用程式時),主機會使用 onCreateScreen 方法,要求顯示初始 Screen

安裝 Car App Library

如需將程式庫新增至應用程式的操作說明,請參閱 Jetpack 程式庫發布頁面

設定應用程式的資訊清單檔案

在建立車輛應用程式之前,請按照下列步驟設定應用程式的資訊清單檔案

宣告 CarAppService

主機會透過您的 CarAppService 實作連線至應用程式。您可以在資訊清單中宣告這項服務,讓主機探索並連線至您的應用程式。

此外,您還需要在應用程式意圖篩選器的 <category> 元素中宣告應用程式類別。如要瞭解此元素允許的值,請參閱支援的應用程式類別清單。

下列程式碼片段說明如何在資訊清單中,為 POI 應用程式宣告車輛應用程式服務:

<application>
    ...
   <service
       ...
        android:name=".MyCarAppService"
        android:exported="true">
      <intent-filter>
        <action android:name="androidx.car.app.CarAppService"/>
        <category android:name="androidx.car.app.category.POI"/>
      </intent-filter>
    </service>

    ...
<application>

支援的應用程式類別

上一節所述,在宣告 CarAppService 時,請在意圖篩選器中加入一或多個下列類別值,藉此宣告應用程式類別:

請參閱「車用 Android 應用程式品質」,進一步瞭解各個類別和應用程式所屬類別的條件。

指定應用程式名稱和圖示

您必須指定應用程式名稱和圖示,讓主機可在系統使用者介面中使用此圖示代表您的應用程式。

您可以使用 CarAppServicelabelicon 屬性,指定用來代表應用程式的名稱和圖示:

...
<service
   android:name=".MyCarAppService"
   android:exported="true"
   android:label="@string/my_app_name"
   android:icon="@drawable/my_app_icon">
   ...
</service>
...

如果未在 <service> 元素中宣告標籤或圖示,主機會改用為 <application> 元素指定的值。

設定自訂主題

如要為車輛應用程式設定自訂主題,請在資訊清單檔案中新增 <meta-data> 元素,如下所示:

<meta-data
    android:name="androidx.car.app.theme"
    android:resource="@style/MyCarAppTheme />

接著,宣告樣式資源,為自訂車用應用程式主題設定下列屬性:

<resources>
  <style name="MyCarAppTheme">
    <item name="carColorPrimary">@layout/my_primary_car_color</item>
    <item name="carColorPrimaryDark">@layout/my_primary_dark_car_color</item>
    <item name="carColorSecondary">@layout/my_secondary_car_color</item>
    <item name="carColorSecondaryDark">@layout/my_secondary_dark_car_color</item>
    <item name="carPermissionActivityLayout">@layout/my_custom_background</item>
  </style>
</resources>

Car App API 級別

車輛應用程式程式庫會定義自己的 API 級別,讓您瞭解車輛上的範本主機支援哪些程式庫功能。如要擷取主機支援的最高 Car App API 級別,請使用 getCarAppApiLevel() 方法。

AndroidManifest.xml 檔案中宣告應用程式支援的最低 Car App API 級別:

<manifest ...>
    <application ...>
        <meta-data
            android:name="androidx.car.app.minCarApiLevel"
            android:value="1"/>
    </application>
</manifest>

請參閱 RequiresCarApi 註解的說明文件,進一步瞭解如何維持回溯相容性,以及如何宣告使用功能所需的最低 API 級別。如要瞭解使用 Car App Library 特定功能所需的 API 級別,請參閱 CarAppApiLevels 的參考文件。

建立 CarAppService 和工作階段

您的應用程式需要擴充 CarAppService 類別,並實作其 onCreateSession 方法,該方法會傳回與主機目前連線相對應的 Session 例項:

Kotlin

class HelloWorldService : CarAppService() {
    ...
    override fun onCreateSession(): Session {
        return HelloWorldSession()
    }
    ...
}

Java

public final class HelloWorldService extends CarAppService {
    ...
    @Override
    @NonNull
    public Session onCreateSession() {
        return new HelloWorldSession();
    }
    ...
}

Session 例項負責在應用程式首次啟動時,傳回要使用的 Screen 例項:

Kotlin

class HelloWorldSession : Session() {
    ...
    override fun onCreateScreen(intent: Intent): Screen {
        return HelloWorldScreen(carContext)
    }
    ...
}

Java

public final class HelloWorldSession extends Session {
    ...
    @Override
    @NonNull
    public Screen onCreateScreen(@NonNull Intent intent) {
        return new HelloWorldScreen(getCarContext());
    }
    ...
}

如要處理汽車應用程式需要從非應用程式主畫面或到達畫面啟動的情況 (例如處理深層連結),您可以在從 onCreateScreen 返回前,使用 ScreenManager.push 預先播種畫面的返回堆疊。預先填入資料可讓使用者從應用程式顯示的第一個畫面返回先前畫面。

建立起始畫面

您可以透過定義可擴充 Screen 類別的類別,並實作其 onGetTemplate 方法,建立應用程式要顯示的螢幕。此方法會傳回 Template 例項,代表要在車輛螢幕上顯示的 UI 狀態。

下列程式碼片段說明如何宣告使用 PaneTemplate 範本的 Screen,以便顯示簡單的「Hello world!」字串:

Kotlin

class HelloWorldScreen(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()
    }
}

Java

public class HelloWorldScreen extends Screen {
    @NonNull
    @Override
    public Template onGetTemplate() {
        Row row = new Row.Builder().setTitle("Hello world!").build();
        Pane pane = new Pane.Builder().addRow(row).build();
        return new PaneTemplate.Builder(pane)
            .setHeaderAction(Action.APP_ICON)
            .build();
    }
}

CarContext 類別

CarContext 類別是 ContextWrapper 子類別,可供 SessionScreen 例項存取。它可讓您存取車輛服務,例如用於管理螢幕堆疊ScreenManager;用於一般應用程式相關功能的 AppManager,例如用於存取 Surface 物件以繪製地圖;以及用於即時路線導航應用程式,用於與主機通訊導航中繼資料和其他導航相關事件NavigationManager

如要查看導航應用程式可用的完整程式庫功能清單,請參閱「存取導覽範本」。

CarContext 也提供其他功能,例如讓您使用車輛螢幕上的設定載入可繪資源、使用意圖在車輛中啟動應用程式,以及指出應用程式應以深色主題顯示地圖。

實作畫面導覽

應用程式通常會顯示多個不同的畫面,每個畫面可能都會使用不同的範本,讓使用者在與畫面中顯示的介面互動時,可以瀏覽這些畫面。

ScreenManager 類別提供一個畫面堆疊,可用於推送畫面,當使用者在汽車螢幕上選取返回按鈕,或使用部分車輛提供的硬體返回按鈕時,系統會自動彈出畫面。

下列程式碼片段說明如何在訊息範本中加入返回動作,以及在使用者選取時推送新畫面的動作:

Kotlin

val template = MessageTemplate.Builder("Hello world!")
    .setHeaderAction(Action.BACK)
    .addAction(
        Action.Builder()
            .setTitle("Next screen")
            .setOnClickListener { screenManager.push(NextScreen(carContext)) }
            .build())
    .build()

Java

MessageTemplate template = new MessageTemplate.Builder("Hello world!")
    .setHeaderAction(Action.BACK)
    .addAction(
        new Action.Builder()
            .setTitle("Next screen")
            .setOnClickListener(
                () -> getScreenManager().push(new NextScreen(getCarContext())))
            .build())
    .build();

Action.BACK 物件是自動叫用 ScreenManager.pop 的標準 Action。您可以使用 CarContext 提供的 OnBackPressedDispatcher 例項,覆寫這項行為。

為確保應用程式在行車時可安全使用,螢幕堆疊最多可有五個螢幕深度。詳情請參閱「範本限制」一節。

重新整理範本內容

應用程式可以呼叫 Screen.invalidate 方法,要求讓 Screen 的內容失效。主機隨後會呼叫應用程式的 Screen.onGetTemplate 方法,以使用新內容擷取範本。

重新整理 Screen 時,請務必瞭解範本中可更新的特定內容,以免主機將新範本計入範本配額。詳情請參閱「範本限制」一節。

建議您架構畫面,讓 Screen 與其透過 onGetTemplate 實作項目傳回的範本類型之間,建立一對一的對應關係。

繪製地圖

使用下列範本的導航和搜尋點 (POI) 應用程式,可透過存取 Surface 繪製地圖:

Template 範本權限 類別指南
NavigationTemplate androidx.car.app.NAVIGATION_TEMPLATES 導覽
MapWithContentTemplate androidx.car.app.NAVIGATION_TEMPLATES
androidx.car.app.MAP_TEMPLATES
導航興趣點天氣
MapTemplate (已淘汰) androidx.car.app.NAVIGATION_TEMPLATES 導覽
PlaceListNavigationTemplate (已淘汰) androidx.car.app.NAVIGATION_TEMPLATES 導覽
RoutePreviewNavigationTemplate (已淘汰) androidx.car.app.NAVIGATION_TEMPLATES 導覽

宣告表面權限

除了應用程式使用的範本所需的權限之外,應用程式還必須在其 AndroidManifest.xml 檔案中宣告 androidx.car.app.ACCESS_SURFACE 權限,才能存取途徑:

<manifest ...>
  ...
  <uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />
  ...
</manifest>

存取表面

如要存取主機提供的 Surface,您必須實作 SurfaceCallback,並將該實作項目提供給 AppManager 車輛服務。目前的 Surface 會傳遞至 onSurfaceAvailable()onSurfaceDestroyed() 回呼的 SurfaceContainer 參數中的 SurfaceCallback

Kotlin

carContext.getCarService(AppManager::class.java).setSurfaceCallback(surfaceCallback)

Java

carContext.getCarService(AppManager.class).setSurfaceCallback(surfaceCallback);

瞭解表面的可見區域

主機可在地圖上層為範本繪製使用者介面元素。只要呼叫 SurfaceCallback.onVisibleAreaChanged 方法,主機即可與使用者絕對可見的開放區域通訊。此外,為了盡量減少變更,主機會透過最小矩形呼叫 SurfaceCallback.onStableAreaChanged 方法,這種矩形一律會根據目前的範本顯示。

舉例來說,如果導航應用程式使用有頂端動作列NavigationTemplate,在使用者有一段時間未與畫面互動後,系統就會隱藏該動作列,為地圖提供更多空間。在這種情況下,系統會使用相同矩形回呼 onStableAreaChangedonVisibleAreaChanged。動作列隱藏時,系統只會使用較大區域呼叫 onVisibleAreaChanged。如果使用者與畫面互動,系統只會使用第一個矩形呼叫 onVisibleAreaChanged

支援深色主題

當主機判定條件可提供擔保時,應用程式必須使用適當的深色在 Surface 例項上重新繪製地圖,如「車用 Android 應用程式品質指南」所述。

如要判斷是否應繪製深色地圖,可以使用 CarContext.isDarkMode 方法。只要深色主題狀態變更,您就會收到 Session.onCarConfigurationChanged 呼叫。

允許使用者與地圖互動

使用下列範本時,您可以新增使用者與繪製地圖互動的支援功能,例如讓使用者縮放及平移地圖,查看地圖的不同部分。

Template 開始支援互動功能的 Car App API 級別
NavigationTemplate 2
PlaceListNavigationTemplate (已淘汰) 4
RoutePreviewNavigationTemplate (已淘汰) 4
MapTemplate (已淘汰) 5 (範本簡介)
MapWithContentTemplate 7 (範本簡介)

實作互動回呼

SurfaceCallback 介面提供多種回呼方法,您可以導入這些方法,在使用前面範本建構的地圖中加入互動功能:

互動 SurfaceCallback 方法 開始支援的 Car App API 級別
輕觸 onClick 5
雙指撥動縮放 onScale 2
單點觸控拖曳 onScroll 2
單點觸控滑動 onFling 2
輕觸兩下 onScale (由範本主機決定縮放比例係數) 2
平移模式的旋轉自動提醒 onScroll (由範本主機決定的距離係數) 2

新增地圖動作區域

這些範本可包含地圖動作列,用來顯示地圖相關動作,例如縮放、重新置中、顯示指南針,以及其他選擇顯示的動作。Google 地圖動作列最多可有四個純圖示按鈕,這些按鈕可重新整理,並且不影響工作深度。處於閒置狀態時,這個動作列會隱藏,待回到啟用狀態才會再顯示。

如要接收地圖互動回呼,您必須在 Google 地圖動作列中新增 Action.PAN 按鈕。使用者按下平移按鈕時,主機會進入平移模式,如下一節所述。

如果應用程式忽略地圖動作列中的 Action.PAN 的按鈕,就無法從 SurfaceCallback 方法收到使用者輸入內容,而且主機會結束先前啟用的平移模式。

如果是觸控螢幕,畫面上不會顯示平移按鈕。

瞭解平移模式

在平移模式下,使用者透過非觸控輸入裝置 (例如旋轉控制器和觸控板) 輸入的內容,會由範本主機轉譯成適當的 SurfaceCallback 方法。使用 NavigationTemplate.Builder 中的 setPanModeListener 方法,可在使用者進入或結束平移模式時做出反應。使用者處於平移模式時,主機可隱藏範本中的其他 UI 元件。

與使用者互動

應用程式可以使用類似行動應用程式的模式與使用者互動。

處理使用者輸入內容

應用程式可以將適當的事件監聽器傳遞至支援的模型,以便回應使用者輸入內容。以下程式碼片段說明如何建立 Action 模型,設定 OnClickListener,以便回呼至應用程式程式碼定義的方法:

Kotlin

val action = Action.Builder()
    .setTitle("Navigate")
    .setOnClickListener(::onClickNavigate)
    .build()

Java

Action action = new Action.Builder()
    .setTitle("Navigate")
    .setOnClickListener(this::onClickNavigate)
    .build();

接著,onClickNavigate 方法可以使用 CarContext.startCarApp 方法啟動預設的車輛導航應用程式

Kotlin

private fun onClickNavigate() {
    val intent = Intent(CarContext.ACTION_NAVIGATE, Uri.parse("geo:0,0?q=" + address))
    carContext.startCarApp(intent)
}

Java

private void onClickNavigate() {
    Intent intent = new Intent(CarContext.ACTION_NAVIGATE, Uri.parse("geo:0,0?q=" + address));
    getCarContext().startCarApp(intent);
}

如要進一步瞭解如何啟動應用程式 (包括 ACTION_NAVIGATE 意圖的格式),請參閱「使用意圖啟動車輛應用程式」一節。

部分動作 (例如需要引導使用者在行動裝置上繼續互動) 僅能在車輛停妥時執行。您可以使用 ParkedOnlyOnClickListener 實作這些動作。如果車輛未停妥,房東會向使用者顯示相關訊息,說明在這種情況下不允許執行這項操作。如果車輛已停妥,程式碼會照常執行。下列程式碼片段說明如何使用 ParkedOnlyOnClickListener 在行動裝置上開啟設定畫面:

Kotlin

val row = Row.Builder()
    .setTitle("Open Settings")
    .setOnClickListener(ParkedOnlyOnClickListener.create(::openSettingsOnPhone))
    .build()

Java

Row row = new Row.Builder()
    .setTitle("Open Settings")
    .setOnClickListener(ParkedOnlyOnClickListener.create(this::openSettingsOnPhone))
    .build();

顯示通知

只有在通知使用 CarAppExtender 擴充功能時,系統才會將通知傳送到行動裝置,並顯示在車輛螢幕上。部分通知屬性 (例如內容標題、文字、圖示和動作) 可在 CarAppExtender 中設定,這樣就能在通知顯示在車輛螢幕上時覆寫通知屬性。

以下程式碼片段說明如何傳送通知至車輛螢幕,讓其顯示與行動裝置上不同的標題:

Kotlin

val notification = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
    .setContentTitle(titleOnThePhone)
    .extend(
        CarAppExtender.Builder()
            .setContentTitle(titleOnTheCar)
            ...
            .build())
    .build()

Java

Notification notification = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
    .setContentTitle(titleOnThePhone)
    .extend(
        new CarAppExtender.Builder()
            .setContentTitle(titleOnTheCar)
            ...
            .build())
    .build();

通知可能會影響使用者介面的以下部分:

  • 系統可能會向使用者顯示抬頭通知 (HUN)。
  • 您可以新增通知中心項目,並視需要在軌道中顯示徽章。
  • 對於導航應用程式,通知可能會顯示在邊欄小工具中,如即時路況通知所述。

您可以選擇如何設定應用程式通知,以便透過通知的優先順序影響每個使用者介面元素,如 CarAppExtender 說明文件所述。

如果使用 true 值呼叫 NotificationCompat.Builder.setOnlyAlertOnce,則重要性高的通知只會以抬頭通知的形式顯示一次。

如要進一步瞭解如何設計車輛應用程式的通知,請參閱「Google 行車設計」指南中的「通知」一節。

顯示浮動式訊息

應用程式可以使用 CarToast 顯示浮動式訊息,如以下程式碼片段所示:

Kotlin

CarToast.makeText(carContext, "Hello!", CarToast.LENGTH_SHORT).show()

Java

CarToast.makeText(getCarContext(), "Hello!", CarToast.LENGTH_SHORT).show();

要求權限

如果應用程式需要存取受限資料或操作 (例如位置資訊),則適用 Android 權限的標準規則。如要要求權限,您可以使用 CarContext.requestPermissions() 方法。

使用 CarContext.requestPermissions() 的優點是,與使用標準 Android API 相比,您不必啟動自己的 Activity 來建立權限對話方塊。此外,您可以在 Android Auto 和 Android Automotive OS 上使用相同的程式碼,而不需要建立依賴平台的流程。

設定 Android Auto 的權限對話方塊樣式

在 Android Auto 上,使用者的權限對話方塊會顯示在手機上。根據預設,對話方塊後方不會有背景。如要設定自訂背景,請在 AndroidManifest.xml 檔案中宣告 車用應用程式主題,並為車用應用程式主題設定 carPermissionActivityLayout 屬性。

<meta-data
    android:name="androidx.car.app.theme"
    android:resource="@style/MyCarAppTheme />

接著,為汽車應用程式主題設定 carPermissionActivityLayout 屬性:

<resources>
  <style name="MyCarAppTheme">
    <item name="carPermissionActivityLayout">@layout/my_custom_background</item>
  </style>
</resources>

使用意圖啟動車輛應用程式

您可以呼叫 CarContext.startCarApp 方法,執行下列任一動作:

  • 開啟撥號應用程式即可撥打電話。
  • 使用預設車用導航應用程式啟動即時路線導航功能,前往某個地點。
  • 使用意圖啟動您自己的應用程式。

以下範例說明如何建立通知,並透過動作開啟應用程式,顯示停車預訂的詳細資料。您可以使用內容意圖擴充通知例項,其中包含 PendingIntent,可將明確意圖包裝至應用程式的動作:

Kotlin

val notification = notificationBuilder
    ...
    .extend(
        CarAppExtender.Builder()
            .setContentIntent(
                PendingIntent.getBroadcast(
                    context,
                    ACTION_VIEW_PARKING_RESERVATION.hashCode(),
                    Intent(ACTION_VIEW_PARKING_RESERVATION)
                        .setComponent(ComponentName(context, MyNotificationReceiver::class.java)),
                    0))
            .build())

Java

Notification notification = notificationBuilder
    ...
    .extend(
        new CarAppExtender.Builder()
            .setContentIntent(
                PendingIntent.getBroadcast(
                    context,
                    ACTION_VIEW_PARKING_RESERVATION.hashCode(),
                    new Intent(ACTION_VIEW_PARKING_RESERVATION)
                        .setComponent(new ComponentName(context, MyNotificationReceiver.class)),
                    0))
            .build());

您的應用程式也必須宣告 BroadcastReceiver,當使用者在通知介面中選取動作,並以包含資料 URI 的意圖叫用 CarContext.startCarApp 時,系統會叫用該 BroadcastReceiver 來處理意圖:

Kotlin

class MyNotificationReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val intentAction = intent.action
        if (ACTION_VIEW_PARKING_RESERVATION == intentAction) {
            CarContext.startCarApp(
                intent,
                Intent(Intent.ACTION_VIEW)
                    .setComponent(ComponentName(context, MyCarAppService::class.java))
                    .setData(Uri.fromParts(MY_URI_SCHEME, MY_URI_HOST, intentAction)))
        }
    }
}

Java

public class MyNotificationReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        String intentAction = intent.getAction();
        if (ACTION_VIEW_PARKING_RESERVATION.equals(intentAction)) {
            CarContext.startCarApp(
                intent,
                new Intent(Intent.ACTION_VIEW)
                    .setComponent(new ComponentName(context, MyCarAppService.class))
                    .setData(Uri.fromParts(MY_URI_SCHEME, MY_URI_HOST, intentAction)));
        }
    }
}

最後,如果停車預訂畫面尚未置於頂端,應用程式中的 Session.onNewIntent 方法會透過在堆疊上推送停車預訂畫面來處理此意圖:

Kotlin

override fun onNewIntent(intent: Intent) {
    val screenManager = carContext.getCarService(ScreenManager::class.java)
    val uri = intent.data
    if (uri != null
        && MY_URI_SCHEME == uri.scheme
        && MY_URI_HOST == uri.schemeSpecificPart
        && ACTION_VIEW_PARKING_RESERVATION == uri.fragment
    ) {
        val top = screenManager.top
        if (top !is ParkingReservationScreen) {
            screenManager.push(ParkingReservationScreen(carContext))
        }
    }
}

Java

@Override
public void onNewIntent(@NonNull Intent intent) {
    ScreenManager screenManager = getCarContext().getCarService(ScreenManager.class);
    Uri uri = intent.getData();
    if (uri != null
        && MY_URI_SCHEME.equals(uri.getScheme())
        && MY_URI_HOST.equals(uri.getSchemeSpecificPart())
        && ACTION_VIEW_PARKING_RESERVATION.equals(uri.getFragment())
    ) {
        Screen top = screenManager.getTop();
        if (!(top instanceof ParkingReservationScreen)) {
            screenManager.push(new ParkingReservationScreen(getCarContext()));
        }
    }
}

如要進一步瞭解如何處理車用應用程式的通知,請參閱「顯示通知」一節。

範本限制

主辦單位可為特定工作指定的範本數量上限為五個,其中最後一個範本必須是下列類型之一:

請注意,這項限制適用於範本數量,而非堆疊中的 Screen 例項數量。舉例來說,如果應用程式在畫面 A 中傳送兩個範本,然後推送畫面 B,現在可以再傳送三個範本。或者,如果每個畫面的結構都是用來傳送單一範本,應用程式就可以將五個畫面執行個體推送至 ScreenManager 堆疊。

這些限制有特殊情況:範本重新整理、返回和重設作業。

範本重新整理

某些內容更新不會計入範本限制。一般來說,如果應用程式推送的範本類型相同,且包含與先前範本相同的主要內容,系統就不會將新範本計入配額。舉例來說,更新 ListTemplate 中某列的切換狀態不會計入配額。請參閱個別範本的說明文件,進一步瞭解哪些類型的內容更新可視為重新整理。

返回操作

為了在工作中啟用子流程,主機會偵測應用程式從 ScreenManager 堆疊彈出 Screen 的時間,並根據應用程式向後移動的範本數量更新剩餘配額。

舉例來說,如果應用程式在畫面 A 中傳送兩個範本,然後推送畫面 B 並傳送另外兩個範本,則應用程式剩餘一個配額。如果應用程式隨後彈回畫面 A,主機會將配額重設為三,因為應用程式已向後移動兩個範本。

請注意,當應用程式彈回畫面時,必須傳送與該畫面上次傳送的範本相同類型的範本。傳送任何其他範本類型都會導致錯誤。不過,只要在返回作業期間類型保持不變,應用程式就可以自由修改範本的內容,而不會影響配額。

重設作業

某些範本具有特殊語義,可表示工作結束。舉例來說,NavigationTemplate 是預期會停留在螢幕上的檢視畫面,並為使用者提供新的即時路線指示。當達到其中一個範本時,主機會重設範本配額,將該範本視為新任務的第一個步驟。這麼做可讓應用程式開始執行新工作。請參閱個別範本的說明文件,瞭解哪些範本會在主機上觸發重設作業。

如果主機從通知動作或啟動器收到意圖,以便啟動應用程式,配額也會重設。此機制可讓應用程式從通知開始新的工作流程,即使應用程式已綁定並處於前景,也能維持這項機制。

如要進一步瞭解如何在車輛螢幕上顯示應用程式的通知,請參閱「顯示通知」一節。如要瞭解如何透過通知動作啟動應用程式,請參閱「使用意圖啟動車用應用程式」一節。

Connection API

您可以使用 CarConnection API 在執行階段擷取連線資訊,判斷應用程式是否在 Android Auto 或 Android Automotive OS 上執行。

舉例來說,在車用應用程式的 Session 中,初始化 CarConnection 並訂閱 LiveData 更新:

Kotlin

CarConnection(carContext).type.observe(this, ::onConnectionStateUpdated)

Java

new CarConnection(getCarContext()).getType().observe(this, this::onConnectionStateUpdated);

接著,您可以在觀察器中對連線狀態變更做出反應:

Kotlin

fun onConnectionStateUpdated(connectionState: Int) {
  val message = when(connectionState) {
    CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> "Not connected to a head unit"
    CarConnection.CONNECTION_TYPE_NATIVE -> "Connected to Android Automotive OS"
    CarConnection.CONNECTION_TYPE_PROJECTION -> "Connected to Android Auto"
    else -> "Unknown car connection type"
  }
  CarToast.makeText(carContext, message, CarToast.LENGTH_SHORT).show()
}

Java

private void onConnectionStateUpdated(int connectionState) {
  String message;
  switch(connectionState) {
    case CarConnection.CONNECTION_TYPE_NOT_CONNECTED:
      message = "Not connected to a head unit";
      break;
    case CarConnection.CONNECTION_TYPE_NATIVE:
      message = "Connected to Android Automotive OS";
      break;
    case CarConnection.CONNECTION_TYPE_PROJECTION:
      message = "Connected to Android Auto";
      break;
    default:
      message = "Unknown car connection type";
      break;
  }
  CarToast.makeText(getCarContext(), message, CarToast.LENGTH_SHORT).show();
}

Constraints API

不同車輛可能會向使用者一次顯示不同數量的 Item 例項。使用 ConstraintManager 在執行階段檢查內容限制,並在範本中設定適當的項目數量。

首先,請從 CarContext 取得 ConstraintManager

Kotlin

val manager = carContext.getCarService(ConstraintManager::class.java)

Java

ConstraintManager manager = getCarContext().getCarService(ConstraintManager.class);

接著,您可以查詢擷取的 ConstraintManager 物件,瞭解相關內容限制。舉例來說,如要取得可在格線中顯示的項目數量,請使用 CONTENT_LIMIT_TYPE_GRID 呼叫 getContentLimit

Kotlin

val gridItemLimit = manager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_GRID)

Java

int gridItemLimit = manager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_GRID);

新增登入流程

如果您的應用程式為使用者提供登入體驗,您可以使用 SignInTemplateLongMessageTemplate 等範本 (搭配 Car App API 2 級以上版本),在車輛主機上處理應用程式的登入作業。

如要建立 SignInTemplate,請定義 SignInMethod。Car App 程式庫目前支援下列登入方式:

舉例來說,如要實作收集使用者密碼的範本,請先建立 InputCallback,以便處理及驗證使用者輸入內容:

Kotlin

val callback = object : InputCallback {
    override fun onInputSubmitted(text: String) {
        // You will receive this callback when the user presses Enter on the keyboard.
    }

    override fun onInputTextChanged(text: String) {
        // You will receive this callback as the user is typing. The update
        // frequency is determined by the host.
    }
}

Java

InputCallback callback = new InputCallback() {
    @Override
    public void onInputSubmitted(@NonNull String text) {
        // You will receive this callback when the user presses Enter on the keyboard.
    }

    @Override
    public void onInputTextChanged(@NonNull String text) {
        // You will receive this callback as the user is typing. The update
        // frequency is determined by the host.
    }
};

InputSignInMethod Builder 需要 InputCallback

Kotlin

val passwordInput = InputSignInMethod.Builder(callback)
    .setHint("Password")
    .setInputType(InputSignInMethod.INPUT_TYPE_PASSWORD)
    ...
    .build()

Java

InputSignInMethod passwordInput = new InputSignInMethod.Builder(callback)
    .setHint("Password")
    .setInputType(InputSignInMethod.INPUT_TYPE_PASSWORD)
    ...
    .build();

最後,使用新的 InputSignInMethod 建立 SignInTemplate

Kotlin

SignInTemplate.Builder(passwordInput)
    .setTitle("Sign in with username and password")
    .setInstructions("Enter your password")
    .setHeaderAction(Action.BACK)
    ...
    .build()

Java

new SignInTemplate.Builder(passwordInput)
    .setTitle("Sign in with username and password")
    .setInstructions("Enter your password")
    .setHeaderAction(Action.BACK)
    ...
    .build();

使用 AccountManager

設有驗證功能的 Android Automotive OS 應用程式都必須使用 AccountManager,原因如下:

  • 提供更優質的使用者體驗和簡便的帳戶管理功能:使用者可透過系統設定中的帳戶選單輕鬆管理所有帳戶 (包括登入及登出帳戶)。
  • 「訪客」體驗:由於車輛是共用裝置,因此原始設備製造商 (OEM) 可以在車輛上啟用訪客體驗,藉此禁止新增帳戶。

新增文字字串變化版本

不同車輛的螢幕尺寸可能會顯示不同數量的文字。使用 Car App API 級別 2 以上版本時,您可以指定多個文字字串變化版本,以便盡可能配合螢幕。如要查看哪些位置可接受文字變化版本,請找出使用 CarText 的範本和元件。

您可以使用 CarText.Builder.addVariant() 方法,將文字字串變化版本新增至 CarText

Kotlin

val itemTitle = CarText.Builder("This is a very long string")
    .addVariant("Shorter string")
    ...
    .build()

Java

CarText itemTitle = new CarText.Builder("This is a very long string")
    .addVariant("Shorter string")
    ...
    .build();

接著,您可以使用這個 CarText,例如做為 GridItem 的主要文字。

Kotlin

GridItem.Builder()
    .addTitle(itemTitle)
    ...
    .build()

Java

new GridItem.Builder()
    .addTitle(itemTitle)
    ...
    build();

請依照偏好順序新增字串,例如從最長到最短。主機會根據車輛螢幕的可用空間,選擇適當長度的字串。

為資料列新增內嵌 CarIcon

您可以使用 CarIconSpan 在文字中加入圖示,為應用程式增添視覺吸引力。如要進一步瞭解如何建立這些區間,請參閱 CarIconSpan.create 的說明文件。請參閱「使用 Span 的 Spantastic 文字樣式」,瞭解使用 Span 的文字樣式運作方式。

Kotlin

  
val rating = SpannableString("Rating: 4.5 stars")
rating.setSpan(
    CarIconSpan.create(
        // Create a CarIcon with an image of four and a half stars
        CarIcon.Builder(...).build(),
        // Align the CarIcon to the baseline of the text
        CarIconSpan.ALIGN_BASELINE
    ),
    // The start index of the span (index of the character '4')
    8,
    // The end index of the span (index of the last 's' in "stars")
    16,
    Spanned.SPAN_INCLUSIVE_INCLUSIVE
)

val row = Row.Builder()
    ...
    .addText(rating)
    .build()
  
  

Java

  
SpannableString rating = new SpannableString("Rating: 4.5 stars");
rating.setSpan(
        CarIconSpan.create(
                // Create a CarIcon with an image of four and a half stars
                new CarIcon.Builder(...).build(),
                // Align the CarIcon to the baseline of the text
                CarIconSpan.ALIGN_BASELINE
        ),
        // The start index of the span (index of the character '4')
        8,
        // The end index of the span (index of the last 's' in "stars")
        16,
        Spanned.SPAN_INCLUSIVE_INCLUSIVE
);
Row row = new Row.Builder()
        ...
        .addText(rating)
        .build();
  
  

汽車硬體 API

從 Car App API 級別 3 開始,Car App Library 提供可用於存取車輛屬性和感應器的 API。

需求條件

如要搭配 Android Auto 使用 API,請先在 Android Auto 模組的 build.gradle 檔案中,新增 androidx.car.app:app-projected 的依附元件。針對 Android Automotive OS,請在 Android Automotive OS 模組的 build.gradle 檔案中,新增對 androidx.car.app:app-automotive 的依附元件。

此外,您必須在 AndroidManifest.xml 檔案中宣告相關權限,才能要求所需的車輛資料。請注意,使用者也必須授予這些權限給您。您可以在 Android Auto 和 Android Automotive OS 上使用相同的程式碼,而不需要建立依平台而異的流程。不過,所需的權限不同。

CarInfo

下表說明 CarInfo API 顯示的屬性,以及您需要申請的權限:

方法 屬性 Android Auto 權限 Android Automotive OS 權限 開始支援的 Car App API 級別
fetchModel 廠牌、型號、年份 android.car.permission.CAR_INFO 3
fetchEnergyProfile 電動車連接器類型、燃料類型 com.google.android.gms.permission.CAR_FUEL android.car.permission.CAR_INFO 3
fetchExteriorDimensions

這項資料僅適用於部分搭載 API 30 以上版本 Android Automotive OS 的車輛

外部尺寸 android.car.permission.CAR_INFO 7
addTollListener
removeTollListener
付費卡狀態、付費卡類型 3
addEnergyLevelListener
removeEnergyLevelListener
電池電量、油量、油量不足、剩餘行駛里程 com.google.android.gms.permission.CAR_FUEL android.car.permission.CAR_ENERGY,
android.car.permission.CAR_ENERGY_PORTS,
android.car.permission.READ_CAR_DISPLAY_UNITS
3
addSpeedListener
removeSpeedListener
原始車速、顯示車速 (顯示在車輛儀表板上) com.google.android.gms.permission.CAR_SPEED android.car.permission.CAR_SPEED
android.car.permission.READ_CAR_DISPLAY_UNITS
3
addMileageListener
removeMileageListener
里程表距離 com.google.android.gms.permission.CAR_MILEAGE 在 Android Automotive OS 上,從 Play 商店安裝的應用程式無法取得這類資料。 3

舉例來說,如要取得剩餘範圍,請將 CarInfo 物件例項化,然後建立並註冊 OnCarDataAvailableListener

Kotlin

val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo

val listener = OnCarDataAvailableListener<EnergyLevel> { data ->
    if (data.rangeRemainingMeters.status == CarValue.STATUS_SUCCESS) {
      val rangeRemaining = data.rangeRemainingMeters.value
    } else {
      // Handle error
    }
  }

carInfo.addEnergyLevelListener(carContext.mainExecutor, listener)

// Unregister the listener when you no longer need updates
carInfo.removeEnergyLevelListener(listener)

Java

CarInfo carInfo = getCarContext().getCarService(CarHardwareManager.class).getCarInfo();

OnCarDataAvailableListener<EnergyLevel> listener = (data) -> {
  if(data.getRangeRemainingMeters().getStatus() == CarValue.STATUS_SUCCESS) {
    float rangeRemaining = data.getRangeRemainingMeters().getValue();
  } else {
    // Handle error
  }
};

carInfo.addEnergyLevelListener(getCarContext().getMainExecutor(), listener);

// Unregister the listener when you no longer need updates
carInfo.removeEnergyLevelListener(listener);

請勿假設車輛的資料隨時可用。如果您收到錯誤訊息,請檢查所要求值的狀態,進一步瞭解為何無法擷取所要求的資料。如需完整的 CarInfo 類別定義,請參閱參考說明文件

CarSensors

CarSensors 類別可讓您存取車輛的加速計、陀螺儀、指南針和位置資料。這些值是否可用取決於原始設備製造商 (OEM)。加速計、陀螺儀和指南針的資料格式,與您從 SensorManager API 取得的資料格式相同。舉例來說,如要檢查車輛的方向:

Kotlin

val carSensors = carContext.getCarService(CarHardwareManager::class.java).carSensors

val listener = OnCarDataAvailableListener<Compass> { data ->
    if (data.orientations.status == CarValue.STATUS_SUCCESS) {
      val orientation = data.orientations.value
    } else {
      // Data not available, handle error
    }
  }

carSensors.addCompassListener(CarSensors.UPDATE_RATE_NORMAL, carContext.mainExecutor, listener)

// Unregister the listener when you no longer need updates
carSensors.removeCompassListener(listener)

Java

CarSensors carSensors = getCarContext().getCarService(CarHardwareManager.class).getCarSensors();

OnCarDataAvailableListener<Compass> listener = (data) -> {
  if (data.getOrientations().getStatus() == CarValue.STATUS_SUCCESS) {
    List<Float> orientations = data.getOrientations().getValue();
  } else {
    // Data not available, handle error
  }
};

carSensors.addCompassListener(CarSensors.UPDATE_RATE_NORMAL, getCarContext().getMainExecutor(),
    listener);

// Unregister the listener when you no longer need updates
carSensors.removeCompassListener(listener);

如要存取車輛的位置資料,您也需要宣告並要求 android.permission.ACCESS_FINE_LOCATION 權限。

測試

如要在 Android Auto 上測試時模擬感應器資料,請參閱電腦版車用運算主機指南的「感應器」和「感應器設定」章節。如要在 Android Automotive OS 上進行測試時模擬感應器資料,請參閱 Android Automotive OS 模擬器指南中的「模擬硬體狀態」一節。

CarAppService、工作階段和畫面生命週期

SessionScreen 類別實作 LifecycleOwner 介面。當使用者與應用程式互動時,系統會叫用 SessionScreen 物件的生命週期回呼,如以下圖表所述。

CarAppService 和工作階段的生命週期

圖 1. Session 生命週期。

如需完整詳細資料,請參閱 Session.getLifecycle 方法的說明文件。

畫面的生命週期

圖 2. Screen 生命週期。

如需完整詳細資料,請參閱 Screen.getLifecycle 方法的說明文件。

使用車輛麥克風錄音

您可以使用應用程式的 CarAppServiceCarAudioRecord API,讓應用程式存取使用者的車輛麥克風。使用者必須授予應用程式存取車輛麥克風的權限。您的應用程式可在應用程式中記錄及處理使用者的輸入內容。

錄音權限

錄製任何音訊之前,您必須先在 AndroidManifest.xml 中宣告錄音權限,並要求使用者授予權限。

<manifest ...>
   ...
   <uses-permission android:name="android.permission.RECORD_AUDIO" />
   ...
</manifest>

您必須在執行階段要求錄音權限。如要進一步瞭解如何在車用應用程式中要求權限,請參閱「要求權限」一節。

錄音

使用者授予錄音權限後,您就可以錄製音訊並處理錄音內容。

Kotlin

val carAudioRecord = CarAudioRecord.create(carContext)
        carAudioRecord.startRecording()

        val data = ByteArray(CarAudioRecord.AUDIO_CONTENT_BUFFER_SIZE)
        while(carAudioRecord.read(data, 0, CarAudioRecord.AUDIO_CONTENT_BUFFER_SIZE) >= 0) {
            // Use data array
            // Potentially call carAudioRecord.stopRecording() if your processing finds end of speech
        }
        carAudioRecord.stopRecording()
 

Java

CarAudioRecord carAudioRecord = CarAudioRecord.create(getCarContext());
        carAudioRecord.startRecording();

        byte[] data = new byte[CarAudioRecord.AUDIO_CONTENT_BUFFER_SIZE];
        while (carAudioRecord.read(data, 0, CarAudioRecord.AUDIO_CONTENT_BUFFER_SIZE) >= 0) {
            // Use data array
            // Potentially call carAudioRecord.stopRecording() if your processing finds end of speech
        }
        carAudioRecord.stopRecording();
 

音訊焦點

透過車輛麥克風錄音時,請先取得音訊焦點,確保所有正在播放的媒體都已停止。如果失去音訊焦點,請停止錄製。

以下是如何取得音訊焦點的範例:

Kotlin

 
val carAudioRecord = CarAudioRecord.create(carContext)
        
        // Take audio focus so that user's media is not recorded
        val audioAttributes = AudioAttributes.Builder()
            .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
            // Use the most appropriate usage type for your use case
            .setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
            .build()
        
        val audioFocusRequest =
            AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
                .setAudioAttributes(audioAttributes)
                .setOnAudioFocusChangeListener { state: Int ->
                    if (state == AudioManager.AUDIOFOCUS_LOSS) {
                        // Stop recording if audio focus is lost
                        carAudioRecord.stopRecording()
                    }
                }
                .build()
        
        if (carContext.getSystemService(AudioManager::class.java)
                .requestAudioFocus(audioFocusRequest)
            != AudioManager.AUDIOFOCUS_REQUEST_GRANTED
        ) {
            // Don't record if the focus isn't granted
            return
        }
        
        carAudioRecord.startRecording()
        // Process the audio and abandon the AudioFocusRequest when done

Java

CarAudioRecord carAudioRecord = CarAudioRecord.create(getCarContext());
        // Take audio focus so that user's media is not recorded
        AudioAttributes audioAttributes =
                new AudioAttributes.Builder()
                        .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
                        // Use the most appropriate usage type for your use case
                        .setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
                        .build();

        AudioFocusRequest audioFocusRequest =
                new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
                        .setAudioAttributes(audioAttributes)
                        .setOnAudioFocusChangeListener(state -> {
                            if (state == AudioManager.AUDIOFOCUS_LOSS) {
                                // Stop recording if audio focus is lost
                                carAudioRecord.stopRecording();
                            }
                        })
                        .build();

        if (getCarContext().getSystemService(AudioManager.class).requestAudioFocus(audioFocusRequest)
                != AUDIOFOCUS_REQUEST_GRANTED) {
            // Don't record if the focus isn't granted
            return;
        }

        carAudioRecord.startRecording();
        // Process the audio and abandon the AudioFocusRequest when done
 

測試程式庫

Android for Cars Testing Library 提供輔助類別,可用於在測試環境中驗證應用程式的行為。舉例來說,SessionController 可讓您模擬與主機的連線,並驗證是否已建立及傳回正確的 ScreenTemplate

如需使用範例,請參閱「範例」。

回報 Android 車輛專用應用程式程式庫相關問題

如果您發現程式庫有問題,請使用 Google Issue Tracker 回報。在問題範本中,請務必填寫所有必要資訊。

建立新問題

提交新問題之前,請先查看程式庫的版本資訊中是否列出了該問題,或是在問題清單中列出了該問題。您可以在追蹤程式中按一下該問題的星號,訂閱該問題並投下一票。詳情請參閱訂閱問題一文。