Interprocess communication and the Binder interface

Stavro XhardhaStavro Xhardha
6 min read

Lately I have been messing around with the internals of Android. While I still enjoy architecting apps 'the proper way,' it all comes down to having a fancy endpoint to receive the data using some fancy parsing tool, have some fancy business logic, and a fancy ViewModel to transform everything from data to a view (or a Composable). The most common use case for such flow is receiving data from an internet server. Android relies on a network protocol which is standardized for most of the devices to make communication between them simple. But how do system components and internals communicate with each other?

Binders

While it is quite normal for an Android phone to rely on the HTTP protocol for network calls, in order for an app to contact another app in the system, using the HTTP protocol is not necessary. The Binder interface is built as a lightweight component to handle every communication within the Android system. It's as simple and as complicated as that. The simple part is that developers do not have to care how the Binder works internally. Google also suggests in their documentation that there is no necessity to work directly with the Binder interface, but rather work on top of the AIDL tool. We will get to AIDL soon. The complicated part is that a Binder, for security reasons, performance reasons, and because it interacts directly with Linux processes for more flexible memory management, this component goes down to the Linux kernel. As the name suggests, "inter-process" communication, this literally means plain OS (in this case Linux) process.

Since an Android developer doesn't have to care much about the internals of Binder interface, a recommended way to work with it from the Android Framework is introduced by Google. This is done through the AIDL (Android Interface Definition Language) interface. AIDL is a language that largely follows Java syntax; every Java developer would likely mistake it for Java at first glance, were it not for the .aidl file extension.

A use case

Let's consider this: Your company building their own version of Android. They want an Authenticator service to work in the system background, and the apps they provide (like mail, calendar, etc.) to authenticate via the same service integrated into the system. For the sake of this example, assume the service is a standard service, not a system service, although little would change in this scenario. Let's explore a minimal example focusing solely on a login use case.

The server

First, make sure your AIDL feature is enabled in build.gradle:

buildFeatures {
        compose = true
        aidl = true
    }

This should happen in both projects. After this, create a new AIDL file. Android Studio will give you an aidl folder with the interface you picked as its name:

And the interface would be:

package com.dispatchersplayground.ipcserver;

interface IAuthenticator {
    int authenticate(String username, String password);
}

If you can read Java, you can read AIDL. Not much going on here—just one functionality that attempts to log the user in. The next step would be to hit the build button. Android Studio will generate a new build folder that also contains Java code specifically designed for the system to use on top of the Binder interface for communication, depending on the interface you created. Let's take a brief look at the generated code:

You don't have to care about implementation details, but what you should know is that a Stub and a Proxy are created to handle the communication. The Stub is the communication implementation of the interface we created, and the Proxy is the component that gives client apps access to this implementation.

Now, let's implement what we want to do after the communication is successful (we will attempt to log the user in). I'll be hardcoding the properties, so this won't be proper authentication:

class AuthenticatorService : Service() {

    private val binder = object : IAuthenticator.Stub() {
        override fun authenticate(username: String?, password: String?): Int {
            return if (username?.isEmpty() == true || password?.isEmpty() == true) {
                -1
            } else 0
        }
    }

    override fun onBind(p0: Intent?): IBinder? = binder.asBinder()
}

The Binder is not a new instance of the IBinder interface, as it typically is for services communicating with activities within the same app. Instead, it is the Stub that was generated for us. Here is where we handle our business logic. By the way, the Binder interface is also used within the same app to establish a handshake between, for instance, an Activity and a Service.

And of course, the AndroidManifest:

<service
            android:name="com.dispatchersplayground.ipcserver.AuthenticatorService"
            android:enabled="true"
            android:exported="true" >
            <intent-filter>
                <action android:name="com.dispatchersplayground.ipcserver.AuthenticatorService" />
            </intent-filter>
        </service>

Client app

The client app will be responsible for pushing a button and authenticating the user using hardcoded values.

                val authResult = remember { mutableStateOf(AuthenticationState.Unauthenticated) }
