AOSP Framework & Internals
6 min read

API Exposure

Learn about API Exposure.

Why Apps Never Touch Binder Directly

Raw Binder IPC is messy and fragile. If application developers interacted directly with system services, they would have to manually manage Binder tokens and handle IPC failures for every single method call. This approach leaks internal platform architecture directly into the application layer. The Android framework solves this architectural problem by hiding the raw communication behind a client-side Manager class. To keep the complexity hidden, the framework pairs every major system service with a corresponding Manager, such as linking LocationManagerService with a client-facing LocationManager.

Let us look at how an application interacts with a custom system service through this abstraction. This flowchart shows the component hierarchy connecting an app to the underlying service. It helps visualize why the application code never handles raw IPC primitives directly. Pay attention to how the Manager sits completely between the application layer and the system service.

The flowchart clearly shows the application only knowing about the Manager. This intermediary acts as a protective shield. It translates clean, simple method calls into complex inter-process communication behind the scenes.

Building the Manager Class

Creating a clean API requires defining this new Manager class directly in the framework. We need to hide the raw AIDL interface from the public SDK. These classes typically live in frameworks/base/core/java/android/os/. The Manager holds a reference to the raw AIDL interface and acts as a translation layer for exceptions. It wraps every remote call in a standard try-catch block.

package android.os;

import android.content.Context;
import android.os.IMyCustomService;

public class MyCustomManager {
    private final Context mContext;
    private final IMyCustomService mService;

    /** @hide */
    public MyCustomManager(Context context, IMyCustomService service) {
        mContext = context;
        mService = service;
    }

    public int getStatus() {
        try {
            return mService.getStatus();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }
}

The constructor accepts the raw IMyCustomService interface along with the application context. When an application calls getStatus(), the Manager forwards the request across the Binder boundary to the system server. If the system server crashes during execution, the Manager catches the RemoteException and converts it into a standard runtime exception. This specific design guarantees that application developers only handle standard Java exceptions instead of system-level IPC crashes. Our Manager successfully protects the application, but the application still needs a way to find the Manager.

Discovering the Manager Instance

Applications need a reliable way to obtain instances of your newly created Manager. In Android, developers request managers using the Context.getSystemService() method. To make your Manager available through this standard API, you must register it inside SystemServiceRegistry.java. This central registry acts as a factory for every Manager in the platform.

// Inside frameworks/base/core/java/android/app/SystemServiceRegistry.java

static {
    // ... other services ...

    registerService(Context.MY_CUSTOM_SERVICE, MyCustomManager.class,
            new CachedServiceFetcher<MyCustomManager>() {
        @Override
        public MyCustomManager createService(ContextImpl ctx) throws ServiceNotFoundException {
            IBinder b = ServiceManager.getServiceOrThrow("my_custom_service");
            IMyCustomService service = IMyCustomService.Stub.asInterface(b);
            return new MyCustomManager(ctx, service);
        }
    });
}

This registration block tells the framework exactly how to construct the Manager. The framework fetches the raw Binder object from ServiceManager, wraps it in the AIDL stub, and passes it into your Manager constructor. It then caches this instance locally inside the application process. Subsequent calls to getSystemService() return the cached instance immediately without performing another Binder lookup.

Let us trace the exact sequence of events when an application requests your service. This sequence diagram illustrates the lifecycle of a getSystemService() request. It clarifies how the framework translates a simple method call into a fully constructed Manager instance. Watch how the registry queries the service manager and caches the resulting connection.

The sequence shows that the expensive ServiceManager lookup happens only once. Our SystemServiceRegistry handles the heavy lifting of establishing the initial connection. This leaves the application with a ready-to-use object, perfectly isolating it from the system server.

Keeping Your API Private

Adding a custom API to the framework creates a significant visibility problem. Google strictly controls the rigorous process of modifying the public Android SDK. You should rarely add custom OEM features to the public SDK because third-party developers might compile against them. If they do, their applications will crash immediately when running on devices from other manufacturers.

To solve this, AOSP provides JavaDoc annotations that tightly control who can see your new code. Placing @hide on a class or method completely removes it from the generated SDK documentation. It also strips the symbol from the android.jar stub file used by Android Studio. This mechanism effectively prevents third-party applications from compiling against your API.

Sometimes you need to expose your custom API to bundled system applications without leaking it to the public SDK. You achieve this by combining @hide with the @SystemApi annotation. These tags expose the API only to applications signed with the platform certificate or installed in the /system/priv-app/ directory.

Modifying any protected APIs causes your next build to fail because the signatures no longer match the recorded history. To fix these signature mismatches and generate new signature tracking files, you must run an API update command in the root of your AOSP tree. This command outputs updated text files in your framework directory to reflect your specific API changes.

make update-api

Common Mistake: Engineers often forget to commit these updated text files to version control. This oversight instantly breaks the build for everyone else on the team.

By wrapping your raw Binder service in a Manager class, you shield application developers from the complexities of inter-process communication. Registering that Manager makes it easily accessible via standard context lookups, while JavaDoc annotations ensure your APIs remain strictly private. The Android system is highly restrictive, and our service still needs permission to actually interact with hardware and other processes. Understanding how the system enforces these security boundaries is where platform development gets truly interesting.