How to autostart a service on boot in AAOS

In the world of standard Android development, starting a service when a device boots up is a well-understood task. However, when you transition to Android Automotive OS (AAOS), the landscape changes dramatically. The vehicle environment introduces stringent requirements for stability, security, and performance that are orders of magnitude greater than those for a mobile phone. A service that fails to start at boot in a car isn't just an inconvenience; it could impact critical vehicle functions, diagnostics, or user experience features from the moment the ignition turns on. This guide provides a deep, comprehensive exploration of how to reliably and correctly autostart a service on boot in AAOS, covering the critical nuances of privileged apps, system permissions, and the AOSP build process.

We will move beyond simple recipes and delve into the underlying principles, ensuring you understand not just the 'how' but the 'why' behind each step. This knowledge is crucial for any developer aiming to build robust, integrated solutions for the next generation of connected vehicles. We'll cover everything from the initial code implementation to building it into a custom AOSP system image.

The Foundation: System Apps and Privileged Permissions in AAOS

Before writing a single line of code for your service, you must first understand the security and application model of Android Automotive. Unlike on a phone where most apps are third-party downloads, many essential AAOS applications are deeply integrated into the system. The service you want to start at boot will almost certainly need access to hardware or protected system APIs, which means it cannot be a standard, sandboxed application.

Distinguishing App Types: Standard vs. System vs. Privileged

In the Android ecosystem, applications are not created equal. Their capabilities are defined by where they are installed on the system image and how they are signed. This distinction is paramount in AAOS.

App Type Installation Location Signature Key Capabilities & Limitations
Standard App /data/app Developer's unique key Installed via app stores or sideloading. Operates within a strict security sandbox. Cannot access protected APIs or hardware directly. Cannot grant itself most system-level permissions. This is unsuitable for a boot-time service that needs deep system access.
System App /system/app Platform key (or other key known to the system) Bundled with the OS image. Can be granted certain system-level permissions that standard apps cannot, but still has limitations. The android:sharedUserId="android.uid.system" attribute is often used here.
Privileged App /system/priv-app Platform key The most powerful type of application. It has all the capabilities of a system app plus the ability to access a special set of "signatureOrSystem" level permissions. For tasks like interacting with the Car API at a low level or managing vehicle hardware, this is often the required level. Your boot-start service will most likely need to be a privileged app.

For a service that needs to start service on boot and interact with vehicle data via the Car APIs (like `CarPropertyManager` or `CarPowerManager`), it must be compiled as a privileged app. This involves two key steps: signing the app with the same platform key used to sign the operating system itself, and placing the compiled APK in the /system/priv-app directory within the AOSP build.

The Role of Platform Signing and Shared User ID

To become a privileged app, your application's digital signature must match the platform's signature. In a development environment, this means using the common development keys provided within the AOSP source tree. In a production environment, the Original Equipment Manufacturer (OEM) uses their private platform keys.

Furthermore, you may encounter the `android:sharedUserId` attribute in the `AndroidManifest.xml`:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.autoservice"
    android:sharedUserId="android.uid.system">

Setting the `sharedUserId` to `android.uid.system` allows your application to run with the same Linux user ID as the core Android system server (`system_server`). This grants it immense power and access to system resources. However, it also comes with great responsibility. A crash or security vulnerability in an app with system UID can destabilize the entire OS. This is generally reserved for core system components. For many boot-time services, being a privileged app (in /system/priv-app and signed with the platform key) is sufficient without needing to share the system UID.

The Core Mechanism: Listening for the Boot Broadcast

The standard Android mechanism for running code after a device boots is to listen for a system broadcast. This fundamental principle remains the same in AAOS, but the context and timing are more critical.

Understanding Boot-Related Intents