Surface(...) {
                    Column(...) {
                        Button(onClick = {
                            // TODO contact the remote service
                        }) {
                            Text(text = "Send authentication event")
                        }

                        Text(
                            text = "Result: ${authResult.value}",
                            color = Color.Black,
                            fontSize = 32.sp,
                            fontFamily = FontFamily.Default,
                            fontStyle = FontStyle.Normal
                        )
                    }
                }

Now we have to configure AIDL on the client as well. We need to repeat the same process to include the interface in that app.

And the interface:

package com.dispatchersplayground.ipcserver;

interface IAuthenticator {
    int authenticate(String username, String password);
}

Do you notice something interesting? The package name for the AIDL interface should remain the same as the server it will be contacting. In this case: com.dispatchersplayground.ipcserver.

After building the client project, Android Studio would generate almost the same Java code in the build folder as seen in the screenshots above from the server app.

If it were pre-Android 11, you would be done. However, for security reasons, the Android team has added an extra mechanism. The client's Android Manifest should specify which packages it intends to contact. In this case, we would need to add something like this:

<manifest> 
    ...
    <queries>
        <package android:name="com.dispatchersplayground.ipcserver" />
    </queries>
</manifest>

With that, the setup is complete. Now let's connect the dots.

class MainActivity : ComponentActivity() {
    private lateinit var service: IAuthenticator

    private val serviceConnection = object : ServiceConnection {

        override fun onServiceConnected(componentName: ComponentName?, binder: IBinder?) {
            service = IAuthenticator.Stub.asInterface(binder)
            Toast.makeText(applicationContext, "Service Connected", Toast.LENGTH_SHORT).show()
        }

        override fun onServiceDisconnected(componentName: ComponentName?) {
            Toast.makeText(applicationContext, "Service Disconnected", Toast.LENGTH_SHORT).show()
        }
    }
}

Notice the Binder interface coming from the onServiceConnected callback. That's how these two apps will communicate. On the client side, the interface just needs to be instantiated, which also comes from the generated code.

service = IAuthenticator.Stub.asInterface(binder)

The binding is straightforward, just as it would be if it were the same app:

override fun onStart() {
    super.onStart()
    val intent = Intent("com.dispatchersplayground.ipcserver.AuthenticatorService").apply {
        setPackage("com.dispatchersplayground.ipcserver")
    }

    bindService(intent, serviceConnection, BIND_AUTO_CREATE)
}

override fun onDestroy() {
    super.onDestroy()
    unbindService(serviceConnection)
}

And all that needs to be done is to prepare the UI:

Column(
    modifier = Modifier.fillMaxSize(),
    verticalArrangement = Arrangement.Center,
    horizontalAlignment = Alignment.CenterHorizontally
) {
    Button(onClick = {
        authResult.value = when (service.authenticate(
            "hardcoded_username",
            "hardocded_password"
        )) {
            0 -> AuthenticationState.Authenticated
            -1 -> AuthenticationState.Unauthenticated
            else -> error("Unknown state")
        }
    }) {
        Text(text = "Send authentication event")
    }

    Text(
        text = "Result: ${authResult.value}",
        color = Color.Black,
        fontSize = 32.sp,
        fontFamily = FontFamily.Default,
        fontStyle = FontStyle.Normal
    )
}

Note: When installing, install the server app first.

And the result:

Closing thoughts

Interprocess communication isn't an everyday use case. In fact, some developers may go through their entire careers without encountering it. It's primarily used by product companies that work mostly in the platform or in rare cases where a product consists of multiple apps.

Understanding how Android works internally, not just on the surface, can be very interesting. It shows you what happens behind the scenes in the Android system. This exploration is a great starting point for anyone wanting to learn more about how Android operates inside. However, AIDL, this is just the beginning of a long learning journey.

0
Subscribe to my newsletter

Read articles from Stavro Xhardha directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Stavro Xhardha
Stavro Xhardha

Android Developer with a wide experience in the medical and the automotive sector.