← Back to list

Building a Custom System Service in Android 15 (AOSP)

Part 1: Architecture and the Interface (The Blueprint)

Arun Aditya · 2026-03-01 08:35 · 53 claps · 17.3 min read
#aosp #android-system-service #android-framework #android-platform #android-automotive
Open on Medium ↗
Wiki topics: 🏛️ · Architecture

Building a Custom System Service in Android 15 (AOSP)

Part 1: Architecture and the Interface (The Blueprint)

Building a system service in Android 15 (AOSP) isn’t just about writing code; it’s about understanding how the “Grand Librarian” of Android (the ServiceManager) keeps track of every capability the OS offers. When you're working on a Raspberry Pi 5, you're not just an app developer—you're a platform engineer.

1. The Starting Point: The Local Manifest

Before a single line of Java was written, we had to ensure our environment was correct. We used the android-15.0.0_r4 branch. For the Raspberry Pi 5, this required a specific set of local manifests to pull in the Broadcom drivers, kernel headers, and firmware necessary for the Pi’s hardware.

2. Understanding the “System Server”

In Android, the System Server is the heart of the OS. It is a single process that hosts dozens of services (Battery, WiFi, Power, etc.). Our calculatorService will live inside this process.

To make this work, we need three distinct layers:

  1. The Interface (AIDL): The “contract” that defines what the service can do.
  2. The Implementation: The Java class that actually does the math.
  3. The Registration: The line of code that tells the OS, “Hey, I’m here!”

3. Step 1: Defining the Interface (AIDL)

We started by defining our capabilities in AIDL (Android Interface Definition Language). This file is used by the AOSP build system (Soong) to generate the “Stub” and “Proxy” classes that handle the low-level Binder IPC.

File Location: frameworks/base/core/java/android/os/ICalculatorService.aidl

package android.os;

/** * Interface for the Calculator System Service.
 * @hide 
 */
interface ICalculatorService {
    /** Basic Math operations */
    int add(int a, int b);
    int sub(int a, int b);
    int multiply(int a, int b);

    /** Metadata operations for the service */
    void initServiceName(String name);
    String getServiceName();
}

Why this file is special:

  • **@hide**: This tag is vital. It tells the build system that this is an internal system API. Without it, the build would fail because you haven't added the methods to the official public Android SDK (the current.txt file).
  • Package: Placing it in android.os is standard for core OS services.

4. Step 2: Registering the AIDL in the Build System

AOSP is massive. If you just drop a file into a folder, the compiler will ignore it. We had to tell the build system to “look” at our new AIDL file.

The File to Modify: frameworks/base/core/java/Android.bp

We searched for the filegroup named framework-core-sources and added our new path:

filegroup {
    name: "framework-core-sources",
    srcs: [
        "android/os/ICalculatorService.aidl", // <-- Our new "Contract"
        "android/accessibilityservice/IAccessibilityServiceConnection.aidl",
        // ... thousands of other files
    ],
}

The “Permission” Exception

Because Android 15 introduced stricter enforcement for AIDL permissions, we also had to update the main frameworks/base/Android.bp file. We added :framework-core-sources to the enforce_permissions_exceptions list. This allowed us to build the service without immediately implementing complex Manifest-based permissions (like android.permission.CALCULATE).

File: frameworks/base/Android.bp

// ... inside framework-minus-apex-defaults -> aidl -> enforce_permissions_exceptions [cite: 36]
enforce_permissions_exceptions: [
    ":framework-annotations",
    // ...
    ":framework-core-sources", // Ensure this is present
    // ...
],

Part 2: The Heart of the Machine (Implementation & Registration)

Now that we have our “contract” defined via AIDL, we need to build the actual engine. In Part 1, we set the stage; now, we’re going deep into the System Server to breathe life into our calculatorService.

1. Implementing the Logic: The Stub

In the world of Binder IPC, your service implementation doesn’t just “implement an interface.” It extends a Stub. The Stub is a static abstract inner class generated by the AIDL tool that handles the heavy lifting: unmarshalling data from the Binder kernel driver and passing it to your Java methods.

