AIDL in Android: Building Inter-Process Communication

AIDL
12 December 2023
Share

AIDL, or Android Interface Definition Language, is a mechanism Android provides to facilitate communication between different apps.

It is similar to other IDLs, IDL in general facilitates communication between unknown Objects or Processes.

What is AIDL?

AIDL is all about defining interfaces that allow one process to communicate with another, regardless of whether they belong to the same app or different apps altogether via interprocess communication (IPC).

How it works?

In Android, one process cannot access the memory of another process so they cannot understand each other, to talk they need to decompose their objects into primitives that the operating system can understand, and Android OS marshals the object across that boundary for you. the code to do that marshaling is complicated to write so Android handles it for you with the help of AIDL.

Each app in Android runs in its processes an app cannot access the memory space of another app this is called application sandboxing.

To allow cross-application communication android provides an implementation of interprocess communication or IPC.

IPC protocol tends to get complicated because all marshalling an unmarshalling of necessary data.

AIDL allows you to define the programming interface that both the client and service agree upon to communicate with each other using inter-process communication, AIDL is a lightweight implementation of IPC using a syntax that is very familiar to JAVA or Kotlin developer and it also provide a tool that automates stub creation.

AIDL Architecture

Understanding Inter-Process Communication (IPC)

Inter-process communication (IPC) refers to the mechanisms and techniques used by different processes to communicate and share data within an operating system or across networked systems.

Types of IPC Mechanisms:

  • Android offers several mechanisms for IPC, each catering to specific requirements:
  • Intent-based Communication: Enables communication between components using intents, but it’s limited to specific scenarios like starting activities or broadcasting messages.
  • Shared Preferences and Files: Allows sharing data via shared preferences or files, yet it might not be suitable for real-time communication or complex data structures.
  • AIDL-based Communication: A robust mechanism specifically designed for RPC (Remote Procedure Call) between processes, offering a structured way to define interfaces and exchange complex data types.

Why AIDL?

In the world of Android, every app typically runs in its own user space, with its instance of the Android Runtime. This architecture ensures both security and stability. However, there are scenarios where apps or services might need to communicate with one another

For instance, two apps have specific functionalities or services common, Rather than creating the same logic for those functionalities. why not just use the functionality that the other application has? This is where AIDL comes into play.

When Use AIDL

  1. Implementing Client-Server Communication:
  • When you have separate processes in your app architecture, such as a background service (server) needing to interact with the UI (client). AIDL helps define interfaces that allow these processes to communicate efficiently.

2. Inter-App Communication:

  • When different apps need to communicate with each other, especially when one app needs to utilize specific functionalities or services provided by another app. AIDL enables the definition of interfaces that enable this interaction securely.

3. Sharing Data Across Processes:

  • When you need to share data between different components of an app running in separate processes, AIDL helps define the structure and rules for exchanging this data efficiently and securely.

4. Implementing Remote Method Invocation:

  • AIDL is used to define methods that can be invoked remotely across different processes. This is essential when you want to trigger actions or exchange data between disparate components of your app.

Remember, AIDL is particularly useful in scenarios involving IPC (Inter-Process Communication) and when you need to define structured interfaces for communication between different parts of your app or even between different apps on the Android platform.

Client-Server AIDL app

This app consists, of

Server app

The server app provides the following background functionality,

  1. isServiceSatarted() – This function returns the Boolean value of whether is service starts or not.
  2. getColor() – This function sends a random color to clients.

Client app

This app gets the functionality from the server i.e the logic part and updates the UI via the AIDL interface.

Implementation of AIDL

1. Gradle configuration

To use AIDL (Android Interface Definition Language) in your project, you typically need to enable the AIDL feature.

android {
    // Other configurations...

   buildFeatures {
        aidl true
    }
}

Server app

Note: This server app has no activity (which means no UI) so this app is doing only background operations.

2. Define the Interface

Create AIDL interface IColorAidl, This interface specifies the methods that the client can call on the server and vice versa. It’s crucial to define the interface in a .aidl file.