The Android system sends out specific broadcast intents when the boot process reaches certain milestones. A `BroadcastReceiver` in your app can listen for these intents to get triggered.

  • android.intent.action.BOOT_COMPLETED: This is the most common and well-known broadcast. It's sent after the system has finished booting, and the user space is ready. This is the primary intent you'll use. However, on encrypted devices, this is only sent after the user unlocks the device for the first time.
  • android.intent.action.LOCKED_BOOT_COMPLETED: This is sent earlier in the boot process on devices that support Direct Boot. The system is running, but the user has not yet unlocked the device, and the app's credential-encrypted storage is not available. This is useful for services that need to run immediately, even before the user logs in, such as an alarm or a notification service.
  • Vehicle-Specific States: In AAOS, the boot process is intertwined with the vehicle's power states (e.g., ignition on, accessory mode, deep sleep). Your service might need to be aware of these states, which are managed by the `CarPowerManager`. While `BOOT_COMPLETED` signals the OS is ready, you might also need to listen for power state changes to start or stop certain tasks.

Step 1: Creating the Boot BroadcastReceiver

The first piece of code is the `BroadcastReceiver`. Its sole purpose is to receive the `BOOT_COMPLETED` intent and immediately delegate the work to a `Service`. It's crucial that the receiver does minimal work itself, as the system imposes a short timeout on the `onReceive()` method. A long-running task here will result in an "Application Not Responding" (ANR) error.

Kotlin Implementation (`BootReceiver.kt`)

package com.example.autoservice

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log

class BootReceiver : BroadcastReceiver() {

    companion object {
        private const val TAG = "BootReceiver"
    }

    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
            Log.i(TAG, "BOOT_COMPLETED broadcast received. Starting VehicleDataService.")

            // Create an intent to start our service
            val serviceIntent = Intent(context, VehicleDataService::class.java)

            // For Android 8 (Oreo) and above, we must use startForegroundService
            // This is critical for services started from a BroadcastReceiver
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                context.startForegroundService(serviceIntent)
            } else {
                context.startService(serviceIntent)
            }
        }
    }
}

Java Implementation (`BootReceiver.java`)

package com.example.autoservice;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.util.Log;

public class BootReceiver extends BroadcastReceiver {

    private static final String TAG = "BootReceiver";

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction() != null && intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
            Log.i(TAG, "BOOT_COMPLETED broadcast received. Starting VehicleDataService.");

            Intent serviceIntent = new Intent(context, VehicleDataService.class);

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                context.startForegroundService(serviceIntent);
            } else {
                context.startService(serviceIntent);
            }
        }
    }
}
Important: As of Android 8.0 (API 26), background execution limits are strictly enforced. Any attempt to call `startService()` from a `BroadcastReceiver` on a modern Android system will result in an `IllegalStateException`. You must use `context.startForegroundService()`, which then requires the service itself to call `startForeground()` within a few seconds.

Implementing the Background Service

The `Service` is where the actual work happens. This component runs in the background, independent of any UI. In an AAOS context, this could be anything from monitoring vehicle speed, managing Bluetooth connections, syncing data with a remote server, or initializing custom hardware.

Step 2: Creating the Foreground Service

Our service, `VehicleDataService`, will be a foreground service. This means it has a higher priority in the system's eyes and is less likely to be killed when memory is low. It also requires showing a persistent notification to the user, making it clear that an application is running in the background. In AAOS, this notification is typically subtle and managed by the system UI, but it is still a requirement.

Kotlin Implementation (`VehicleDataService.kt`)

package com.example.autoservice

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import android.os.IBinder
import android.os.Looper
import android.os.Process
import android.util.Log
import androidx.core.app.NotificationCompat

class VehicleDataService : Service() {

    private var serviceLooper: Looper? = null
    private var serviceHandler: ServiceHandler? = null

    companion object {
        private const val TAG = "VehicleDataService"
        private const val NOTIFICATION_CHANNEL_ID = "VehicleServiceChannel"
        private const val NOTIFICATION_ID = 1
    }