File Location: frameworks/base/services/core/java/com/android/server/calculator/CalculatorService.java

package com.android.server.calculator;

import android.os.ICalculatorService;
import android.util.Slog;

/**
 * The actual implementation of our Calculator.
 * Extends ICalculatorService.Stub to handle Binder transactions.
 */
public class CalculatorService extends ICalculatorService.Stub {
    private static final String TAG = "CalculatorService";
    private String mName = "RaspberryPi-Calc-v15";

    @Override
    public int add(int a, int b) {
        Slog.d(TAG, "Adding " + a + " + " + b);
        return a + b;
    }

    @Override
    public int sub(int a, int b) {
        return a - b;
    }

    @Override
    public int multiply(int a, int b) {
        return a * b;
    }

    @Override
    public void initServiceName(String name) {
        this.mName = name;
        Slog.i(TAG, "Service name re-initialized to: " + name);
    }

    @Override
    public String getServiceName() {
        return mName;
    }
}

Key Technical Details:

  • **Slog (System Log):** We don't use Log.d here. In the system process, we use android.util.Slog. These logs are prioritized and can be filtered specifically using adb logcat -b system.
  • Thread Safety: Binder calls are executed on a thread pool maintained by the system. If our service held shared state (like a running total), we would need to use synchronized blocks or Atomic variables.

2. The Integration: Wiring into SystemServer.java

Your code could be perfect, but it’s a “ghost” until the Android boot sequence recognizes it. The SystemServer is a massive Java process that starts shortly after the Zygote process. It is responsible for starting every major service in the OS.

File to Modify: frameworks/base/services/java/com/android/server/SystemServer.java

We need to tell SystemServer to instantiate our service and register it with the ServiceManager.

import com.android.server.calculator.CalculatorService; [cite_start]// [cite: 1]

// Inside the startOtherServices() method
private void startOtherServices(@NonNull TimingsTraceAndSlog t) {
    // ... around line 3400+ in Android 15

    t.traceBegin("StartCalculatorService");
    try {
        Slog.i(TAG, "Calculator Service");
        // This line places our service in the global registry
        ServiceManager.addService("calculatorService", new CalculatorService()); 
    } catch (Throwable e) {
        Slog.e(TAG, "Failure starting Calculator Service", e);
    }
    t.traceEnd();
}

3. The “Missing Symbol” Trap

This is the moment of frustration for many AOSP newcomers. You run m (make), and the compiler screams: error: cannot find symbol: class CalculatorService.

As we discovered during the build, the services.jar is built using its own Android.bp. Because we created a new package (com.android.server.calculator), We have to ensure two things:

  1. The Import: You must manually add the import statement in SystemServer.java.
  2. The Directory Structure: AOSP’s build system expects the folder structure to exactly match the package name. If your package is com.android.server.calculator, your file must be in .../com/android/server/calculator/.

4. Why We Didn’t Use a “Manager” Yet

You’ll notice we are calling ServiceManager.addService directly. In a standard Android app, you’d expect to call context.getSystemService(Context.CALCULATOR_SERVICE).

To get to that point, we would need to:

  1. Define a CalculatorManager class.
  2. Register a “Service Fetcher” in SystemServiceRegistry.java.
  3. Update the Context class with a new constant.

For now, we kept it “Lean and Mean.” By skipping the Manager for this phase, we ensured that our core Binder logic was 100% solid before adding the complexity of the App Layer.

Part 3: The Silent Guard (SELinux & Verification)

In Parts 1 and 2, we built the engine and wired it to the ignition. But if you try to start a modern Android 15 system on a Raspberry Pi 5 without Part 3, the engine won’t even turn over. Why? Because of SELinux (Security-Enhanced Linux).

1. The Gatekeeper: Understanding SEPolicy

Android 15 is “Secure by Default.” Even though your code is part of the system_server process, the OS doesn't inherently trust your new calculatorService. If you don't explicitly label it, the ServiceManager will block the registration, and any attempt to call it will result in a "Permission Denied" error in logcat.

