Skip to content

Apps & UI

Build a privileged system app

An APK built into the image, signed with the platform key, holding a signature permission and using hidden APIs — and how to iterate on it without reflashing.

Intermediate5 min readSystem app · Soong · Signing · Privileged

What you will build

A VegaComfort app installed to /product/priv-app, platform-signed, holding your OEM permission and calling your Car subservice.

Estimated time
2–3 hours
Steps
6 steps

Ordinary apps cannot touch the vehicle. A privileged, platform-signed app can — at the cost of shipping inside the image and inheriting the OEM's release cadence.

Files you will create

vendor/oem/vega/apps/VegaComfort/ ├── Android.bp ├── AndroidManifest.xml ├── src/com/oem/vega/comfort/ │ ├── MainActivity.kt │ └── SeatMassageController.kt └── res/ ├── layout/activity_main.xml └── values/strings.xml

Step 1 — The Soong module#

This is where the "privileged" part actually happens.

vendor/oem/vega/apps/VegaComfort/Android.bp
android_app {
    name: "VegaComfort",
 
    srcs: ["src/**/*.kt"],
    resource_dirs: ["res"],
    manifest: "AndroidManifest.xml",
 
    // Hidden and @SystemApi surfaces — needed for android.car internals
    platform_apis: true,
 
    // Signed with the platform key. This is what makes signature
    // permissions grantable.
    certificate: "platform",
 
    // Installs to priv-app rather than app. Requires a privapp-permissions
    // entry for every signature permission it requests.
    privileged: true,
 
    // Lands on /product, the OEM partition.
    product_specific: true,
 
    static_libs: [
        "androidx.appcompat_appcompat",
        "car-ui-lib",
        "kotlin-stdlib",
    ],
 
    // Provided by the platform at runtime, not bundled into the APK.
    libs: [
        "android.car",
    ],
 
    optimize: {
        enabled: false,   // keep stack traces readable during bring-up
    },
}
PropertyWhat it buys youWhat it costs
platform_apis: trueHidden and @SystemApi accessCannot build outside AOSP
certificate: "platform"Signature permissionsThe OEM signs it, not you
privileged: truepriv-app placementNeeds an allowlist entry
product_specific: true/product partitionShips in the image

platform_apis and sdk_version are mutually exclusive

Setting both fails the build with a message that does not obviously say so. An app using android.car internals needs platform_apis: true and no sdk_version line at all.

Step 2 — The manifest#

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.oem.vega.comfort"
    android:sharedUserId="android.uid.system">
 
    <!-- Automotive build only -->
    <uses-feature android:name="android.hardware.type.automotive" android:required="true"/>
    <uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
 
    <!-- Your OEM permissions, allowlisted in privapp-permissions-vega.xml -->
    <uses-permission android:name="com.oem.vega.permission.CONTROL_SEAT_MASSAGE"/>
    <uses-permission android:name="com.oem.vega.permission.READ_SEAT_STATE"/>
 
    <application
        android:label="@string/app_name"
        android:theme="@style/Theme.CarUi">
 
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <!-- Marks this activity safe to show while driving. This is a
                 safety claim OEM acceptance will test — do not set it to work
                 around the block. -->
            <meta-data android:name="distractionOptimized" android:value="true"/>
 
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>
</manifest>

sharedUserId is a one-way door

android.uid.system makes the app run as the system user, which some platform APIs require. It also means the app can never be updated independently of the platform, cannot be uninstalled, and cannot change its shared user id later without a data wipe. Add it only when a specific API demands it.

Step 3 — Call the platform#

src/com/oem/vega/comfort/SeatMassageController.kt
package com.oem.vega.comfort
 
import android.car.Car
import android.car.VehicleAreaSeat
import android.car.comfort.CarComfortManager
import android.content.Context
import android.util.Log
 
class SeatMassageController(context: Context) {
 
    private var comfort: CarComfortManager? = null
 
