Build and test a parked app for Android Automotive OS: Support audio while driving

1. Before you begin

What this isn't

  • A guide on how to create media (audio - e.g. music, radio, podcasts) apps for Android Auto and Android Automotive OS. See Build media apps for cars for details on how to build such apps.

What you'll need

What you'll build

In this codelab, you'll learn how to add support for audio while driving in Road Reels, an existing app that supports both mobile and Android Automotive OS devices.

The starting version of the pausing playback while user experience restrictions are active.

The completed version of the app continues playback while user experience restrictions are active.

What you'll learn

  • How to enable background audio playback in a video app on Android Automotive OS

2. Get set up

Get the code

  1. The code for this codelab can be found in the build-a-parked-app 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 build-a-parked-app/end directory. The build-a-parked-app/audio-while-driving 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.

3. Learn about parked apps for Android Automotive OS

Parked apps make up a subset of the app categories supported by Android Automotive OS. At the time of writing, they consist of video streaming apps, web browsers, and games. These apps are a great fit in cars given the hardware present in vehicles with Google built-in and the increasing prevalence of electric vehicles, in which charging time represents a great opportunity for drivers and passengers to engage with these types of apps.

In many ways, cars are similar to other large screen devices like tablets and foldables. They have touchscreens with similar sizes, resolutions, and aspect ratios and which may be in either portrait or landscape orientation (though, unlike tablets, their orientation is fixed). They are also connected devices which may come in and out of network connection. With all that in mind, it's not surprising that apps which are already optimized for large screens often require a minimal amount of work to bring a great user experience to cars.

Similar to large screens, there are also app quality tiers for apps in cars:

  • Tier 3 - Car ready: Your app is large screen compatible and can be used while the car is parked. While it may not have any car-optimized features, users can experience the app just as they would on any other large screen Android device. Mobile apps that meet these requirements are eligible to be distributed to cars as-is through the Car ready mobile apps program.
  • Tier 2 - Car optimized: Your app provides a great experience on the car's center stack display. To accomplish this, your app will have some car-specific engineering to include capabilities that can be used across driving or parked modes, depending on your app's category.
  • Tier 1- Car differentiated: Your app is built to work across the variety of hardware in cars and can adapt its experience across driving and parked modes. It provides the best user experience designed for the different screens in cars such as the center console, instrument cluster, and additional screens - like panoramic displays seen in many premium cars.

In this codelab, you'll be implementing audio-while driving, which is a Tier 1 feature, making the app a Car differentiated app.

4. Run the app in 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 of the API 34 Android Automotive emulator images listed in Add generic system images. Images can only run on machines with the same architecture (x86/ARM) as themselves.

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 (1408p landscape) bundled hardware profile from the list and click Next.

6a32a01404a7729f.png

  1. On the next page, select the system image from the previous step. Click Next and select any advanced options you want before finally creating the AVD by clicking Finish.

Run the app

Run the app on the emulator you just created using the app run configuration. Test the behavior of the app by navigating to the player screen and simulating driving.

5. Detect support for audio while driving

Because audio while driving isn't supported on all vehicles, you'll need to detect if the current device supports the feature and adapt your app's behavior correspondingly. To do this, you can use the CarFeatures class from the androidx.car.app:app library.

  1. Add a dependency on the androidx.car.app:app artifact.

libs.version.toml

[versions]
...
carApp = "1.7.0-rc01"


[libraries]
...
androidx-car-app = { group = "androidx.car.app", name = "app", version.ref = "carApp" }

build.gradle.kts (Module :app)

implementation(libs.androidx.car.app)
  1. In RoadReelsPlayer, you can then determine if the feature is supported and update the logic used to calculate the value of shouldPreventPlay.

RoadReelsPlayer.kt

if (packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
    ...

    val isBackgroundAudioWhileDrivingSupported = CarFeatures.isFeatureEnabled(
        context,
        CarFeatures.FEATURE_BACKGROUND_AUDIO_WHILE_DRIVING
    )

    shouldPreventPlay = !isBackgroundAudioWhileDrivingSupported &&
            carUxRestrictionsManager.currentCarUxRestrictions.isRequiresDistractionOptimization
    invalidateState()

    carUxRestrictionsManager.registerListener { carUxRestrictions: CarUxRestrictions ->
        shouldPreventPlay = !isBackgroundAudioWhileDrivingSupported &&
            carUxRestrictions.isRequiresDistractionOptimization
        ...
    }
}

