Learn Car App Library fundamentals

1. Before you begin

In this codelab you learn how to build distraction-optimized apps for Android Auto and Android Automotive OS using the Android for Cars App Library. You first add support for Android Auto and then, with minimal additional work, create a variant of the app that can run on Android Automotive OS. After getting the app running on both platforms, you build out an additional screen and some basic interactivity!

What this isn't

What you'll need

What you'll build

Android Auto

Android Automotive OS

A screen recording showing the app running on Android Auto using the Desktop Head Unit.

A screen recording showing the app running on an Android Automotive OS emulator.

What you'll learn

  • How the Car App Library's client-host architecture works.
  • How to write your own CarAppService, Session, and Screen classes.
  • How to share your implementation across both Android Auto and Android Automotive OS.
  • How to run Android Auto on your development machine using the Desktop Head Unit.
  • How to run the Android Automotive OS emulator.

2. Get set up

Get the code

  1. The code for this codelab can be found in the car-app-library-fundamentals directory within the car-codelabs GitHub repository. To clone it, run the following command:
git clone https://github.com/android/car-codelabs.git
  1. Alternatively, you can download the repository as a ZIP file:

Open the project

  • After starting Android Studio, import the project, selecting just the car-app-library-fundamentals/start directory. The car-app-library-fundamentals/end directory contains the solution code, which you can reference at any point if you get stuck or just want to see the full project.

Familiarize yourself with the code

  • After opening the project in Android studio, take some time to look through the starting code.

Notice that the starter code for the app is broken up into two modules, :app and :common:data.

The :app module depends on the :common:data module.

The :app module contains the mobile app's UI and logic, and the :common:data module contains the Place model data class and the repository used to read Place models. For simplicity's sake, the repository reads from a hardcoded list but could easily read from a database or a backend server in a real app.

The :app module includes a dependency on the :common:data module so that it can read and present the list of Place models.

3. Learn about the Android for Cars App Library

The Android for Cars App Library is a set of Jetpack libraries that enable developers to build apps for use in vehicles. It provides a templated framework that provides driving-optimized user interfaces while also taking care of adapting to the various hardware configurations present in cars (for example, input methods, screen sizes, and aspect ratios). Together, this makes it easy for you as a developer to build an app and have confidence that it will perform well on a variety of vehicles running both Android Auto and Android Automotive OS.

Learn how it works

Apps built using the Car App Library don't run directly on Android Auto or Android Automotive OS. Instead, they rely on a host app that communicates with client apps and renders the client's user interfaces on their behalf. Android Auto itself is a host and the Google Automotive App Host is the host used on Android Automotive OS vehicles with Google built-in. The following are the key classes of the Car App Library you must extend when building your app:

CarAppService

CarAppService is a subclass of Android's Service class and serves as the entry point for host applications to communicate with client apps (such as the one you build in this codelab). Its main purpose is to create Session instances that the host app interacts with.

Session

You can think of a Session as an instance of a client app running on a display in the vehicle. Like other Android components, it has a life cycle of its own that can be used to initialize and tear down resources used throughout the Session instance's existence. There is a one-to-many relationship between CarAppService and Session. For example, one CarAppService may have two Session instances, one for a primary display and another for a cluster display for navigation apps that support cluster screens.

Screen

Screen instances are responsible for generating the user interfaces rendered by host apps. These user interfaces are represented by Template classes that each model a specific type of layout, such as a grid or list. Each Session manages a stack of Screen instances that handle user flows through the different parts of your app. As with a Session, a Screen has a lifecycle of its own that you can hook into.

A diagram of how the Car App Library works. On the left side are two boxes titled Display. In the center, there is a box titled Host. On the right, there is a box titled CarAppService. Within the CarAppService box, there are two boxes, each titled Session. Within the first Session, there are three Screen boxes on top of each other. Within the second Session, there are two Screen boxes on top of each other. There are arrows between the each of the Displays and the host as well as between the host and the Sessions to indicate how the host manages communication between all of the different components.

You write a CarAppService, Session, and Screen in the Write your CarAppService section of this codelab, so don't worry if things don't quite click yet.

4. Set up your initial configuration

To begin, set up the module that contains the CarAppService and declare its dependencies.

Create the car-app-service module

  1. With the :common module selected in the Project window, right click and choose the New > Module option.
  2. In the module wizard that opens up, select the Android Library template (so this module can be used as a dependency by other modules) in the list on the left side, and then use the following values:
  • Module name: :common:car-app-service
  • Package name: com.example.places.carappservice
  • Minimum SDK: API 23: Android 6.0 (Marshmallow)

Create New Module wizard with the values set as described in this step.

Set up dependencies

  1. In the project-level build.gradle file, add a variable declaration for the Car App Library version as follows. This allows you to easily use the same version across each of the modules in the app.

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. Next, add two dependencies to the :common:car-app-service module's build.gradle file.
  • androidx.car.app:app. This is the primary artifact of the Car App Library and includes all of the core classes used when building apps. There are three other artifacts that make up the library, androidx.car.app:app-projected for Android Auto-specific functionality, androidx.car.app:app-automotive for Android Automotive OS functionality code, and androidx.car.app:app-testing for some helpers useful for unit testing. You make use of app-projected and app-automotive later in the codelab.
  • :common:data. This is the same data module used by the existing mobile app and allows the same data source to be used for every version of the app experience.

build.gradle (Module :common:car-app-service)

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

With this change, the dependency graph for the app's own modules is the following:

The :app and :common:car-app-service modules both depend on the :common:data module.

Now that dependencies are set up, it's time to write the CarAppService!

5. Write your CarAppService

  1. Start by creating a file named PlacesCarAppService.kt in the carappservice package within the :common:car-app-service module.
  2. Within this file, create a class named PlacesCarAppService, which extends 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()
    }
}

The CarAppService abstract class implements Service methods such as onBind and onUnbind for you and prevents further overrides of those methods to ensure proper interoperability with host applications. All you have to do is implement createHostValidator and onCreateSession.

The HostValidator you return from createHostValidator is referenced when your CarAppService is being bound in order to ensure that the host is trusted and that binding fails if the host doesn't match the parameters you define. For the purposes of this codelab (and testing in general), the ALLOW_ALL_HOSTS_VALIDATOR makes it easy to ensure your app connects but shouldn't be used in production. See the documentation for createHostValidator for more on how to configure this for a production app.

For an app as simple as this, onCreateSession can simply return an instance of a Session. In a more complicated app, this would be a good place to initialize long-lived resources such as metrics and logging clients that are used while your app is running on the vehicle.

  1. Finally, you need to add the <service> element that corresponds to the PlacesCarAppService in the :common:car-app-service module's AndroidManifest.xml file in order to let the operating system (and, by extension, other apps such as hosts) know it exists.

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>

There are two important pieces to note here:

  • The <action> element allows host (and launcher) applications to find the app.
  • The <category> element declares the app's category, which determines what app quality criteria it must meet (more details on that later). Other possible values are androidx.car.app.category.NAVIGATION and androidx.car.app.category.IOT.

Create the PlacesSession class

  • Create a PlacesCarAppService.kt file and add the following code:

PlacesCarAppService.kt

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

For a simple app like this one, you can just return the main screen in onCreateScreen. However, since this method takes an Intent as a parameter, a more feature-rich app might also read from it and populate a back stack of screens or use some other conditional logic.

Create the MainScreen class

Next, create a new package named screen.

  1. Right click on the com.example.places.carappservice package and select New > Package (the full package name for it will be com.example.places.carappservice.screen). This is where you put all of the Screen subclasses for the app.
  2. In the screen package, create a file named MainScreen.kt to contain the MainScreen class, which extends Screen. For now, it shows a simple Hello, world! message using the PaneTemplate.

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. Add support for Android Auto

Though you've now implemented all of the logic needed to get the app up and running, there are two more pieces of configuration to set up before you can run it on Android Auto.

Add a dependency on the car-app-service module

In the :app module's build.gradle file, add the following:

build.gradle (Module :app)

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

With this change, the dependency graph for the app's own modules is the following:

The :app and :common:car-app-service modules both depend on the :common:data module. The :app module also depends on the :common:car-app-service module.