    // Handler that receives messages from the thread
    private inner class ServiceHandler(looper: Looper) : Handler(looper) {
        override fun handleMessage(msg: android.os.Message) {
            // This is where your long-running work goes.
            // For example, connect to CarPropertyManager, subscribe to vehicle signals, etc.
            Log.i(TAG, "Service work started on background thread.")
            try {
                // Simulate some work
                for (i in 1..5) {
                    Log.d(TAG, "Doing work... step $i")
                    Thread.sleep(2000)
                }
            } catch (e: InterruptedException) {
                Thread.currentThread().interrupt()
            }
            Log.i(TAG, "Service work finished.")

            // Once work is done, you might decide to stop the service.
            // stopSelf()
        }
    }
    
    override fun onCreate() {
        super.onCreate()
        Log.d(TAG, "onCreate called.")

        // Start up the thread running the service. Note that we create a
        // separate thread because the service normally runs in the process's
        // main thread, which we don't want to block.
        HandlerThread("VehicleServiceBackgroundThread", Process.THREAD_PRIORITY_BACKGROUND).apply {
            start()
            serviceLooper = looper
            serviceHandler = ServiceHandler(looper)
        }
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.i(TAG, "onStartCommand called.")
        
        createNotificationChannel()
        val notification = createNotification()
        startForeground(NOTIFICATION_ID, notification)

        // For each start request, send a message to start a job and deliver the
        // start ID so we know which request we're stopping when we finish the job.
        serviceHandler?.obtainMessage()?.also { msg ->
            msg.arg1 = startId
            serviceHandler?.sendMessage(msg)
        }

        // If the service is killed, restart it
        return START_STICKY
    }

    override fun onBind(intent: Intent): IBinder? {
        // We don't provide binding, so return null
        return null
    }
    
    override fun onDestroy() {
        super.onDestroy()
        Log.w(TAG, "onDestroy called. Service is shutting down.")
        serviceLooper?.quitSafely()
    }

    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val serviceChannel = NotificationChannel(
                NOTIFICATION_CHANNEL_ID,
                "Vehicle Service Channel",
                NotificationManager.IMPORTANCE_DEFAULT
            )
            val manager = getSystemService(NotificationManager::class.java)
            manager.createNotificationChannel(serviceChannel)
        }
    }

    private fun createNotification(): Notification {
        // The PendingIntent to launch your activity if the user taps the notification
        // In AAOS, this might be less relevant, but it's good practice.
        // val pendingIntent: PendingIntent = ... 

        return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
            .setContentTitle("Vehicle System Service")
            .setContentText("Monitoring vehicle systems.")
            .setSmallIcon(R.drawable.ic_launcher_foreground) // Replace with your own icon
            .build()
    }
}
Worker Threads are Essential: The `onStartCommand()` method, like `onCreate()`, runs on the application's main thread. Performing network operations, heavy I/O, or long computations here will freeze your app and lead to an ANR. The example above correctly uses a `HandlerThread` to offload the actual work to a separate background thread, which is a robust pattern for services.

Configuring the AndroidManifest.xml for Boot Startup

The `AndroidManifest.xml` file is the central nervous system of an Android application. It declares the app's components and tells the Android OS how they should be treated. For our boot-start service, this configuration is critical for both functionality and security.

Step 3 & 4: Declaring Components and Requesting Permissions

We need to perform three key actions in the manifest:

  1. Declare the `BroadcastReceiver` and specify that it should listen for the `BOOT_COMPLETED` intent.
  2. Declare the `Service` itself.
  3. Request all the necessary permissions for the receiver and the service to operate correctly.