To fix this, we have to modify the SEPolicy (Security Policy) files. Since we are working on a Raspberry Pi 5, these files are usually found in your device-specific tree (e.g., device/brcm/rpi5/sepolicy/ or the vendor equivalent).

Step 1: Define the Service Type

First, we tell the system that a new type of service exists.

File: service.te

type calculator_service, system_server_service, service_manager_type;
  • **system_server_service**: This tells Android that the service is hosted inside the system_server process.
  • **service_manager_type**: This allows the ServiceManager to index it.

Step 2: Map the Name to the Type

Next, we link the string name we used in SystemServer.java ("calculatorService") to the security label we just created.

File: service_contexts

calculatorService        u:object_r:calculator_service:s0

2. The Final Build: m -j$(nproc)

With the code, the registration, the build configuration, and the security policies in place, it’s time for the final compilation.

From the root of your AOSP directory:

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

Why eng build? The eng (engineering) build flavor is vital for this stage. It leaves the shell unconfined, allowing us to use powerful debugging tools and making the service command accessible without strict production restrictions.

3. The Moment of Truth: Terminal Verification

Once the Pi 5 boots up and you’ve connected via USB or Ethernet, you don’t need a custom app to see if your work paid off. We use the Binder Debugging Tools built into the Android shell.

Action A: The Presence Check

Ask the ServiceManager if it actually has the service in its registry.

adb shell service check calculatorService

Success Response: Service calculatorService: found

adb shell service list | grep calculatorService

Success Response: 75 calculatorService: [android.os.ICalculatorService]

Action B: The Logic Check (Transaction 1)

This is where the magic happens. We call the add(int a, int b) method. Since add was the first method defined in our AIDL, its transaction ID is 1.

# We send '10' and '20' as 32-bit integers (i32)
adb shell service call calculatorService 1 i32 10 i32 20
  • The Result: Result: Parcel(00000000 0000001e '........')
  • Translation: The hex value 1e is 30. The math is being done inside the system process and sent back to your terminal via Binder!

Action C: The Metadata Check (Transaction 5)

Verify the custom name we initialized in the implementation.

adb shell service call calculatorService 5
  • The Result: You will see the hex representation of the string "RaspberryPi-Calc-v15".

4. Final Thoughts: Why This Matters

You have just successfully bypassed the entire Android SDK to build a native feature of the operating system. You’ve touched the AIDL layer, the SystemServer, the Build System, and the Security Policy.

This foundation is exactly what top-tier platform engineers at companies like Mercedes-Benz use to integrate vehicle-specific hardware (like climate control or sensors) into the Android Automotive OS.

Part 3: CalculatorManager

To move from raw terminal commands to a proper Android API, we need to implement the Manager Pattern. This pattern acts as a bridge, allowing any application process to communicate with our system service using a clean, object-oriented API rather than low-level Binder calls.

Here is the implementation to complete the “Control Room” of your project.

1. Create the CalculatorManager.java

The Manager is a client-side proxy. When an app calls manager.add(5, 10), the Manager uses the ICalculatorService interface to send that request across the Binder boundary to your CalculatorService in the system_server.

File Location: frameworks/base/core/java/android/app/CalculatorManager.java

package android.app;

import android.annotation.SuppressLint; // Add this
import android.annotation.NonNull;      // Add this
import android.annotation.SystemService;
import android.content.Context;
import android.os.ICalculatorService;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.Log;

/**
 * Provides access to the system calculator service.
 * Apps can obtain an instance via Context.getSystemService("calculator").
 */
@SystemService(Context.CALCULATOR_SERVICE)
@SuppressLint("UnflaggedApi")
public class CalculatorManager {
    private static final String TAG = "CalculatorManager";
    private final ICalculatorService mService;

    /** @hide */
    public CalculatorManager(Context context, ICalculatorService service) {
        mService = service;
    }