This bundles the code you just wrote in the :common:car-app-service module along with other components included in the Car App Library, such as the provided permission granting activity.

Declare the com.google.android.gms.car.application meta-data

  1. Right click the :common:car-app-service module and select the New > Android Resource File option, and then override the following values:
  • File name: automotive_app_desc.xml
  • Resource type: XML
  • Root element: automotiveApp

New Resource File wizard with the values set as described in this step.

  1. Within that file, add the following <uses> element in order to declare that your app uses the templates provided by the Car App Library.

automotive_app_desc.xml

<?xml version="1.0" encoding="utf-8"?>
<automotiveApp>
    <uses name="template"/>
</automotiveApp>
  1. In the :app module's AndroidManifest.xml file, add the following <meta-data> element that references the automotive_app_desc.xml file you just created.

AndroidManifest.xml (:app)

<application ...>

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

    ...

</application>

This file is read by Android Auto and lets it know which capabilities your app supports–in this case, that it uses the Car App Library's templating system. This information is then used to handle behavior such as adding the app to the Android Auto launcher and opening it from notifications.

Optional: Listen for projection changes

Sometimes, you want to know whether or not a user's device is connected to a car. You can accomplish this by using the CarConnection API, which provides a LiveData that lets you observe the connection state.

  1. To use the CarConnection API, first add a dependency in the :app module on the androidx.car.app:app artifact.

build.gradle (Module :app)

dependencies {
    ...
    implementation "androidx.car.app:app:$car_app_library_version"
    ...
}
  1. For demonstration purposes, you can then create a simple Composable such as the following that displays the current connection state. In a real app, this state might be captured in some logging, used to disable some functionality on the phone screen while projecting, or something else.

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. Now that there's a way to display the data, read it and pass it into the Composable, as demonstrated in the following snippet.

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. If you run the app, it should say Not projecting.

There is now an additional line of text on the screen for the projection state that says 'Not projecting'

7. Test with the Desktop Head Unit (DHU)

With the CarAppService implemented and the Android Auto configuration in place, it's time to run the app and see how it looks.

  1. Install the app on your phone and then follow the instructions on installing and running the DHU.

With the DHU up and running, you should see the app icon in the launcher (if not, double check that you've followed all the steps in the previous section, and then quit and restart the DHU from the terminal).

  1. Open the app from the launcher

The Android Auto launcher showing the app grid, including the Places app.

Uh-oh–it crashed!

There is an error screen with the message 'Android Auto has encountered an unexpected error'. There is a debug toggle in the upper right corner of the screen.

  1. To see why the app crashed, you can either toggle the debug icon in the top right corner (only visible when running on the DHU) or check the Logcat in Android Studio.

The same error screen as the previous figure, but now with the debug toggle enabled. A stack trace is displayed on the screen.

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

From the log, you can see that there is a missing declaration in the manifest for the minimum API level that the app supports. Before adding that entry, it's best to understand why it's necessary.

Like Android itself, the Car App Library also has a concept of API levels since there needs to be a contract between host and client applications in order for them to communicate. Host applications support a given API level and its associated features (and, for backward compatibility, those from earlier levels as well). For example, the SignInTemplate can be used on hosts running API level 2 or above. But, if you tried to use it on a host that only supports API level 1, that host wouldn't know about the template type and wouldn't be able to do anything meaningful with it.

During the process of binding host to client, there must be some amount of overlap in supported API levels in order for the binding to succeed. For example, if a host only supports API level 1, but a client app cannot run without features from API level 2 (as indicated by this manifest declaration), the apps shouldn't connect because the client wouldn't be able to run successfully on the host. Thus, the minimum required API level must be declared by the client in its manifest in order to ensure that only a host that can support it is bound to it.

  1. To set the minimum supported API level, add the following <meta-data> element in the :common:car-app-service module's AndroidManfiest.xml file:

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. Install the app again and launch it on the DHU, and then you should see the following:

The app shows a basic 'Hello, world' screen

For the sake of completeness, you can also try setting the minCarApiLevel to a large value (e.g. 100) to see what happens when you try to start the app if the host and client aren't compatible (hint: it crashes, similar to when no value is set).

It's also important to note that, as with Android itself, you can use features from an API higher than the declared minimum if you verify at runtime that the host supports the required level.

Optional: Listen for projection changes

  • If you added the CarConnection listener in the previous step, you should see the state update on your phone when the DHU is running, as seen below:

The line of text displaying the projection state now says 'Projecting' since the phone is connected to the DHU.

8. Add support for Android Automotive OS

With Android Auto up and running, it's time to go the extra mile and support Android Automotive OS as well.

Create the :automotive module

  1. To create a module that contains the code specific to the Android Automotive OS version of the app, open File > New > New Module... in Android Studio, select the Automotive option from the list of template types on the left, and then use the following values:
  • Application/Library name: Places (the same as the main app, but you could also choose a different name if desired)
  • Module name: automotive
  • Package name: com.example.places.automotive
  • Language: Kotlin
  • Minimum SDK: API 29: Android 10.0 (Q)—as mentioned earlier when creating the :common:car-app-service module, all Android Automotive OS vehicles that support Car App Library apps run at least API 29.

The Create New Module wizard for the Android Automotive OS module showing the values listed in this step.

  1. Click Next, then select No Activity on the next screen before finally clicking Finish.

The second page of the Create New Module wizard. Three options are shown, 'No Activity', 'Media Service', and 'Messaging Service'. The 'No Activity' option is selected.

Add dependencies

Just like with Android Auto, you need to declare a dependency on the :common:car-app-service module. By doing so, you get to share your implementation across both platforms!

Additionally, you need to add a dependency on the androidx.car.app:app-automotive artifact. Unlike the androidx.car.app:app-projected artifact, which is optional for Android Auto, this dependency is required on Android Automotive OS since it includes the CarAppActivity used to run your app.

  1. To add dependencies, open the build.gradle file, and then insert the following code:

build.gradle (Module :automotive)

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

With this change, the dependency graph for the app's own modules is the following:

The :app and :common:car-app-service modules both depend on the :common:data module. The :app and :automotive modules depend on the :common:car-app-service module.

Set up the manifest

  1. First, you need to declare two features, android.hardware.type.automotive and android.software.car.templates_host, as required.

android.hardware.type.automotive is a system feature that indicates that the device itself is a vehicle (see FEATURE_AUTOMOTIVE for more details). Only apps that mark this feature as required can be submitted to an Automotive OS track on the Play Console (and apps submitted to other tracks cannot require this feature). android.software.car.templates_host is a system feature only present in vehicles that have the template host required to run template apps.

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. Next, you need to declare some features as not being required.

This is to ensure that your app is compatible with the range of hardware available in cars with Google built-in. For example, if your app requires the android.hardware.screen.portrait feature, it's not compatible with vehicles with landscape screens since orientation is fixed in most vehicles. This is why the android:required attribute is set to false for these features.

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. Next, you need to add a reference to the automotive_app_desc.xml file as you did for Android Auto.

Note that this time the android:name attribute is different from before–instead of com.google.android.gms.car.application, it is com.android.automotive. Like before, this references the automotive_app_desc.xml file in the :common:car-app-service module, meaning that the same resource is used across both Android Auto and Android Automotive OS. Note that the <meta-data> element is within the <application> element (so you have to change the application tag from being self-closing)!

AndroidManifest.xml (:automotive)

<application>
    ...
    <meta-data android:name="com.android.automotive"
        android:resource="@xml/automotive_app_desc"/>
    ...
</application>
  1. Finally, you need to add an <activity> element for the CarAppActivity that's included in the library.

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>

And here's what all of that does:

  • android:name lists the fully qualified class name of the CarAppActivity class from the app-automotive package.
  • android:exported is set to true since this Activity must be launchable by an app other than itself (the launcher).
  • android:launchMode is set to singleTask so there can only be one instance of the CarAppActivity at a time.
  • android:theme is set to @android:style/Theme.DeviceDefault.NoActionBar so that the app takes up the full screen space available to it.
  • The intent filter indicates that this is the launcher Activity for the app.
  • There is a <meta-data> element that indicates to the system that the app can be used while UX restrictions are in place, such as when the vehicle is in motion.

Optional: copy the launcher icons from the :app module

Since you just created the :automotive module, it has the default green Android logo icons.

  • If you want, copy and paste the mipmap resource directory from the :app module into the :automotive module to use the same launcher icons as the mobile app!

9. Test with the Android Automotive OS emulator

Install the Automotive with Play Store System images

  1. First, open the SDK Manager in Android Studio and select the SDK Platforms tab if it is not already selected. In the bottom-right corner of the SDK Manager window, make sure that the Show package details box is checked.
  2. Install one or more of the following emulator images. Images can only run on machines with the same architecture (x86/ARM) as themselves.
  • 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

Create an Android Automotive OS Android Virtual Device

  1. After opening the Device Manager, select Automotive under the Category column on the left side of the window. Then, select the Automotive (1024p landscape) device definition from the list and click Next.

The Virtual Device Configuration wizard showing the 'Automotive (1024p landscape)' hardware profile selected.

  1. On the next page, select a system image from the previous step (if you chose the Android 11/API 30 image, it may be under the x86 Images tab and not the default Recommended tab). Click Next and select any advanced options you want before finally creating the AVD by clicking Finish.

Run the app

  1. Run the app on the emulator you just created using the automotive run configuration.

The

When you first run the app, you might see a screen like the following:

The app displays a screen saying 'System update required' with a button that says 'Check for updates' below it.

If that's the case, click the Check for updates button, which takes you to the Play Store page for the Google Automotive App Host app, where you should click the Install button. If you aren't signed in when you click the Check for updates button, you are taken through the sign-in flow. Once signed in, you can open the app again in order to click the button and go back to the Play Store page.

The Google Automotive App Host Play Store page - there is an 'Install' button in the upper right corner.

  1. Finally, with the host installed, open the app from the launcher (the nine-dot grid icon in the bottom row) again and you should see the following:

The app shows a basic 'Hello, world' screen

In the next step, you make changes in the :common:car-app-service module to display the list of places and to allow the user to start navigation to a chosen location in another app.

10. Add a map and detail screen

Add a map to the main screen

  1. To begin, replace the code in the MainScreen class's onGetTemplate method with the following:

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

This code reads the list of Place instances from the PlacesRepository and then converts each of them into a Row to be added to the ItemList displayed by the PlaceListMapTemplate.

  1. Run the app again (on either or both platforms) to see the result!

Android Auto

Android Automotive OS

Another stack trace is shown due to an error

The app just crashes and the user is taken back to the launcher after opening it.

Uh-oh, another error–it looks like there's a permission missing.

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. To fix the error, add the following <uses-permission> element in the :common:car-app-service module's manifest.

This permission must be declared by any app that uses the PlaceListMapTemplate or the app crashes as just demonstrated. Note that only apps which declare their category as androidx.car.app.category.POI can use this template and, in turn, this permission.

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>

If you run the app after adding the permission, it should look like the following on each platform:

Android Auto

Android Automotive OS

A list of locations is shown on the left side of the screen and a map with pins corresponding to the locations is shown behind it, filling the rest of the screen.

A list of locations is shown on the left side of the screen and a map with pins corresponding to the locations is shown behind it, filling the rest of the screen.

Rendering the map is handled for you by the application host when you provide the necessary Metadata!

Add a detail screen

Next, it's time to add a detail screen to let users see more information about a specific location and have the option to either navigate to that location using their preferred navigation app or return to the list of other places. This can be done using the PaneTemplate, which lets you display up to four rows of information next to optional action buttons.

  1. First, right click the res directory in the :common:car-app-service module and click New > Vector Asset, and then create a navigation icon using the following configuration:
  • Asset type: Clip art
  • Clip art: navigation
  • Name: baseline_navigation_24
  • Size: 24dp by 24dp
  • Color: #000000
  • Opacity: 100%

Asset Studio wizard showing the inputs mentioned in this step

  1. Then, in the screen package, create a file named DetailScreen.kt (next to the existing MainScreen.kt file) and add the following code:

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

Pay special attention to how the navigateAction is built—the call to startCarApp in its OnClickListener is the key to interacting with other apps on Android Auto and Android Automotive OS.

Now that there are two types of screens, it's time to add navigation between them! Navigation in the Car App Library uses a stack model of pushing and popping that's ideal for the simple task flows suitable for completion while driving.

A diagram representation the way in-app navigation works with the Car App Library. On the left, there is a stack with just a MainScreen. Between it and the center stack is an arrow labeled 'Push DetailScreen'. The center stack has a DetailScreen on top of the existing MainScreen. Between the center stack and the right stack there is an arrow labeled 'Pop'. The right stack is the same as the left one, just a MainScreen.

  1. To navigate from one of the list items on the MainScreen to a DetailScreen for that item, add the following code:

MainScreen.kt

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

Navigating back from a DetailScreen to the MainScreen is already handled since setHeaderAction(Action.BACK) is called when building the PaneTemplate displayed on the DetailScreen. When the header action is clicked by a user, the host handles popping the current screen off of the stack for you, but this behavior can be overridden by your app if desired.

  1. Run the app now to see the DetailScreen and in-app navigation in action!

11. Update the content on a screen

Often, you want to let a user interact with a screen and change the state of elements on that screen. To demonstrate how to do this, you build out functionality to let users toggle between favoriting and unfavoriting a place from the DetailScreen.

  1. First, add a local variable, isFavorite that holds the state. In a real app, this should be stored as part of the data layer, but a local variable is sufficient for demonstration purposes.

DetailScreen.kt

class DetailScreen(carContext: CarContext, private val placeId: Int) : Screen(carContext) {
    private var isFavorite = false
    ...
}
  1. Next, right click the res directory in the :common:car-app-service module and click New > Vector Asset, and then create a favorite icon using the following configuration:
  • Asset type: Clip art
  • Name: baseline_favorite_24
  • Clip art: favorite
  • Size: 24dp by 24dp
  • Color: #000000
  • Opacity: 100%

Asset Studio wizard showing the inputs mentioned in this step

  1. Then, in DetailsScreen.kt, create an ActionStrip for the PaneTemplate.

ActionStrip UI components are placed in the header row opposite the title and are ideal for secondary and tertiary actions. Since navigating is the primary action to be taken on the DetailScreen, placing the Action for favoriting or unfavoriting in an ActionStrip is a great way to structure the screen.

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

...

There are two pieces of interest here:

  • The CarIcon is tinted depending on the state of the item.
  • setOnClickListener is used to react to inputs from the user and toggle the favorite state.
  1. Don't forget to call setActionStrip on the PaneTemplate.Builder in order to use it!

DetailScreen.kt

return PaneTemplate.Builder(...)
    ...
    .setActionStrip(actionStrip)
    .build()
  1. Now, run the app and see what happens:

The DetailScreen is shown. The user is tapping on the favorite icon, but it isn't changing color as expected.

Interesting... it looks like the clicks are happening but the UI isn't updating.

This is because the Car App Library has a concept of refreshes. To limit driver distraction, refreshing content on the screen has certain limitations (which vary by the template being displayed), and each refresh must be explicitly requested by your own code by calling the Screen class' invalidate method. Just updating some state that is referenced in onGetTemplate is not enough to update the UI.

  1. To fix this issue, update the OnClickListener as follows:

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. Run the app again to see that the color of the heart icon should update on each click!

The DetailScreen is shown. The user is tapping on the favorite icon and it is now changing color as expected.

And just like that, you have a basic app that's well integrated with both Android Auto and Android Automotive OS!

12. Congratulations

You successfully built your first Car App Library app. Now it's time to take what you learned and apply it to your own app!

As mentioned earlier, only certain categories built using the Car App Library apps can be submitted to the Play Store at this time. If your app is a navigation app, point-of-interest (POI) app (like the one you worked on in this codelab), or an internet-of-things (IOT) app, you can start building today and release your app all the way to production on both platforms.

New app categories are being added every year, so even if you can't immediately apply what you learned, check back later and the time might be right to extend your app to the car!

Things to try out

  • Install an OEM's emulator (e.g. the Polestar 2 emulator) and see how OEM customization can change the look and feel of Car App Library apps on Android Automotive OS. Note that not all OEM emulators support Car App Library apps.
  • Check out the Showcase sample application that demonstrates the full functionality of the Car App Library.

Further reading

Reference docs