Arun Aditya
Menu
Building a Native Android System Service in AOSP (Android 15) — Step-by-Step Guide

Building a Native Android System Service in AOSP (Android 15) — Step-by-Step Guide

2026-06-20

AOSPAndroidBinderSELinuxNativeC++SystemService

If you've ever wondered how system services like SurfaceFlinger, AudioServer, or CameraServer are built inside AOSP, this post walks you through building one yourself from scratch. We'll create a native Binder service called namedService that runs on Android 15 (targeting Raspberry Pi 5), registers with ServiceManager, and exposes a custom AIDL interface over Binder IPC.

By the end you'll have a working service binary, SELinux policy, init integration, and a shell CLI tool to test it — the exact same layers every real AOSP service is built on.


Prerequisites & Environment Setup

Start by fetching the AOSP source using the Raspberry Vanilla manifest, then set up your build environment:

source build/envsetup.sh
lunch aosp_rpi5-ap3a-userdebug
make bootimage systemimage vendorimage -j$(nproc)

Project Directory Structure

Create the service directory under frameworks/native/services/:

frameworks/native/services/namedservice/
├── android/os/
│   └── INamedService.aidl
├── NamedService.h
├── NamedService.cpp
├── service_main.cpp
├── namedservice.rc
└── Android.bp

Keeping everything self-contained in one directory makes it easy to diff against upstream AOSP later.


Step 1 — Define the AIDL Interface

The AIDL file is the contract between clients and the service. Create android/os/INamedService.aidl:

package android.os;

interface INamedService {
    int add(int a, int b);
    int sub(int a, int b);
    int multiply(int a, int b);
    void initServiceName();
    String getServiceName();
    void setAuthorName(String firstName, String lastName);
    String getAuthorName();
}

The AOSP build system will auto-generate the BnNamedService (server stub) and BpNamedService (client proxy) C++ classes from this file.


Step 2 — Android.bp Build Configuration

aidl_interface {
    name: "namedservice_aidl",
    srcs: ["android/os/INamedService.aidl"],
    backend: {
        cpp: { enabled: true },
        java: { enabled: false },
        ndk: { enabled: false },
    },
}

cc_binary {
    name: "namedservice",
    srcs: [
        "NamedService.cpp",
        "service_main.cpp",
    ],
    shared_libs: [
        "libbinder",
        "libutils",
        "liblog",
    ],
    static_libs: ["namedservice_aidl-cpp"],
    init_rc: ["namedservice.rc"],
}

The init_rc field tells the build system to install the RC file automatically. No manual copying needed.


Step 3 — Service Header: NamedService.h

#pragma once

#include <android/os/BnNamedService.h>
#include <utils/String8.h>

namespace android {

class NamedService : public os::BnNamedService {
public:
    NamedService();

    binder::Status add(int32_t a, int32_t b, int32_t* result) override;
    binder::Status sub(int32_t a, int32_t b, int32_t* result) override;
    binder::Status multiply(int32_t a, int32_t b, int32_t* result) override;
    binder::Status initServiceName() override;
    binder::Status getServiceName(::android::String16* result) override;
    binder::Status setAuthorName(const ::android::String16& firstName,
                                  const ::android::String16& lastName) override;
    binder::Status getAuthorName(::android::String16* result) override;

private:
    String8 mServiceName;
    String8 mAuthorName;
};

} // namespace android

Step 4 — Service Implementation: NamedService.cpp

#include "NamedService.h"
#include <utils/Log.h>

namespace android {

NamedService::NamedService()
    : mServiceName("namedService"), mAuthorName("Unknown Author") {}

binder::Status NamedService::add(int32_t a, int32_t b, int32_t* result) {
    *result = a + b;
    return binder::Status::ok();
}

binder::Status NamedService::sub(int32_t a, int32_t b, int32_t* result) {
    *result = a - b;
    return binder::Status::ok();
}

binder::Status NamedService::multiply(int32_t a, int32_t b, int32_t* result) {
    *result = a * b;
    return binder::Status::ok();
}

binder::Status NamedService::initServiceName() {
    mServiceName = "namedService_initialized";
    return binder::Status::ok();
}

binder::Status NamedService::getServiceName(String16* result) {
    *result = String16(mServiceName);
    return binder::Status::ok();
}

binder::Status NamedService::setAuthorName(const String16& firstName,
                                            const String16& lastName) {
    String8 first(firstName);
    String8 last(lastName);
    mAuthorName = first + " " + last;
    return binder::Status::ok();
}

binder::Status NamedService::getAuthorName(String16* result) {
    *result = String16(mAuthorName);
    return binder::Status::ok();
}

} // namespace android

One thing to watch: Binder always carries strings as UTF-16 (String16). When you need to do any real string manipulation, convert to String8 first, work in UTF-8, then convert back before returning.


Step 5 — Service Entry Point: service_main.cpp

#include <binder/IPCThreadState.h>
#include <binder/IServiceManager.h>
#include <binder/ProcessState.h>
#include <utils/Log.h>
#include "NamedService.h"