Complete `AndroidManifest.xml` Example

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.autoservice">

    <!-- *CRITICAL* Permission to receive the boot completed broadcast -->
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    
    <!-- *CRITICAL* Permission to run as a foreground service -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    
    <!-- Example permissions your AAOS service might need -->
    <!-- These are often privileged and require the app to be signed with the platform key -->
    <uses-permission android:name="android.car.permission.CAR_INFO" />
    <uses-permission android:name="android.car.permission.CAR_POWER" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyApplication"
        android:persistent="true"> <!-- persistent attribute can be useful for system apps -->

        <!-- Declare the BroadcastReceiver -->
        <receiver
            android:name=".BootReceiver"
            android:enabled="true"
            android:exported="true"> <!-- exported=true is needed for system broadcasts on some versions -->
            <intent-filter>
                <!-- This is the trigger for our receiver -->
                <action android:name="android.intent.action.BOOT_COMPLETED" />
            </intent-filter>
        </receiver>

        <!-- Declare the Service -->
        <service
            android:name=".VehicleDataService"
            android:enabled="true"
            android:exported="false" /> <!-- exported=false as other apps should not start it directly -->
            
    </application>

</manifest>

A note on `android:exported`: For apps targeting Android 12 (API 31) and higher, you must explicitly set the `android:exported` attribute for any component with an intent filter. For a receiver listening to a system-wide broadcast like `BOOT_COMPLETED`, this should be set to `true`.

Android Developer Documentation

Building and Deploying a Privileged AAOS App

Creating the code is only half the battle. Unlike a normal app you build with a single Gradle command in Android Studio, a privileged system app for AAOS must be integrated into the AOSP source tree and built as part of the entire operating system image.

Integrating into the AOSP Build System

The AOSP build system uses Soong (with `Android.bp` files) or the legacy Make system (with `Android.mk` files). Soong is the modern, preferred approach. To include your application, you'll need to follow these general steps:

  1. Place Your Source Code: Copy your application's source code directory (e.g., `VehicleService`) into a suitable location within the AOSP tree, typically `packages/apps/`.
  2. Create an `Android.bp` File: Inside your app's main directory, create a build file named `Android.bp`. This file tells the Soong build system how to compile your app.

Example `Android.bp` for a Privileged App

This is the most critical part of the process. This build file defines your app as privileged and signs it with the platform certificate.

// Android.bp file for our VehicleDataService app

android_app {
    // A unique name for your module
    name: "VehicleDataServiceApp", 

    // List of all source code directories
    srcs: [
        "app/src/main/java/**/*.java",
        "app/src/main/java/**/*.kt",
    ],

    // Specify the package name from your manifest
    package_name: "com.example.autoservice",

    // Specify the location of the AndroidManifest.xml
    manifest: "app/src/main/AndroidManifest.xml",

    // Specify resource directories
    resource_dirs: ["app/src/main/res"],

    // Use platform APIs, not the public SDK
    sdk_version: "system_current",

    // This is the magic flag that marks the app as privileged.
    // The build system will install it into /system/priv-app/
    privileged: true,

    // This signs the APK with the platform key, granting it access
    // to signature-level permissions.
    certificate: "platform",

    // Add dependencies if needed, e.g., the Android Car API library
    static_libs: [
        "androidx.core.core-ktx",
        "android.car-lib",
    ],

    // Required for Kotlin support
    kotlincflags: ["-Xjvm-default=all"],
}

Building the System Image

Once your `Android.bp` file is in place, you need to add your module to the device's product package list. This is typically done by editing a `.mk` file, such as `device/<vendor>/<product>/device.mk`, and adding your module name (`VehicleDataServiceApp`) to the `PRODUCT_PACKAGES` list:

# In your device-specific .mk file
PRODUCT_PACKAGES += \
    VehicleDataServiceApp

After this, you can build the entire Android Automotive OS image using the standard AOSP build commands:

# Set up the build environment
source build/envsetup.sh

# Choose your target device (e.g., emulator)
lunch aosp_car_x86_64-userdebug

# Build the entire system image (-j specifies the number of parallel threads)
make -j16

