Golang Logo

Go/Golang Training

Unser Hands-On-Einstieg in die Entwicklung mit Go. Nächster Termin: 14.- 15.05. in Köln – jetzt buchen!
Zum Training 
A stack of files in front of the Android logo
Apps

Using Kotlin APIs for Android Services with AIDLs

Lesezeit
8 ​​min

How can we design modern and concurrent Kotlin APIs for our Android-bound service which come with its Binder and AIDL constraints?

When implementing services in Android it often comes to the point where we need to create an API for other apps and processes which consume our service. This is often the case in Android embedded or automotive projects, whenever we have hardware abstractions or provide headless functionality. But also in regular app development, we might want to provide an interface for other apps to consume.

The practical approach to implementing an API across applications with potentially multiple consumers calling concurrently is an Android-bound service with an AIDL interface. However, this RPC-style interface bears a couple of challenges that need to be considered when implementing our API. The most important ones are:

  • You can only pass Primitives, Bundles (Parcelable objects), or IInterfaces in your AIDL interface
  • You should avoid long-running (blocking) calls to prevent running out of Binder threads in your service

This is especially important when we want to wrap this into a nice Kotlin API with suspending calls and flows. Let me show you how to make these concurrent calls possible. But first, we need an example use case to work on.

Building a demo

Let’s imagine our app being a general-purpose chatbot AI you can chat with in a standalone application. In addition, our app provides an API with a lib for third-party apps to offer our service to their customers, like for example in an E-Commerce app or a travel booking app to ask the bot something in their context. For this, I implemented a basic demo with the following architecture:

Shows the basic architecture of the demo application with three gradle modules, :service, :lib and :demo.We have these three modules in our demo:

  • :service – which contains the Service Application with UI and the BotService to provide the functionality to others
  • :lib – helps the client side for easy connection to the BotService and defines the common API
  • :client – contains the Client Application with UI and utilizes the lib to connect to the Service Application

You can find the example in our GitHub repository. For demonstration purposes, I basically put all messages into a datastore and the bot answers a predefined answer after two seconds. Both apps look alike, except for a headline for easy identification and the client application has the ability to clear the whole chat:

Shows both apps for our demo, both with a basic chat dialog containing the same messages.
Shows both apps for our demo, both with a basic chat dialog containing the same messages.

For the API let’s say we can start a new chat session, query the bot details, send a message and observe incoming messages. To make that comfortable to use we decide upon the following Kotlin API with suspending calls and Flows:

Unfortunately, these concurrent Kotlin features are not available in the Android Interface Definition Language (AIDL), so how can we do that across AIDL? Let’s take a look at each method at a time.

Trigger calls across Binder

A regular function like newSession() or sendMessage() without a result can basically be declared and called in a coroutine, since we do not need to wait for any result to return. This ensures that the binder thread of the service will not be blocked. The only thing required here is that Message needs to implement Parcelable, which can be easily done with the parcelize gradle plugin.

Sidenote: Using the oneway declaration here does not solve the binder thread problem and may introduce race conditions when calling these methods directly after another, since they may be executed in a different order.

Suspending calls across Binder

Concurrent calls which return only one result can be implemented with binder callbacks. For getBotDetails() we introduced a Parcelable data class BotDetails and an additional callback aidl, BotDetailsCallback.aidl. With suspendCoroutine we can nicely cover that callback mechanism in a clean suspending function.

Flow across Binder

To pass a continuous flow of elements across the Binder interface, like in getMessages(), we use the same approach as for suspending calls, but now with registering and unregistering calls in our AIDL. We also define a Parcelable class Message and its callback Interface, MessagesCallback.aidl. As for suspending functions, we can hide these callback mechanics with a callbackFlow Flow builder. And with awaitClose we ensure a clean unregistration when the flow is not active anymore.

As you can see in the service implementation we have not yet published any data into the callbacks, but instead, we use a RemoteCallbackList to handle the “grunt work of maintaining a list of remote interfaces“. It’s worth taking a look at that documentation to understand the underlying work. But in short, there are a couple of edge cases, like dying clients, which need to be handled to not send values to already dead bindings.

In this implementation messagesCallbackList is a self-implemented extension of RemoteCallbackList called SubscribingRemoteCallbackList which also keeps track of collecting from a given flow and broadcasting the data to all registered callbacks. The declaration looks like this:

And here you can find the implementation of it. This ensures that the given flow, getMessages() in our case, is only hot when one or more callbacks are registered.

One side note for all the shown examples above: Be aware that we still need to cover and expect RemoteExceptions on every single Binder call!

Service Connection with Kotlin Flows

Although one typically has Manager classes in place to provide an easy interface for service connections, it sometimes is tricky to handle all potential edge cases, like occasionally connection loss and automatic Binder re-connections. Also, we want to avoid new service connections on every single call and we want to have something like a re-connection mechanic when a binder connection fails due to flaky HAL implementations. To tackle all these requirements, I often suggest the following pattern:

You can find the full repository implementation and how to use it here.

Let’s go through this snipped step by step:

  1. We initialize a flow with callbackFlow to connect to the service using the manager.
  2. We pass the interface instance, or a success indicator (depending on the manager implementation) on a successful connection.
    And raise exceptions or cancel the flow when the connection failed or got lost.
  3. With awaitClose we ensure that the service connection is closed whenever the flow has completed.
  4. With retry(3) we want to wait and retry the service connection on failure three times.
  5. Even if the retry failed three times we need to handle and materialize the error case with catch, since we cannot pass error cases in shared flows.
  6. We convert the flow into a sharedFlow with the following characteristics:
    • The flow is shared with the WhileSubscribed strategy: While we have consumers, we want to keep the connection alive and shared.
    • Even when the last subscriber has unsubscribed, it keeps it alive for 10 more seconds, to avoid re-connections on every single call.
    • Replay is not cached to avoid calls on unconnected instances.

There might be one edge case in this pattern when a connection is lost in between and cannot be reestablished by retry. Then it might happen that AIDL calls are made on an unconnected instance during the retry, which needs to be cached. Nevertheless, this pattern could be a resilient starting point for your service connection.

In combination with the getMessages() flow we now can have a flow, which establishes a connection, subscribes to messages via AIDL, and is capable of surviving service re-connections under the hood:

Conclusion

With basic usage of Kotlin builders like suspendedCoroutine and callbackFlow we are able to map AIDLs legacy callback APIs into modern and clean Kotlin APIs. We can also streamline and simplify the use of such APIs with sharedFlow and basic flow mechanics.

Hat dir der Beitrag gefallen?

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert