Android IoT, Automotive, & Smart TV Customizations

Mastering AAOS: Build Your First Custom Car Service from Scratch (Step-by-Step Tutorial)

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to AAOS Custom Car Services

Android Automotive OS (AAOS) is a full-stack, open-source platform powering the infotainment systems in modern vehicles. While AAOS provides a robust set of core services through its `CarService` framework (e.g., HVAC, Radio, VHAL), there will inevitably be scenarios where vehicle manufacturers or third-party developers need to expose custom functionalities specific to their hardware or unique vehicle features. This is where custom Car Services come into play.

A custom Car Service in AAOS operates similarly to a standard Android bound service but is specifically designed to interact with vehicle-specific hardware or software components, often running with elevated privileges within the system partition. This tutorial will guide you through building your very first custom Car Service from the ground up, covering everything from AIDL definition to client interaction and deployment.

Prerequisites

  • An AAOS development environment (e.g., AOSP build targeting a Cuttlefish emulator or a physical AAOS device).
  • Android Studio for developing the service and client applications.
  • Basic understanding of Android Service lifecycle and Inter-Process Communication (IPC) using AIDL.
  • `adb` command-line tool configured for your AAOS device/emulator.

Step 1: Define the AIDL Interface

AIDL (Android Interface Definition Language) is crucial for defining the interface that clients will use to interact with your custom Car Service. It allows processes to communicate with each other through IPC. For our example, let’s create a simple service that provides a custom message and a counter.

Create a new Android Library module (or within your main app module if it’s a system app) named `customcarservice_aidl`. Inside this module, define your AIDL file. In Android Studio, right-click on `app/src/main` > `New` > `AIDL` > `AIDL File` and name it `ICustomCarService`.

// ICustomCarService.aidl
package com.example.customcarservice;

interface ICustomCarService {
    String getCustomMessage();
    int incrementAndGetCounter();
    void registerCallback(ICustomCarServiceCallback callback);
    void unregisterCallback(ICustomCarServiceCallback callback);
}

Now, let’s define the callback interface for asynchronous communication:

// ICustomCarServiceCallback.aidl
package com.example.customcarservice;

oneway interface ICustomCarServiceCallback {
    void onCounterChanged(int newCounterValue);
}

Build the project once to generate the Java interfaces from these AIDL files.

Step 2: Implement the Custom Car Service

Now, we’ll create the actual Android Service that implements the `ICustomCarService` interface. Create a new Android Application module (e.g., `CustomCarServiceApp`) in your project.

2.1 Add AIDL dependency

First, ensure your `CustomCarServiceApp` module depends on your AIDL library. In `build.gradle` for `CustomCarServiceApp`:

dependencies {
    implementation project(':customcarservice_aidl')
}

2.2 Create the Service Class

Create a new Java class `CustomCarService` extending `android.app.Service`.

package com.example.customcarserviceapp;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;

import com.example.customcarservice.ICustomCarService;
import com.example.customcarservice.ICustomCarServiceCallback;

import java.util.concurrent.atomic.AtomicInteger;

public class CustomCarService extends Service {

    private static final String TAG = "CustomCarService";
    private AtomicInteger mCounter = new AtomicInteger(0);
    private final RemoteCallbackList<ICustomCarServiceCallback> mCallbacks = new RemoteCallbackList<>();

    private final ICustomCarService.Stub mBinder = new ICustomCarService.Stub() {
        @Override
        public String getCustomMessage() throws RemoteException {
            Log.d(TAG, "getCustomMessage() called");
            return "Hello from Custom AAOS Car Service!";
        }

        @Override
        public int incrementAndGetCounter() throws RemoteException {
            int newCount = mCounter.incrementAndGet();
            Log.d(TAG, "incrementAndGetCounter() called, newCount: " + newCount);
            notifyCounterChanged(newCount);
            return newCount;
        }

        @Override
        public void registerCallback(ICustomCarServiceCallback callback) throws RemoteException {
            if (callback != null) {
                mCallbacks.register(callback);
                Log.d(TAG, "Callback registered.");
            }
        }

        @Override
        public void unregisterCallback(ICustomCarServiceCallback callback) throws RemoteException {
            if (callback != null) {
                mCallbacks.unregister(callback);
                Log.d(TAG, "Callback unregistered.");
            }
        }
    };

    @Override
    public IBinder onBind(Intent intent) {
        Log.d(TAG, "onBind() called with intent: " + intent);
        return mBinder;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate() called.");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        mCallbacks.kill();
        Log.d(TAG, "onDestroy() called.");
    }

    private void notifyCounterChanged(int newCounterValue) {
        int i = mCallbacks.beginBroadcast();
        while (i > 0) {
            i--;
            try {
                mCallbacks.getBroadcastItem(i).onCounterChanged(newCounterValue);
            } catch (RemoteException e) {
                Log.e(TAG, "Error notifying callback", e);
            }
        }
        mCallbacks.finishBroadcast();
    }
}

2.3 Declare Service in AndroidManifest.xml