    @SuppressLint("UnflaggedApi")
    public int add(int a, int b) {
        try {
            return mService.add(a, b);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @SuppressLint("UnflaggedApi")
    public int sub(int a, int b) {
        try {
            return mService.sub(a, b);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @SuppressLint("UnflaggedApi")
    public int multiply(int a, int b) {
        try {
            return mService.multiply(a, b);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @SuppressLint("UnflaggedApi")
    @NonNull
    public String getServiceName() {
        try {
            return mService.getServiceName();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }
}

2. Register the Service Constant

To call context.getSystemService("calculator"), the string "calculator" must be defined as a constant in the Context class.

File: frameworks/base/core/java/android/content/Context.java Search for CALCULATOR_SERVICE (or add it among other service constants):

    /** @hide */
    public static final String CALCULATOR_SERVICE = "calculatorService";

3. Map the Manager in SystemServiceRegistry

This is the final “glue.” The registry tells the Android framework how to create the CalculatorManager instance when an app requests it.

File: frameworks/base/core/java/android/app/SystemServiceRegistry.java

  1. Import your Manager:
import android.app.CalculatorManager;
import android.os.ICalculatorService;

2. Register the Service Fetcher: Find the static block where other services are registered and add:

registerService(Context.CALCULATOR_SERVICE, CalculatorManager.class,
        new CachedServiceFetcher<CalculatorManager>() {
    @Override
    public CalculatorManager createService(ContextImpl ctx) throws ServiceNotFoundException {
        IBinder b = ServiceManager.getServiceOrThrow(Context.CALCULATOR_SERVICE);
        ICalculatorService service = ICalculatorService.Stub.asInterface(b);
        return new CalculatorManager(ctx, service);
    }});
  1. You need to add the CalculatorManager.java file to the framework-core-sources filegroup. This ensures the Java compiler includes your Manager when building the framework.jar.

The File to Modify: frameworks/base/core/java/Android.bp

We searched for the filegroup named framework-core-sources and added our new path:

Final block:

filegroup {
    name: "framework-core-sources",
    srcs: [
        "**/*.java",
        "**/*.aidl",
        ":systemfeatures-gen-srcs",
        ":framework-nfc-non-updatable-sources",
        ":messagequeue-gen",
        ":ranging_stack_mock_initializer",
        "android/os/ICalculatorService.aidl", // Add this line (relative to frameworks/base/core/java/)
        // ... other files
        "android/app/CalculatorManager.java", // <--- ADD THIS LINE HERE
    ],
    // Exactly one MessageQueue.java will be added to srcs by messagequeue-gen
    exclude_srcs: [
        "android/os/*MessageQueue/**/*.java",
        "android/ranging/**/*.java",
        ":dynamic_instrumentation_manager_aidl_sources",
    ],
    visibility: ["//frameworks/base"],
}

Why here?

By adding it to framework-core-sources, you are telling the build system that this file is a "non-updatable" core part of the Android OS. This allows other parts of the system (like SystemServiceRegistry) to see the class during compilation.

Here are the specific adb commands and techniques to verify that your CalculatorManager is correctly wired and accessible.

1. Verify Manager Registration via dumpsys

The dumpsys command is the most powerful tool for checking if the Framework has correctly initialized your manager and service.

# This checks if the system recognizes "calculator" as a valid service name
adb shell dumpsys -l | grep calculator

Expected Output: calculator

If you want to see if the system_server has initialized your service properly during the boot sequence:

adb shell dumpsys calculator

Note: Since we haven’t implemented a custom dump() method in our CalculatorService.java yet, this might return nothing or a default message, but it confirms the service is reachable.

Testing the Manager via service call (Advanced)

Even though we have a Manager, the service call command remains your best friend for testing the low-level Binder transactions. Now that you've added the Manager, the method indices in your AIDL still apply.

3. Verification through cmd (The Modern Way)

In newer Android versions like Android 15, many services use the cmd utility. While this requires implementing an onShellCommand in your service, you can verify if the service is "command-aware" by running:

adb shell cmd calculator

If you haven’t implemented shell commands, it will likely return a help message or “Unknown command”.

4. Real-time “Manager-to-Service” Log Monitoring

We know that logs are the ultimate source of truth. Open two terminal windows to watch the communication happen:

Terminal 1 (The Listener):

# Filter for both the Service implementation and the Manager
adb logcat -s CalculatorService CalculatorManager

Terminal 2 (The Trigger):

adb shell service call calculator 1 i32 10 i32 20

output:

arun@arun-Z790-EAGLE-AX:~/aosp-rpi$ adb shell service call calculatorService 1 i32 10 i32 20
Result: Parcel( 00000000 0000001e   '........')
arun@arun-Z790-EAGLE-AX:~/aosp-rpi$

5. Checking the “Service Fetcher” Cache

Because we used CachedServiceFetcher in SystemServiceRegistry.java, the system should only create the Manager once per process. To verify the service is "found" by the registry:

adb shell logcat | grep -i "Calculator"

output:

arun@arun-Z790-EAGLE-AX:~/aosp-rpi$ adb shell logcat | grep -i "Calculator"
03-01 07:39:02.087   671   671 D SystemServerTiming: StartCalculatorService
03-01 07:39:02.088   671   671 V SystemServerTiming: StartCalculatorService took to complete: 1ms
03-01 07:39:41.365  2083  2083 D nativeloader: Configuring clns-shared-7 for other apk /system/system_ext/priv-app/CalculatorTestApp/CalculatorTestApp.apk. target_sdk_version=35, uses_libraries=, library_path=/system/system_ext/priv-app/CalculatorTestApp/lib/arm64:/system/system_ext/priv-app/CalculatorTestApp/CalculatorTestApp.apk!/lib/arm64-v8a:/system/lib64, permitted_path=/data:/mnt/expand:/data/user/0/com.example.picalctest:/system/system_ext/priv-app/CalculatorTestApp:/system/lib64
03-01 07:40:05.170     0     0 E SELinux : avc:  denied  { find } for pid=2083 uid=1000 name=calculatorService scontext=u:r:system_app:s0 tcontext=u:object_r:default_android_service:s0 tclass=service_manager permissive=1
03-01 08:38:40.622  2359  2359 W cmd     : Can't find service calculator
03-01 08:38:41.062     0     0 E SELinux : avc:  denied  { find } for pid=2359 uid=2000 name=calculator scontext=u:r:shell:s0 tcontext=u:object_r:default_android_service:s0 tclass=service_manager permissive

During boot-up, look for any “Failure starting Calculator Service” logs which would indicate an issue in your SystemServer.java registration.

Error: You’ve added new capabilities to the Android SDK that weren’t there before.

In Android, any change to the public API (the android.* packages) must be recorded in a text file called current.txt. This ensures that developers don't accidentally break the SDK.

1. Syncing the API

To tell Android, “Yes, I intentionally added CalculatorManager to the OS," you need to run the specific update command shown in your error log.

Run this command from your AOSP root:

m api-stubs-docs-non-updatable-update-current-api

What this does:

  • It takes the “diff” you see in your terminal (the + public class CalculatorManager part).
  • It writes that information into frameworks/base/core/api/current.txt.
  • This “approves” the API for the rest of the build process.

2. Resume the Build

Once that command finishes (it should be very quick), you can resume your main build. Since you are working on a Raspberry Pi 5, I recommend building the framework and services together to ensure everything is linked:

m framework services

3. Verification of the Build

After this build finishes, your framework.jar will officially contain the CalculatorManager. You can verify this without even flashing the Pi yet by checking the generated current.txt file:

grep -A 5 "public class CalculatorManager" frameworks/base/core/api/current.txt

Expected Output:

arun@arun-Z790-EAGLE-AX:~/aosp-rpi$ grep -A 5 "public class CalculatorManager" frameworks/base/core/api/current.txt
  public class CalculatorManager {
    method public int add(int, int);
    method @NonNull public String getServiceName();
    method public int multiply(int, int);
    method public int sub(int, int);
  }
arun@arun-Z790-EAGLE-AX:~/aosp-rpi$

Note: The frameworks/base/core/api/current.txt file in the Android Open Source Project (AOSP) contains a machine-readable, plain-text description of every public API and protected API class, method, and field provided by the Android framework. This file serves as the API specification for the current version of the Android source code.

m api-stubs-docs-non-updatable-update-current-api

Gemini said

In the AOSP (Android Open Source Project) ecosystem, these commands are part of the Soong build system, which is the successor to the traditional Make system. Since you are working on Android 15 (API 35), these commands are essential for managing the strict API governance and the massive framework structure.

1. m api-stubs-docs-non-updatable-update-current-api

This command is the “official approval” for changes to the Android SDK.

In Part 3, you saw that Metalava (the API auditing tool) detected a difference between your code and the “approved” API files. Every time you add a public class like CalculatorManager or a public method, the build system checks it against text files located in frameworks/base/core/api/current.txt.

What happens when you run this?

  • Documentation Generation: It runs Metalava to scan your source code and generate "stubs" (headers of your classes without the implementation logic).
  • Signature Extraction: It extracts the signature of your new classes (e.g., public class CalculatorManager { method public int add(int, int); }).
  • Text File Sync: It compares this new signature to the existing current.txt. Since yours is new, it updates current.txt to include your CalculatorManager.
  • Validation: It ensures that your changes follow Android’s naming conventions and nullability rules (which is why it previously failed when you missed @NonNull).

1. Use this manager inside a normal Android app

Since you have successfully updated the API and compiled the framework, your Raspberry Pi 5 now officially “knows” what a CalculatorManager is. Now, we shift from being an OS Developer to an Application Developer to consume that new service.

1. The “SDK Problem” and the Workaround

Standard Android Studio uses the public Android SDK. Since CalculatorManager is a custom addition to your OS, the standard SDK won't recognize it. To fix this, you have two options:

  1. The Professional Way: Export your custom framework as a JAR and add it as a library to your project.
  2. The “Hacker” Way (Reflection): Use Java Reflection to call the service by its string name.

For this test, we will use Reflection because it allows you to test immediately without reconfiguring your entire Android Studio SDK.

2. Creating the Android Studio Project

  1. Open Android Studio and create a New Project -> Empty Views Activity.
  2. Set the Language to Java (to match our framework code).
  3. Set the Minimum SDK to API 35 (Android 15).

4. Deploying to the Raspberry Pi 5

  1. Flash your build: Ensure you have flashed the system.img (which contains your framework.jar and services.jar) to your SD card.
  2. Connect via ADB: Make sure your Pi 5 is visible using adb devices.
  3. Run the App: Click the “Run” button in Android Studio.

5. What to look for

When you click the button, several things happen in the background:

  • Your App process calls the system_server process via the Binder driver.
  • The CalculatorService implementation on the Pi 5 performs the math.
  • The result is “parcelled” and sent back to your app.

Troubleshooting with Logcat

If the app crashes or returns an error, run this in your terminal:

adb logcat -s CalculatorService CalculatorManager SystemServer

You should see the Slog.d messages you added to your service logic earlier!

3. Why Reflection is Necessary (The “Developer Context”)

Even though you are an expert in Jetpack Compose and Koin, your Android Studio environment uses a pre-compiled android.jar from Google's servers. Since that JAR doesn't contain your CalculatorManager, code like val calc = getSystemService(CalculatorManager::class.java) will fail to compile in the IDE.

Reflection bypasses the compile-time check and looks for the class at runtime on the Raspberry Pi 5 hardware, where your custom framework.jar resides.

4. Professional Way

[embed]A professional way to use a custom framework to develop Android apps. The “Professional Way” is the industry standard for platform developers — including those at companies like…medium.com

5. Make the system app

create the calculatorTestApp directory inside the packages

/home/arun/aosp-rpi/packages/apps/CalculatorTestApp

create an empty res directory inside calculatorTestApp

/home/arun/aosp-rpi/packages/apps/CalculatorTestApp/res

res directory is not needed in compose app as ui will also be in MainActivity not like xml

create MainActivity.kt

/home/arun/aosp-rpi/packages/apps/CalculatorTestApp/src/com/example/picalctest/MainActivity.kt

MainActivity.kt

package com.example.picalctest

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import java.lang.reflect.Method

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                    CalculatorTestScreen()
                }
            }
        }
    }

@Composable
fun CalculatorTestScreen() {
    var num1 by remember { mutableStateOf("") }
    var num2 by remember { mutableStateOf("") }
    var resultText by remember { mutableStateOf("Ready to test...") }
    var serviceName by remember { mutableStateOf("Unknown") }

    Column(
        modifier = Modifier
            .padding(24.dp)
            .fillMaxSize()
            .verticalScroll(rememberScrollState()),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        Text(text = "AOSP System Service Test", style = MaterialTheme.typography.headlineMedium)
        Text(text = "Target Service: $serviceName", style = MaterialTheme.typography.bodyLarge)

        HorizontalDivider()

        // Input Fields
        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
            OutlinedTextField(
                value = num1,
                onValueChange = { num1 = it },
                label = { Text("Num 1") },
                modifier = Modifier.weight(1f),
                keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
            )
            OutlinedTextField(
                value = num2,
                onValueChange = { num2 = it },
                label = { Text("Num 2") },
                modifier = Modifier.weight(1f),
                keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
            )
        }

        // Action Buttons Grid
        val a = num1.toIntOrNull() ?: 0
        val b = num2.toIntOrNull() ?: 0

        Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
            Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
                Button(modifier = Modifier.weight(1f), onClick = { performOperation("add", a, b) { name, res -> 
                    serviceName = name
                    resultText = "Sum: $res" 
                }}) { Text("Add") }

                Button(modifier = Modifier.weight(1f), onClick = { performOperation("sub", a, b) { name, res -> 
                    serviceName = name
                    resultText = "Difference: $res" 
                }}) { Text("Subtract") }
            }
            Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
                Button(modifier = Modifier.weight(1f), onClick = { performOperation("multiply", a, b) { name, res -> 
                    serviceName = name
                    resultText = "Product: $res" 
                }}) { Text("Multiply") }

                Button(modifier = Modifier.weight(1f), onClick = { performOperation("getServiceName", 0, 0) { name, _ -> 
                    serviceName = name
                    resultText = "Service Info: $name" 
                }}) { Text("Get Info") }
            }
        }