    // Use the lifecycle-listener form. Car Service restarts on real hardware,
    // and every manager you hold becomes permanently invalid when it does.
    private val car: Car = Car.createCar(
        context,
        /* handler = */ null,
        Car.CAR_WAIT_TIMEOUT_DO_NOT_WAIT,
    ) { car, ready ->
        comfort = if (ready) {
            car.getCarManager(Car.CAR_COMFORT_SERVICE) as? CarComfortManager
        } else {
            null   // service died; wait for the next ready callback
        }
        onAvailabilityChanged(comfort != null)
    }
 
    fun setIntensity(level: Int) {
        val manager = comfort ?: run {
            Log.w(TAG, "comfort service unavailable")
            return
        }
        runCatching {
            manager.setMassageIntensity(VehicleAreaSeat.SEAT_ROW_1_LEFT, level)
        }.onFailure { Log.w(TAG, "set failed", it) }
    }
 
    fun release() = car.disconnect()
 
    private companion object { const val TAG = "VegaComfort" }
}

Step 4 — Add it to the product#

device/oem/vega/vega.mk
PRODUCT_PACKAGES += VegaComfort
m VegaComfort          # fast — build just this module
m -j                   # then the image

Verify it built into the right place, signed correctly

# 1. Built to priv-app on /product
ls -la $ANDROID_PRODUCT_OUT/product/priv-app/VegaComfort/
 
# 2. Signed with the platform key (not the test key, on a release build)
apksigner verify --print-certs \
  $ANDROID_PRODUCT_OUT/product/priv-app/VegaComfort/VegaComfort.apk

Step 5 — Flash and verify on device#

emulator -wipe-data -no-snapshot & adb wait-for-device && sleep 45

Verify it is privileged and holds the permission

# 1. Installed, and in priv-app
adb shell pm path com.oem.vega.comfort
# expect: /product/priv-app/VegaComfort/VegaComfort.apk
 
# 2. Recognised as privileged
adb shell dumpsys package com.oem.vega.comfort | grep -i 'flags=.*PRIVILEGED'
 
# 3. Holds your signature permission
adb shell dumpsys package com.oem.vega.comfort | grep -A10 'granted permissions'
 
# 4. Appears in the launcher
adb shell cmd package query-activities \
  -a android.intent.action.MAIN -c android.intent.category.LAUNCHER | grep -i vega
 
# 5. Actually runs and reaches Car Service
adb shell am start -n com.oem.vega.comfort/.MainActivity
adb logcat -b all | grep -i VegaComfort

Step 6 — Iterate without reflashing#

Reflashing for every code change is unusable. On a userdebug build:

Push and restart, ~30 seconds
m VegaComfort
 
adb root && adb remount
 
adb push $ANDROID_PRODUCT_OUT/product/priv-app/VegaComfort/VegaComfort.apk \
  /product/priv-app/VegaComfort/VegaComfort.apk
 
# Package manager must re-scan; a soft restart is enough
adb shell stop && adb shell start
adb wait-for-device && sleep 20

adb install will not work for a privileged app

adb install places the APK in /data/app, where it is not privileged and its signature permissions are refused. The symptom is a SecurityException that looks like a permission bug and is actually an installation-location one. Push to the priv-app path instead.

If adb remount fails, verified boot is on:

adb root && adb disable-verity && adb reboot
adb wait-for-device && adb root && adb remount

That is a development-only operation. It breaks the verified boot state, which is exactly what it should not be possible to do on a production vehicle.

Troubleshooting#

SymptomCause
Image will not bootMissing privapp-permissions entry
SecurityException on a Car APIApp installed to /data/app, not priv-app
Not in the launcherMissing LAUNCHER category, or installed for another user
Cannot resolve android.carMissing libs: ["android.car"]
Cannot use a hidden APIMissing platform_apis: true
Blocked while drivingNo distractionOptimized metadata
Build error about sdk_versionplatform_apis and sdk_version both set

Next#

Making the platform look like yours, without forking it.

References & further reading

Code links target the main branch on cs.android.com. AOSP moves — if a path 404s, search the symbol instead.