package rishiz.com.aidl_server;

    interface IColorAidl {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    boolean isServiceSatarted();
    int getColor();
}

3. Stub Generation

When you define an AIDL interface in Android, the system generates a “stub” for that interface. This stub acts as a proxy for the actual object residing in a different process.

To generate stub, Rebuild the project,

After rebuilding, the Android framework automatically generates the stub on the below path.

4. Implement the Stub for the Server-side Service

The Android SDK tools generate an interface in the Java programming language based on your .aidl file. This interface has an inner abstract class named Stub that extends Binder and implements methods from your AIDL interface. You must extend the Stub class and implement the methods.

 private val binder: IColorAidl.Stub = object : IColorAidl.Stub() {
        override fun isServiceSatarted(): Boolean {
            return true
        }

        override fun getColor(): Int {
            val rnd = Random.Default //kotlin.random
            return Color.argb(255, rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256))
        }
    

Implement an Service and override onBind() to return your implementation of the Stub class, Create the service named AidlService, and implement onBind that returns the binder object.

See Android App Components to know Services in detail.

package rishiz.com.aidl_server

import android.app.Service
import android.content.Intent
import android.graphics.Color
import android.os.IBinder
import android.util.Log
import kotlin.random.Random

class AidlService : Service() {
    companion object {
        private val TAG = AidlService::class.java.canonicalName
    }

    override fun onCreate() {
        super.onCreate()
        Log.d(TAG, "onCreate")
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        super.onStartCommand(intent, flags, startId)
        Log.d(TAG, "onStartCommand")

        return START_STICKY
    }

    override fun onBind(intent: Intent?): IBinder {
        Log.d(TAG, "onBind")
        return binder
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d(TAG, "onDestroy")
    }

    private val binder: IColorAidl.Stub = object : IColorAidl.Stub() {
        override fun isServiceSatarted(): Boolean {
            return true
        }

        override fun getColor(): Int {
            val rnd = Random.Default //kotlin.random
            return Color.argb(255, rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256))
        }
    }
}

Override onBind methods return the binder and other callback methods of service.

Add this service to the manifest.

 <service
            android:name=".AidlService"
            android:exported="true">
<!--            <intent-filter>-->
<!--                <action android:name="IService"/>-->
<!--            </intent-filter>-->
        </service>

Here you can add intent action if needed so that the client app uses this action to bind this service this action else service can bind with a name, this will clear later.

Client App

1.Add dependencies

Add the following dependencies,

// LiveData
    def lifecycle_version = "2.2.0"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
    //For circular view
    implementation 'com.mikhaellopez:circleview:1.4.1'

2. UI

Include the following views,

  1. Two text views one is to display text ServiceStarted and another for showing the service started status.

2. Circular imageview as per need.

3. One button to on the circular lights.

3.Create AIDL interface

Make one package with same name as your server app’s package name in which your AIDL file resides and, in that put, same “.aidl” files of server.

4. Bind to a service

On the client side (e.g., an Activity or another Service), bind to the AidlService to access its methods:

We have create LocalService to bind AidlService.

Application components (clients) can bind to a service by calling bindService(). The Android system then calls the service’s onBind() method, which returns an IBinder for interacting with the service.

The binding is asynchronous, and bindService() returns immediately without returning the IBinder to the client. To receive the IBinder, the client must create an instance of ServiceConnection and pass it to bindService(). The ServiceConnection includes a callback method that the system calls to deliver the IBinder.

To bind to a service from your client, follow these steps,

1.Implement ServiceConnection.

Your implementation must override two callback methods:

onServiceConnected()

  • The system calls this to deliver the IBinder returned by the service’s onBind() method.

onServiceDisconnected()

  • The Android system calls this when the connection to the service is unexpectedly lost, such as when the service crashes or is killed. This is not called when the client unbinds.