Finally, you flash the newly built image (`.img` files) to your target hardware or run it in the AAOS emulator. If everything is configured correctly, your `BootReceiver` will be triggered on the next boot, and your `VehicleDataService` will start automatically.

Advanced Topics and Best Practices

Simply getting a service to run at boot is just the beginning. In a real-world automotive environment, you need to consider dependencies, performance, and robustness.

Alternative Boot Triggers and Dependencies

Sometimes, `BOOT_COMPLETED` is not the right trigger. Your service might depend on another system service, like the `Car` service, which may not be immediately available when the OS boots. In these cases, you need a more sophisticated initialization strategy.

Trigger Use Case Implementation Detail
BOOT_COMPLETED General purpose tasks that can run as soon as the OS is up. Does not depend on the Car Service. Standard `BroadcastReceiver` as detailed above.
Car Service Connection Any task that requires interaction with vehicle hardware through the Car APIs (`CarPropertyManager`, `CarPowerManager`, etc.). Start a service on `BOOT_COMPLETED`, but within that service, attempt to connect to the `Car` service using `Car.createCar()`. Only proceed with vehicle-specific logic once the connection is established. Implement retry logic in case the Car service is not yet ready.
Vehicle Power State Changes Tasks that should only run when the car is fully "on" (e.g., ignition in RUN state) and should be suspended or stopped during sleep states to conserve power. Use `CarPowerManager` to register a `CarPowerStateListener`. The service can start on boot but then modulate its behavior based on the power state notifications (`CarPowerManager.CarPowerStateListener.onStateChanged`).

Debugging Boot-Time Services

Debugging a service that runs during the boot sequence can be challenging because it happens before you can easily attach a debugger.

  • Logcat is Your Best Friend: This is the most crucial tool. Use extensive logging (`Log.i`, `Log.d`, `Log.e`) in your receiver and service. You can capture the boot-time logs using `adb logcat` from a connected computer. Start capturing logs right as the device begins to power on. Use filters to narrow down to your application's specific log tags.
  • Check for SELinux Denials: Security-Enhanced Linux (SELinux) is enforced in AAOS and is a common source of "permission denied" errors that are not related to standard Android permissions. If your service is trying to access a file or hardware resource it shouldn't, SELinux will block it and log a denial. Check `logcat` for messages containing "avc: denied".
  • Wait for Debugger: For tricky race conditions or initialization issues, you can programmatically force your service to wait for a debugger to attach using `android.os.Debug.waitForDebugger()`. Place this at the beginning of your service's `onCreate()` method. When the service starts, it will pause execution until you attach a debugger from Android Studio. This should only be used for debugging, never in production code.
Common Pitfalls:
  • Forgetting the `RECEIVE_BOOT_COMPLETED` permission in the manifest.
  • Using `startService()` instead of `startForegroundService()` on modern Android versions.
  • Failing to call `startForeground()` within the service after it's started by `startForegroundService()`.
  • Incorrect `Android.bp` configuration (e.g., `privileged: false` or wrong `certificate`).
  • SELinux policies blocking your service from accessing necessary files or system properties.

Conclusion: Mastering Boot-Time Services in AAOS

Successfully and reliably starting a service at boot in Android Automotive OS is a fundamental skill for any developer in this space. It's a process that touches on every unique aspect of the platform: the heightened security model, the necessity of privileged app permissions, the integration with the AOSP build system, and the interaction with vehicle-specific services.

By following the steps outlined—creating a privileged system app, implementing a `BroadcastReceiver` for the `BOOT_COMPLETED` action, launching a robust foreground `Service`, and correctly configuring the `AndroidManifest.xml` and `Android.bp` files—you can build components that form the backbone of a modern in-vehicle experience. Remember that with the great power of privileged access comes the great responsibility of ensuring your code is efficient, stable, and secure. Every service that runs at boot contributes to the vehicle's startup time and overall system health, making meticulous development and testing not just a best practice, but a necessity.

Post a Comment