        // Result Display
        Card(
            modifier = Modifier.fillMaxWidth(),
            colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
        ) {
            Text(
                text = resultText,
                modifier = Modifier.padding(16.dp),
                style = MaterialTheme.typography.titleLarge
            )
        }
    }
}

private fun performOperation(methodName: String, a: Int, b: Int, onResult: (String, Any) -> Unit) {
    try {
        val calcManager = getSystemService("calculatorService")
        if (calcManager != null) {
            val clazz = calcManager.javaClass

            // Get Service Name for every call to keep UI updated
            val getNameMethod = clazz.getMethod("getServiceName")
            val name = getNameMethod.invoke(calcManager) as String

            // Call the specific math method
            val result = if (methodName == "getServiceName") {
                name
            } else {
                val mathMethod = clazz.getMethod(methodName, Int::class.java, Int::class.java)
                mathMethod.invoke(calcManager, a, b)
            }

            onResult(name, result)
        } else {
            onResult("Not Found", "Error: Manager is null")
        }
    } catch (e: Exception) {
        onResult("Error", e.message ?: "Unknown Error")
    }
}
/* 
    @Composable
    fun CalculatorTestScreen() {
        var resultText by remember { mutableStateOf("Ready to test...") }
        var serviceName by remember { mutableStateOf("Unknown") }

        Column(
            modifier = Modifier.padding(24.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            Text(text = "AOSP System Service Test", style = MaterialTheme.typography.headlineMedium)
            Text(text = "Target Service: $serviceName", style = MaterialTheme.typography.bodyLarge)

            Divider()

            Button(
                modifier = Modifier.fillMaxWidth(),
                onClick = {
                    val (name, sum) = callSystemCalculator(10, 20)
                    serviceName = name
                    resultText = "10 + 20 = $sum"
                }
            ) {
                Text("Test Add (10 + 20)")
            }

            Card(
                modifier = Modifier.fillMaxWidth(),
                colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
            ) {
                Text(
                    text = resultText,
                    modifier = Modifier.padding(16.dp),
                    style = MaterialTheme.typography.titleLarge
                )
            }
        }
    }
*/
    /**
     * Uses Reflection to talk to the custom CalculatorManager
     */
     /* 
    private fun callSystemCalculator(a: Int, b: Int): Pair<String, Int> {
        return try {
            // 1. Fetch the Manager using the string constant defined in Context.java
            val calcManager = getSystemService("calculatorService") 

            if (calcManager != null) {
                // 2. Reflectively find the methods
                val addMethod: Method = calcManager.javaClass.getMethod("add", Int::class.java, Int::class.java)
                val getNameMethod: Method = calcManager.javaClass.getMethod("getServiceName")

                // 3. Invoke
                val sum = addMethod.invoke(calcManager, a, b) as Int
                val name = getNameMethod.invoke(calcManager) as String

                name to sum
            } else {
                "Not Found" to 0
            }
        } catch (e: Exception) {
            e.printStackTrace()
            "Error: ${e.message}" to -1
        }
    }
        */
}