private var aidlserviceConnection: ServiceConnection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            Log.d(TAG, "Connected to aidlService")
            if (iColorAidl == null) {
                iColorAidl = IColorAidl.Stub.asInterface(service)
            }
        }

        override fun onServiceDisconnected(name: ComponentName?) {
            Log.d(TAG, "Disconnected from aidlService")
            iColorAidl = null
        }

    }

2.Call bindService(), passing the ServiceConnection implementation

With this ServiceConnection, the client can bind to a service by passing it to bindService(), as shown (recommded to bind onCreate())

I. If Intent action not provide

Intent().setClassName("rishiz.com.aidl_server","rishiz.com.aidl_server.AidlService")
    .also{ 
        bindService(it, aidlserviceConnection, Context.BIND_AUTO_CREATE) 
    }

II. Passing intent action.

Intent("AidlService").setPackage("rishiz.com.aidl_server")
    .also{
        bindService(it, aidlserviceConnection, Context.BIND_AUTO_CREATE)
}

3.When the system calls your onServiceConnected() callback method, you can begin making calls to the service, using the methods defined by the interface.

4. To disconnect from the service, call unbindService()

It is a better practice to unbind the client as soon as it is done interacting with the service.

Manage the lifecycle of a bound service.

5.Create a bound service

Our intention is to make LocalService as Service that provides binding.

If only the local application uses your service and it doesn’t need to work across processes, then you can implement your own Binder class that provides your client direct access to public methods in the service.

Here’s how to set it up:

  1. In your service, create an instance of Binder that does one of the following:
    • Contains public methods that the client can call.
    • Returns the current Service instance, which has public methods the client can call.
    • Returns an instance of another class hosted by the service with public methods the client can call.
  2. Return this instance of Binder from the onBind() callback method.
  3. In the client, receive the Binder from the onServiceConnected() callback method and make calls to the bound service using the methods provided.
class LocalService : Service() {
    // Binder given to clients.
    private val binder = LocalBinder()

    /**
     * Class used for the client Binder. Because we know this service always
     * runs in the same process as its clients, we don't need to deal with IPC.
     */
    inner class LocalBinder : Binder() {
        // Return this instance of LocalService so clients can call public methods.
        fun getService(): LocalService = this@LocalService
    }

    override fun onBind(intent: Intent): IBinder {
        return binder
    }
}

Final LocalService Code:

class LocalService : Service() {
    var iColorAidl: IColorAidl? = null
    private val binder = LocalBinder()

    companion object {
        private val TAG = LocalService::class.java.canonicalName
    }

    inner class LocalBinder : Binder() {
        // Return this instance of LocalService so clients can call public methods.
        val service: LocalService
            get() = this@LocalService
    }

    private var aidlserviceConnection: ServiceConnection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            Log.d(TAG, "Connected to aidlService")
            if (iColorAidl == null) {
                iColorAidl = IColorAidl.Stub.asInterface(service)
            }
        }

        override fun onServiceDisconnected(name: ComponentName?) {
            Log.d(TAG, "Disconnected from aidlService")
            iColorAidl = null
        }

    }

    @RequiresApi(Build.VERSION_CODES.N)
    override fun onCreate() {
        super.onCreate()
        Log.d(TAG, "onCreate")
        //If intent action not provided for server side service  
        Intent().setClassName("rishiz.com.aidl_server", "rishiz.com.aidl_server.AidlService")
            .also {
                bindService(it, aidlserviceConnection, Context.BIND_AUTO_CREATE)
            }
        //If intent action provided for server side service
        Intent("AidlService").setPackage("rishiz.com.aidl_server")
            .also {
                bindService(it, aidlserviceConnection, Context.BIND_AUTO_CREATE)
            }
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        super.onStartCommand(intent, flags, startId)
        Log.d(TAG, "onStartCommand")
        return START_STICKY
    }

    override fun onBind(intent: Intent?): IBinder {
        Log.d(TAG, "onBind")
        return binder
    }

    override fun onUnbind(intent: Intent?): Boolean {
        Log.d(TAG, "onBind")
        return super.onUnbind(intent)
    }

    override fun onRebind(intent: Intent?) {
        Log.d(TAG, "onRebind")
        super.onRebind(intent)
    }

    override fun onDestroy() {
        Log.d(TAG, "onRebind")
        unbindService(aidlserviceConnection)
        super.onDestroy()
    }

    fun isServiceStarted(): Boolean? {
        return iColorAidl?.isServiceSatarted
    }

    fun getColor(): Int? {
        return iColorAidl?.color
    }
}

