Zuhaib

Inter-Process Communication in Android - Lessons & Learnings

SUNDAY APRIL 28 2019 - 8 MIN

Recently, I was tasked with building a solution for OTA (Over-the-Air) updates for one of our products at SodaLabs. Now for those of you who are not familiar with the concept, here's a definition by TechTarget:

An over-the-air update is the wireless delivery of new software or data to mobile devices. Wireless carriers and original equipment manufacturers (OEMs) typically use over-the-air (OTA) updates to deploy firmware and configure phones for use on their networks. The initialization of a newly purchased phone, for example, requires an over-the-air update. With the rise of smartphones, tablets and internet of things (IoT) devices, carriers and manufacturers have also turned to over-the-air updates for deploying new operating systems (OSs) to these devices.

Basically, the updates you receive for your Android, iOS, Windows, Mac or Linux devices are distributed through the mechanism known as OTA programming.

Going the Sandbox Way

We had a couple of choices around how to build this OTA client, but in the end we decided to build it as a completely separate project. The reason was that both the main product and OTA client were equally complex solutions and we didn't want to make it more complex by adding the OTA as part of the main project.

The OTA would essentially be just a service that runs as a background process and communicate with the main application (the product whose updates this OTA was supposed to manage) via AIDL i.e. the IPC mechanism used by Android.

This was the first time I was going to be working with IPC in general. So unsurprisingly, there was a lot of frustration, learning and fixing through trial and error involved.

My goal in this post is to write about the basics and share some insights from my experience so that you can avoid the confusions that I had to face.

Inter-Process Communication

Let's start by understand what Inter-Process communication is. Inter-Process Communication or IPC in short, is a mechanism that allows multiple independent processes to communicate and exchange data.

This communication is often achieved through the use of some shared interfaces defined through Interface Description Language (IDL). What it does is basically establish a set of rules that all the interested processes follow. These shared interfaces serves as a common language that all the parties use to communicate with each other.

How IPC Works In Android

All the applications in Android runs in its own process in a sandbox environment. Which means that each application is independent of others. This isolation helps improve the performance of overall system and in achieving better security through different levels of permissions and access privileges.

Android has its own version of IDL called Android Interface Definition Language (AIDL), which has very much a Java-esque syntax. You basically start by defining an interface (or multiple based on your use case) and then implement the interface in your application and in the client application that wishes to communicate our app.

Understanding The Architecture

One important thing to understand is that IPC communication in Android is also client-server based. Which means that one application has to act as the server while others can interact with it using the interface it exposes.

Generally that Server application is the one you are writing and which you wish to be controlled from outside sources. Following are the components and steps involved:

  • An AIDL interface: You define an interface in your server application containing the API that you want to expose to your clients.

  • Implementation of AIDL interface: Rebuilding your project will generate a java interface from your AIDL file, which you can then use to write a concrete implementation of it.

  • Expose the interface to the clients: Now that you have a concrete implementation of your AIDL interface, it is time to expose it to your clients by binding it with a Service. This Service can be invoked by external applications and then used a communication channel.

  • Exchanging data via IPC: On Android, one process cannot normally access the memory of another process. So to talk, they need to decompose their objects into primitives that the operating system can understand, and marshall the objects across that boundary for you. This means that you can only exchange primitive type data through IPC in the most basic from.

  • Writing the parcelable implementations: Since in most real-world applications you cannot live off just the primitives. Android also provides a mechanism to exchange your Objects through IPC with the use of Parcelables.

These are the 5 main steps required to implement the IPC capabilities in an application. However, admittedly they are not simple and bound to give you a lot of trouble. Let's look at the implementation now.

Off To The Code

Let's build a very basic application that let's you perform addition. To demonstrate the use of data exchange with both primitives and objects, instead of returning a simple integer value for result, we will return a made-up Result object.

Start by creating a simple project with two modules, one serving as a Client while the other as the Server. You can find the link to the complete code at the end of post.

The AIDL Interface

Now that you have a basic setup, let's first define our very simple AIDL interface in which we want to expose a single method called performAddition which takes 2 integer values and returns a made-up Result object.

// IAddition.aidl package com.zuhaibahmad.aidldemo; import com.zuhaibahmad.aidldemo.Result; // Declare any non-default types here with import statements interface IAddition { Result performAddition(int numOne, int numTwo); }

Now as I mentioned above, since we are using a custom object, we need to define a simple AIDL implementation for it as well.

// Result.aidl package com.zuhaibahmad.aidldemo; parcelable Result;

Build the project and if no errors occur, then you should be able to find a java implementation of of your AIDL interface.

Once successful, make sure to copy both of your AIDL files to the client project as well.

Implementation Of The AIDL Interface

Next step is to provide concrete implementation of the AIDL interfaces. For the Result object, all you need is a parcelable POJO for it. However! You need to write it the old school java way.

I know you love Kotlin and must be thinking of using parcelize or at least take advantage of the simpler Kotlin code for data classes, but for some reason Kotlin based parcelables don't work at the time of this writing.

Oh well!

So here's your implementation for Result class in pure java style:

Make sure to copy this class into client's source code as well since it is needed by client to interpret the Result AIDL interface.