Run the app again with these changes and simulate driving. Notice that audio playback now continues when the app's UI is obscured by the system. However, you're not done yet - to meet all of the requirements, you'll need to make a few more changes.

6. Support background playback

Currently, the app's media playback is handled by an activity. As such, media playback may continue for a short period of time after the user experience restrictions become active and the activity is obscured, but your app will eventually be cached by the system, causing playback to end.

To support long-running playback, the app must be updated to use a service to handle the playback. This can be achieved using Media3 MediaSessionService.

Create a MediaSessionService

  1. Right click on the com.example.android.cars.roadreels package in the Project window and select New > Kotlin Class/File. Enter PlaybackService as the file name as the name and click the Class type.
  2. Add the following implementation of the PlaybackService. See Background playback with a MediaSessionService for more information on MediaSessionService.

PlaybackService.kt

import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService

@UnstableApi
class PlaybackService : MediaSessionService() {
    private var mediaSession: MediaSession? = null

    override fun onCreate() {
        super.onCreate()
        val player = RoadReelsPlayer(this)
        mediaSession = MediaSession.Builder(this, player).build()
    }

    override fun onDestroy() {
        mediaSession?.run {
            player.release()
            release()
            mediaSession = null
        }
        super.onDestroy()
    }

    override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
        if (controllerInfo.isTrusted || controllerInfo.packageName == packageName) {
            return mediaSession
        }
        return null
    }
}
  1. Add the following <uses-permission> elements in the manifest file. Media playback is handled using a foreground service

AndroidManifest.xml

<manifest ...>
    ...
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
    ...
</manifest>
  1. Declare the PlaybackService in the manifest as well:

AndroidManifest.xml

<application ...>
    ...
    <service
        android:name=".PlaybackService"
        android:foregroundServiceType="mediaPlayback"
        android:exported="true">
        <intent-filter>
            <action android:name="androidx.media3.session.MediaSessionService"/>
        </intent-filter>
    </service>
</application>

Update the PlayerViewModel to use the PlaybackService

  1. Because the PlaybackService creates and exposes a MediaSession itself, there's no longer a need for the PlayerViewModel to create one. Find and delete the following line and all of the references to the variable:

PlayerViewModel.kt

private var mediaSession: MediaSession? = null
  1. Next, replace the section of the init block that instantiates a RoadReelsPlayer with the following code that connects the app to the PlaybackService using a MediaController.

PlayerViewModel.kt

import android.content.ComponentName
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import com.example.android.cars.roadreels.PlaybackService
import com.google.common.util.concurrent.MoreExecutors

...

init {
        viewModelScope.launch {
            ...
        }
        
        val sessionToken = SessionToken(
            application,
            ComponentName(application, PlaybackService::class.java)
        )

        val mediaControllerFuture =
            MediaController.Builder(getApplication(), sessionToken).buildAsync()

        mediaControllerFuture.addListener(
            { _player.update { mediaControllerFuture.get() } },
            MoreExecutors.directExecutor()
        )
    }

Test the app again, and you should see a different UI appear when the app is blocked by the system. Now, the user is able to control the playback while they're in motion. And when they're back at rest, they can click the exit button to return to the full app experience.

33eb8ff3d4035978.gif

Remove lifecycle playback changes

Because background playback is now supported, you can remove the two LifecycleEventEffect blocks in PlayerScreen.kt so that playback can continue when the user leaves the app, which includes when the playback controls shown in the previous screen are shown.

Pause playback when leaving the player screen

Because the actual player (which is now contained within the PlaybackService) isn't released when leaving the player screen, you'll need to add a call to pause playback when navigating away from it to maintain the prior behavior. To do this, you can update the implementation of the PlayerViewModel's onCleared method:

PlayerViewModel.kt

override fun onCleared() {
    super.onCleared()
    _player.value?.apply {
        pause()
        release()
    }
}

7. Update the manifest

Lastly, to indicate that your app supports audio while driving, you need to add the following <uses-feature> element in your app's manifest.

AndroidManifest.xml

<application>
    <uses-feature android:name="com.android.car.background_audio_while_driving" android:required="false">
</application>

8. Congratulations

You successfully added support for audio-while-driving to a video app. Now it's time to take what you learned and apply it to your own app!

Further reading