int main(int /*argc*/, char** /*argv*/) {
    android::sp<android::ProcessState> ps = android::ProcessState::self();
    ps->setThreadPoolMaxThreadCount(4);

    android::sp<android::IServiceManager> sm = android::defaultServiceManager();
    sm->addService(android::String16("namedService"),
                   android::sp<android::NamedService>::make());

    android::ProcessState::self()->startThreadPool();
    android::IPCThreadState::self()->joinThreadPool();

    return 0;
}

joinThreadPool() on the main thread keeps the process alive and processing Binder transactions.


Step 6 — Init RC File: namedservice.rc

service namedservice /system/bin/namedservice
    class main
    user system
    group system
    oneshot

class main means the service starts during the main boot phase, alongside other framework services. oneshot means init won't restart it if it exits — change to restart if you want automatic recovery.


Step 7 — SELinux Policy

SELinux is the layer most developers get tripped up on. You need three things:

namedservice.te — type enforcement rules:

type namedservice, domain;
type namedservice_exec, exec_type, vendor_file_type, file_type;

init_daemon_domain(namedservice)
binder_use(namedservice)
add_service(namedservice, namedservice_service)

service_contexts — register the service name with SELinux:

namedService    u:object_r:namedservice_service:s0

file_contexts — label the binary:

/system/bin/namedservice    u:object_r:namedservice_exec:s0

Without these three files wired up correctly, ServiceManager will silently refuse the addService() call and you'll spend hours staring at a service check namedService that returns not found.


Step 8 — Build and Flash

m systemimage
./rpi5-mkimg.sh

Flash the resulting image to your Raspberry Pi 5 and boot. Check that the service started:

adb shell service check namedService
# Service namedService: found

adb shell service list | grep named
# namedService: [android.os.INamedService]

adb shell ps -A | grep namedservice
# system  1234  ...  namedservice

Step 9 — Testing via Binder Shell

The service call command is the quickest way to exercise a Binder interface from ADB without writing a client app:

# add(5, 3) → 8
adb shell service call namedService 1 i32 5 i32 3
# Result: Parcel(00000000 00000008 '........')

# sub(10, 4) → 6
adb shell service call namedService 2 i32 10 i32 4

# multiply(6, 7) → 42
adb shell service call namedService 3 i32 6 i32 7

# getServiceName()
adb shell service call namedService 5

# setAuthorName("Arun", "Aditya") then getAuthorName()
adb shell service call namedService 6 s16 Arun s16 Aditya
adb shell service call namedService 7

Method indices map 1:1 to the order they appear in the AIDL file. Keep that in mind if you reorder methods — your shell test commands will break.


Bonus: Building a CLI Tool

Raw service call output is unreadable for complex types. A small CLI binary makes testing far more ergonomic:

// namedservice_cli.cpp
#include <binder/IServiceManager.h>
#include <android/os/INamedService.h>
#include <utils/String8.h>
#include <iostream>

using namespace android;

int main(int argc, char** argv) {
    if (argc < 2) {
        std::cerr << "Usage: namedservice_cli <add|sub|multiply|set-author|get-author> [args]\n";
        return 1;
    }

    sp<IBinder> binder = defaultServiceManager()->waitForService(String16("namedService"));
    sp<os::INamedService> svc = interface_cast<os::INamedService>(binder);

    std::string cmd = argv[1];

    if (cmd == "add" && argc == 4) {
        int32_t result;
        svc->add(atoi(argv[2]), atoi(argv[3]), &result);
        std::cout << "Result: " << result << "\n";
    } else if (cmd == "sub" && argc == 4) {
        int32_t result;
        svc->sub(atoi(argv[2]), atoi(argv[3]), &result);
        std::cout << "Result: " << result << "\n";
    } else if (cmd == "set-author" && argc == 4) {
        svc->setAuthorName(String16(argv[2]), String16(argv[3]));
        std::cout << "Author set.\n";
    } else if (cmd == "get-author") {
        String16 name;
        svc->getAuthorName(&name);
        std::cout << String8(name).c_str() << "\n";
    }

    return 0;
}

Add it to Android.bp and device.mk:

cc_binary {
    name: "namedservice_cli",
    srcs: ["namedservice_cli.cpp"],
    shared_libs: ["libbinder", "libutils", "libbase"],
    static_libs: ["namedservice_aidl-cpp"],
}
# device.mk
PRODUCT_PACKAGES += namedservice namedservice_cli

Usage:

adb shell namedservice_cli add 5 3
# Result: 8

adb shell namedservice_cli set-author Arun Aditya
adb shell namedservice_cli get-author
# Arun Aditya

Wrapping Up

What we built mirrors the exact architecture of production AOSP services:

| Layer | What we did | |---|---| | AIDL | Defined the IPC contract | | Binder | Implemented BnNamedService for the server side | | ServiceManager | Registered and looked up the service | | Init | Managed the service lifecycle | | SELinux | Enforced access control at the kernel level | | CLI tool | Verified the whole stack end-to-end |

The next step from here is to expose this service to Java/Kotlin via the Android SDK layer — but that's a post for another day.

If you have questions or want to see the full source, check the repo links in my projects section.