package com.zuhaibahmad.aidldemo; import android.os.Parcel; import android.os.Parcelable; public class Result implements Parcelable { private int numOne; private int numTwo; private int result; public Result() {} public Result(int numOne, int numTwo, int result) { this.numOne = numOne; this.numTwo = numTwo; this.result = result; } public int getNumOne() { return numOne; } public void setNumOne(int numOne) { this.numOne = numOne; } public int getNumTwo() { return numTwo; } public void setNumTwo(int numTwo) { this.numTwo = numTwo; } public int getResult() { return result; } public void setResult(int numResult) { this.result = numResult; } @Override public String toString() { return numOne + " + " + numTwo + " = " + result; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeInt(numOne); parcel.writeInt(numTwo); parcel.writeInt(result); } public static final Parcelable.Creator<Result> CREATOR = new Parcelable.Creator<Result>(){ @Override public Result createFromParcel(Parcel parcel) { Result res=new Result(); res.numOne = parcel.readInt(); res.numTwo = parcel.readInt(); res.result = parcel.readInt(); return res; } @Override public Result[] newArray(int size) { return new Result[size]; } }; }

My eyes bleed

Anyways, let's now provide the implementation for our IAddition AIDL interface in the server application:

package com.zuhaibahmad.aidldemo import android.app.Service import android.content.Intent import android.os.IBinder import android.util.Log class AdditionService : Service() { override fun onBind(intent: Intent?): IBinder? { Log.e("server", "Binding Service") return object : IAddition.Stub() { override fun performAddition(numOne: Int, numTwo: Int): Result { return Result(numOne, numTwo, numOne + numTwo) } } } }

While we are at it, let's also register this service in our manifest file. With that our server application will be ready:

<service android:name=".AdditionService" android:enabled="true" android:exported="true" > <intent-filter> <action android:name="com.zuhaibahmad.aidldemo.AdditionService" /> </intent-filter> </service>

Hooking The Service To The Client

Last step is to bind the AdditionService in the server application to the client app and starting it remotely. I have a very basic activity with the following UI for client app:

Client App Screenshot

Let's implement the activity now:

package com.zuhaibahmad.aidldemo import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.os.Bundle import android.os.IBinder import android.support.v7.app.AppCompatActivity import android.util.Log import kotlinx.android.synthetic.main.activity_main.btEquals import kotlinx.android.synthetic.main.activity_main.etNumOne import kotlinx.android.synthetic.main.activity_main.etNumTwo import kotlinx.android.synthetic.main.activity_main.tvResult class MainActivity : AppCompatActivity(), ServiceConnection { private var isBound: Boolean = false private var iRemoteService: IAddition? = null // Called when the connection with the service is established override fun onServiceConnected( className: ComponentName, service: IBinder ) { // Following the example above for an AIDL interface, // this gets an instance of the IRemoteInterface, // which we can use to call on the service Log.e("client", "Service connected!") iRemoteService = IAddition.Stub.asInterface(service) isBound = true } // Called when the connection with the service disconnects override fun onServiceDisconnected(className: ComponentName) { Log.e("client", "Service disconnected!") iRemoteService = null } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) bindService() btEquals.setOnClickListener { onPerformAddition() } } private fun onPerformAddition() { val numOne = etNumOne.text.toString().toInt() val numTwo = etNumTwo.text.toString().toInt() tvResult.text = if (isBound) { val result = iRemoteService?.performAddition(numOne, numTwo) result.toString() } else { "Service not bound!" } } override fun onDestroy() { unbindService() super.onDestroy() } private fun bindService() { Log.e("client", "Attempting to bind service") val serviceIntent = Intent() serviceIntent.setClassName(PACKAGE_NAME, SERVICE_NAME) serviceIntent.action = ACTION_REMOTE_BIND bindService(serviceIntent, this, Context.BIND_AUTO_CREATE) } private fun unbindService() { if (isBound) { // Detach our existing connection. unbindService(this) isBound = false } } companion object { @JvmStatic val PACKAGE_NAME: String = "com.zuhaibahmad.aidldemo" @JvmStatic val SERVICE_NAME: String = "com.zuhaibahmad.aidldemo.AdditionService" @JvmStatic val ACTION_REMOTE_BIND = "$SERVICE_NAME-remote-bind" } }

If all goes well, you should be able to compute your additions through the server app using AIDL interfaces.

Things To Remember

In the end, as you may have realized that this mechanism is not very robust and prone to error. So here are few tips from what I learnt working with Android's IPC about the things that could easily go wrong and make you waste a lot of time.

  • Be very careful about the intent that you use to trigger the service, a minor typo will lead to service never starting.

  • On the same note, if you face a situation at the start where you are not getting the results, verify your service bindings first before diving into debugging. It could just be a faulty service binding and will save you a lot of effort.

  • Remember that ONLY primitive types are allowed, so always make sure that you are not accidentally accepting or returning a non-primitive object.

  • One of the biggest pain was with threading, I never realized that thread would affect IPC as well. Basically, calls made from the local process are executed in the same thread that is making the call. If this is your main UI thread, that thread continues to execute in the AIDL interface. So be very careful if you are using RxJava or any other async mechanism.

  • Remember, Kotlin classes does not work for parcelable implementations with AIDL. At least not at the time of this writing, and what's worse is that it does not give you any error.


Helpful Resources


You can find complete source code for this post here

For suggestions and queries, just contact me.

Zuhaib Ahmad © 2024