Open `AndroidManifest.xml` in `CustomCarServiceApp` and declare your service. It’s crucial to add an intent filter with a unique action that clients can use to bind to your service. We’ll also add a custom permission for binding, enhancing security.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.customcarserviceapp">

    <permission android:name="com.example.customcarservice.BIND_CUSTOM_CAR_SERVICE"
        android:protectionLevel="signature" />

    <uses-permission android:name="com.example.customcarservice.BIND_CUSTOM_CAR_SERVICE" />

    <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.CustomCarServiceApp">
        <service
            android:name=".CustomCarService"
            android:exported="true">
            <intent-filter>
                <action android:name="com.example.customcarservice.BIND"
                    />
            </intent-filter>
        </service>
    </application>
</manifest>

Note the `android:exported=”true”` and the custom permission with `protectionLevel=”signature”`. This means only apps signed with the same certificate as your service can bind to it. For system apps, `signature` protection is common. For simpler debugging or if installed as a user app, you might temporarily use `normal` protection, but be aware of security implications.

Step 3: Build and Deploy the Service APK

Build the `CustomCarServiceApp` module in Android Studio to generate the APK. Once built, deploy it to your AAOS emulator or device using ADB.

adb install path/to/CustomCarServiceApp/build/outputs/apk/debug/CustomCarServiceApp-debug.apk

If you’re deploying to a read-only system partition (common for custom car services meant to be part of the system image), you might need to push it to `/system/priv-app/` or `/system/app/` after remounting the system partition as writable and rebooting. For development, `adb install` to `/data/app` is usually sufficient.

Step 4: Create a Client Application

Now, let’s create a separate Android Application module (e.g., `CustomCarServiceClient`) that will bind to and interact with our custom service.

4.1 Add AIDL dependency

Similar to the service app, the client also needs access to the AIDL interfaces. In `build.gradle` for `CustomCarServiceClient`:

dependencies {
    implementation project(':customcarservice_aidl')
}

4.2 Client Code to Bind and Interact

Create a simple UI (e.g., `MainActivity.java` with a few buttons and TextViews) to display the message and counter. The core logic will be in `MainActivity`.

package com.example.customcarserviceclient;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;

import com.example.customcarservice.ICustomCarService;
import com.example.customcarservice.ICustomCarServiceCallback;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "CustomClient";
    private ICustomCarService mService;
    private boolean mBound = false;

    private TextView messageTextView;
    private TextView counterTextView;

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.d(TAG, "Service Connected");
            mService = ICustomCarService.Stub.asInterface(service);
            mBound = true;
            try {
                String message = mService.getCustomMessage();
                messageTextView.setText("Service Message: " + message);
                mService.registerCallback(mCallback);
                int currentCounter = mService.incrementAndGetCounter(); // Get initial counter
                counterTextView.setText("Counter: " + currentCounter);
            } catch (RemoteException e) {
                Log.e(TAG, "Error calling service method", e);
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.d(TAG, "Service Disconnected");
            mService = null;
            mBound = false;
            messageTextView.setText("Service Message: Disconnected");
            counterTextView.setText("Counter: Disconnected");
        }
    };

    private ICustomCarServiceCallback.Stub mCallback = new ICustomCarServiceCallback.Stub() {
        @Override
        public void onCounterChanged(int newCounterValue) throws RemoteException {
            Log.d(TAG, "onCounterChanged: " + newCounterValue);
            runOnUiThread(() -> counterTextView.setText("Counter: " + newCounterValue));
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        messageTextView = findViewById(R.id.messageTextView);
        counterTextView = findViewById(R.id.counterTextView);
        Button incrementButton = findViewById(R.id.incrementButton);

        incrementButton.setOnClickListener(v -> {
            if (mBound) {
                try {
                    int newCount = mService.incrementAndGetCounter();
                    // UI update happens via callback now
                } catch (RemoteException e) {
                    Log.e(TAG, "Error incrementing counter", e);
                }
            }
        });
    }

    @Override
    protected void onStart() {
        super.onStart();
        Intent intent = new Intent("com.example.customcarservice.BIND");
        intent.setComponent(new ComponentName(
                "com.example.customcarserviceapp",
                "com.example.customcarserviceapp.CustomCarService"));
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (mBound) {
            try {
                mService.unregisterCallback(mCallback);
            } catch (RemoteException e) {
                Log.e(TAG, "Error unregistering callback", e);
            }
            unbindService(mConnection);
            mBound = false;
        }
    }
}

4.3 Client Manifest

In `AndroidManifest.xml` for `CustomCarServiceClient`, declare the permission to bind to the service.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.customcarserviceclient">

    <uses-permission android:name="com.example.customcarservice.BIND_CUSTOM_CAR_SERVICE" />

    <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.CustomCarServiceClient">
        <activity android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

Step 5: Testing the Custom Car Service

1. **Install the client app:** Build `CustomCarServiceClient` and install it on your AAOS device/emulator.adb install path/to/CustomCarServiceClient/build/outputs/apk/debug/CustomCarServiceClient-debug.apk

2. **Launch the client:** Find the `CustomClient` app in the AAOS launcher and open it.

3. **Observe:** Upon launch, the client app should bind to your service. You should see the

Android Mobile Specs & Compare Directory

Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!

Compare Devices Specs →
Google AdSense Inline Placement - Content Footer banner