create Android.bp file

/home/arun/aosp-rpi/packages/apps/CalculatorTestApp/Android.bp

Android.bp

android_app {
    name: "CalculatorTestApp",
    srcs: ["src/**/*.kt"],
    resource_dirs: ["res"],
    manifest: "AndroidManifest.xml",

    // Essential for System Apps
    platform_apis: true,        // Allows access to @hide and internal APIs
    certificate: "platform",    // Signs the app with the system's private key
    privileged: true,           // Places the app in /system/priv-app/ instead of /system/app/
    system_ext_specific: true,  // Recommended for modern AOSP (Android 15)
    min_sdk_version: "35",
    target_sdk_version: "35",

    static_libs: [
        // **Kotlin and AndroidX Core**
        "kotlinx-coroutines-android",
        "androidx.lifecycle_lifecycle-runtime-ktx",
        "androidx.activity_activity-compose",

        // **Compose UI and Foundation (REQUIRED FOR Column)**
        "androidx.compose.ui_ui",
        "androidx.compose.ui_ui-tooling",
        "androidx.compose.ui_ui-tooling-preview",
        "androidx.compose.runtime_runtime", // Crucial: Runtime dependency
        "androidx.compose.foundation_foundation", // Crucial: Contains Column/Row

        // **Compose Material (Used for Button, Text, MaterialTheme)**
        "androidx.compose.material3_material3",
    ],

    optimize: {
        enabled: false,         // Useful during debugging to see clear logs
    },
}

create AndroidManifest.xml

/home/arun/aosp-rpi/packages/apps/CalculatorTestApp/AndroidManifest.xml

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.picalctest"
    android:sharedUserId="android.uid.system"> 
    <application
        android:label="Calculator Test App"
        android:theme="@android:style/Theme.DeviceDefault.NoActionBar">
        <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>

modify device.mk

/home/arun/aosp-rpi/device/brcm/rpi5/device.mk

device.mk

PRODUCT_PACKAGES += CalculatorTestApp

메타데이터
post_id
4ff26fcb1dbb
slug
building-a-custom-system-service-in-android-15-aosp-4ff26fcb1dbb
url
https://medium.com/@aruncse2k20/building-a-custom-system-service-in-android-15-aosp-4ff26fcb1dbb
canonical_url
https://medium.com/@aruncse2k20/building-a-custom-system-service-in-android-15-aosp-4ff26fcb1dbb
author_url
https://medium.com/@aruncse2k20
status
ok
fetched_at
2026-06-29 01:02:39