
Building a Native Android System Service in AOSP (Android 15) — Step-by-Step Guide
2026-06-20
AOSPAndroidBinderSELinuxNativeC++SystemServiceIf 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.