The LocalBinder provides the getService() method for clients to retrieve the current instance of LocalService. This lets clients call public methods in the service.

6.Bound Service with MVVM

Implement the service connections methods and receive the Binder from the onServiceConnected() callback method and make calls to the bound service methods.

class MyViewModel: ViewModel() {
    private val mBinder:MutableLiveData<LocalService.LocalBinder?> = MutableLiveData()
    companion object{
        private val TAG=MyViewModel::class.java.canonicalName
    }

    private val localServiceConnection:ServiceConnection=object:ServiceConnection{
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            Log.d(TAG,"Connected to LocalService")
            val binder = service as LocalService.LocalBinder
            mBinder.postValue(binder)
        }

        override fun onServiceDisconnected(name: ComponentName?) {
            Log.d(TAG,"Disconnected to LocalService")
            mBinder.postValue(null)
        }
    }

    fun getServiceConnection(): ServiceConnection {
        return localServiceConnection
    }

    fun isServiceStarted(): Boolean? {
        return mBinder.value?.service?.isServiceStarted()
    }

    fun getColor(): Int? {
        return mBinder.value?.service?.getColor()
    }
}

7.MainActivity

OnCreate Defined  ViewModel variable ,Start The LocalService and bind it.

Initialize the views.

class MainActivity : AppCompatActivity() {
    companion object {
        private val TAG = MainActivity::class.java.canonicalName
    }

    private lateinit var viewModel: MyViewModel


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.d(TAG, "onCreate")
        viewModel = ViewModelProvider(this)[MyViewModel::class.java]
        
        Intent(this,LocalService::class.java)
            .also{serviceIntent->
                startService(serviceIntent)
                bindService(serviceIntent, viewModel.getServiceConnection(), BIND_AUTO_CREATE)
            }

        val btn = findViewById<Button>(R.id.button)
        val isServiceStart = findViewById<TextView>(R.id.isStart)
        btn.setOnClickListener {
            isServiceStart.text = viewModel.isServiceStarted().toString()
            circularView1()
            circularView2()
            circularView3()
            circularView4()
            circularView6()
            circularView7()
            circularView8()
            circularView9()

        }

    }

    override fun onStart() {
        super.onStart()
        Log.d(TAG, "onStart")

    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d(TAG, "onDestroy")
        unbindService(viewModel.getServiceConnection())
    }

    private fun circularView1() {
        val circleView = findViewById<CircleView>(R.id.circleView1)
        circleView.apply {
            // Set Color
            circleColor = viewModel.getColor()!!
            // or with gradient
            circleColorStart = viewModel.getColor()!!
            circleColorEnd = viewModel.getColor()!!
            circleColorDirection = CircleView.GradientDirection.TOP_TO_BOTTOM

            // Set Border
            borderWidth = 10f
            borderColor = viewModel.getColor()!!
            // or with gradient
            borderColorStart = viewModel.getColor()!!
            borderColorEnd = viewModel.getColor()!!
            borderColorDirection = CircleView.GradientDirection.TOP_TO_BOTTOM

            // Add Shadow with default param
            shadowEnable = true
            // or with custom param
            shadowRadius = 15f
            shadowColor = viewModel.getColor()!!
            shadowGravity = CircleView.ShadowGravity.CENTER
        }
    }
                      .
                      .   //Do create same circular view methods for other views.
                      .   // If not getting then full code is on github
}

Download Code

Server app: rishiroid/android-aidl_server (github.com)

Client app: rishiroid/android-aidl_client (github.com)