Android Bound Service is a client-server architecture. It allows Android components (clients) to bind Service (server) to send requests and even perform interprocess communication (IPC). This article will introduce the basic concepts of Bound Service.
The complete code for this chapter can be found in and .
Table of Contents
Overview
Android Service component can be divided into Service and Bound Service. This article will only cover the Bound Service. For Service, please refer to the following articles.
Client calls Context.bindService() to bind server. Where clients are Android components, such as Activity, and server is Service. At the end, the client calls Context.unbindService() to unbind the server. When all clients have called unbind, the system will terminate the service.
When the service has not been started, and a client calls Context.bindService(), the system will start the service and call Service.onCreate() and Service.onBind(). Service.onBind() will return an IBinder. The system will cache this IBinder. After that, when other clients call Context.bindService(), the system will not start the service, nor will it call Service.onBind(), but will pass the cached IBinder to the clients.
There are two types of Bound Services:
- Local Bound Service: A long-standing connection established by other components of the same app.
- Remote Bound Service: A long-standing connection established by other apps.
Local Bound Service
Local Bound Service means that both client and server are in the same process of the same app. This situation is very similar to Background Service. The difference is that the Local Bound Service needs to implement the IBinder interface, and the client can call the public methods of the IBinder.
DownloadBoundService has two inner classes, one is ServiceHandler and the other is DownloadBinder. ServiceHandler inherits Handler to perfom file download, and DownloadBinder implements Binder, and provides an object service
that allows clients to directly access DownloadBoundService. Client then can directly call startDownload()
to start file download.
DownloadBoundActivity binds the service in onStart()
. When it wants to trigger the file download, it can call DownloadBoundService.startDownload()
directly. Then call unbindService()
in onStop()
. If it is a Background Service, then call startCommand() to trigger the file download. In comparison, it is less convenient.
package com.waynestalk.androidboundserviceexample import android.app.Service import android.content.Intent import android.os.* import android.util.Log class DownloadBoundService : Service() { companion object { const val RESULT_CODE_UPDATE_PROGRESS = 7 const val RESULT_PROGRESS = "progress" private const val TAG = "DownloadBoundService" } inner class ServiceHandler(looper: Looper) : Handler(looper) { override fun handleMessage(msg: Message) { Log.d(TAG, "handleMessage: msg.arg1=${msg.arg1} => ${Thread.currentThread()}") for (i in 1..10) { try { Thread.sleep(1000) val resultData = Bundle().apply { putInt(RESULT_PROGRESS, i * 10) } receiver?.send(RESULT_CODE_UPDATE_PROGRESS, resultData) } catch (e: InterruptedException) { e.printStackTrace() } } } } inner class DownloadBinder : Binder() { val service: DownloadBoundService get() = this@DownloadBoundService } private val binder = DownloadBinder() private var looper: Looper? = null private var serviceHandler: ServiceHandler? = null private var receiver: ResultReceiver? = null override fun onCreate() { Log.d(TAG, "onCreate => ${Thread.currentThread()}") val handlerThread = HandlerThread("DownloadService", Process.THREAD_PRIORITY_BACKGROUND) handlerThread.start() looper = handlerThread.looper serviceHandler = ServiceHandler(handlerThread.looper) } override fun onDestroy() { super.onDestroy() Log.d(TAG, "onDestroy => ${Thread.currentThread()}") } override fun onBind(intent: Intent): IBinder { Log.d(TAG, "onBind => ${Thread.currentThread()}") return binder } fun startDownload(receiver: ResultReceiver) { this.receiver = receiver serviceHandler?.obtainMessage()?.let { message -> message.arg1 = 0 serviceHandler?.sendMessage(message) } } }
package com.waynestalk.androidboundserviceexample import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.os.Bundle import android.os.Handler import android.os.IBinder import android.os.ResultReceiver import android.util.Log import androidx.appcompat.app.AppCompatActivity import com.waynestalk.androidboundserviceexample.databinding.DownloadBoundActivityBinding class DownloadBoundActivity : AppCompatActivity() { companion object { private const val TAG = "DownloadBoundActivity" } private var _binding: DownloadBoundActivityBinding? = null private val binding: DownloadBoundActivityBinding get() = _binding!! private var service: DownloadBoundService? = null private var isBound = false private val connection = object : ServiceConnection { override fun onServiceConnected(className: ComponentName, binder: IBinder) { Log.d(TAG, "onServiceConnected: className=${className} => ${Thread.currentThread()}") val serviceBinder = binder as DownloadBoundService.DownloadBinder service = serviceBinder.service isBound = true } override fun onServiceDisconnected(className: ComponentName) { service = null isBound = false } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) _binding = DownloadBoundActivityBinding.inflate(layoutInflater) setContentView(binding.root) binding.downloadWithBoundService.setOnClickListener { if (isBound) { service?.startDownload(UpdateReceiver(Handler(mainLooper))) } else { Log.d(TAG, "Service is not bound yet") } } } override fun onStart() { super.onStart() val intent = Intent(this, DownloadBoundService::class.java) bindService(intent, connection, Context.BIND_AUTO_CREATE) } override fun onStop() { super.onStop() if (isBound) { unbindService(connection) isBound = false } } inner class UpdateReceiver constructor(handler: Handler) : ResultReceiver(handler) { override fun onReceiveResult(resultCode: Int, resultData: Bundle) { super.onReceiveResult(resultCode, resultData) if (resultCode == DownloadBoundService.RESULT_CODE_UPDATE_PROGRESS) { val progress = resultData.getInt(DownloadBoundService.RESULT_PROGRESS) Log.d(TAG, "progress=" + progress + " => " + Thread.currentThread()) binding.progress.text = "$progress %" } } } }
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.waynestalk.androidboundserviceexample"> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.AndroidBoundServiceExample" tools:targetApi="31"> <activity android:name=".MainActivity" android:exported="true" android:label="@string/app_name" android:theme="@style/Theme.AndroidBoundServiceExample.NoActionBar"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <meta-data android:name="android.app.lib_name" android:value="" /> </activity> <activity android:name=".DownloadBoundActivity" /> <service android:name=".DownloadBoundService" /> </application> </manifest>
Remote Bound Service
Remote Bound Service refers to the fact that client and server are in different processes, or even different apps. This is interprocess communication (IPC).
Remote Bound Service provides two ways for client and server to communicate, one is to use Messenger, the other is to use AIDL. In most cases we should use Messenger because it is much easier to implement. If AIDL is used, the implementation is a bit more complicated, and we also have to deal with multithreading. So this article will only describe how to use Messenger.
In DownloadRemoteBoundService, we declare a Messenger and return messenger.binder
in onBind()
. Then the client will get the binder.
In handleMessage()
, whenever a Message is received, if the client has assigned Message.replyTo a Messenger, the service can return the result to the client through it.
In DownloadRemoteBoundActivity, we can see that when a request is sent to the service, it puts the parameters into a Message and assigns Message.replyTo to a Messenger.
Finally, declare the server in AndroidManifest.xml, and set the name of the process in the attribute android:process. If not set, the service and activity will be executed in the same process.
package com.waynestalk.androidboundserviceexample import android.app.Service import android.content.Intent import android.os.* import android.util.Log class DownloadRemoteBoundService : Service() { companion object { const val CMD_UPDATE = 1 const val RES_UPDATE_PROGRESS = 2 const val RES_UPDATE_COMPLETE = 3 private const val TAG = "RemoteBoundService" } inner class IncomingHandler(looper: Looper) : Handler(looper) { override fun handleMessage(msg: Message) { Log.d( TAG, "handleMessage: msg.what=${msg.what} => pid=${Process.myPid()}, ${Thread.currentThread()}" ) when (msg.what) { CMD_UPDATE -> { for (i in 1..10) { try { Thread.sleep(1000) if (!send(msg.replyTo, RES_UPDATE_PROGRESS, i * 10)) return } catch (e: InterruptedException) { e.printStackTrace() send(msg.replyTo, RES_UPDATE_COMPLETE, 0) return } } send(msg.replyTo, RES_UPDATE_COMPLETE, 1) } else -> super.handleMessage(msg) } } private fun send(replyTo: Messenger, command: Int, result: Int): Boolean { return try { val resultMsg = Message.obtain(null, command, result, 0) replyTo.send(resultMsg) true } catch (e: RemoteException) { // The client is dead. e.printStackTrace() false } } } private var looper: Looper? = null private lateinit var messenger: Messenger override fun onCreate() { Log.d(TAG, "onCreate => pid=${Process.myPid()}, ${Thread.currentThread()}") val handlerThread = HandlerThread("DownloadRemoteService", Process.THREAD_PRIORITY_BACKGROUND) handlerThread.start() looper = handlerThread.looper messenger = Messenger(IncomingHandler(handlerThread.looper)) } override fun onDestroy() { Log.d(TAG, "onDestroy => pid=${Process.myPid()}, ${Thread.currentThread()}") } override fun onBind(intent: Intent): IBinder? { return messenger.binder } }
package com.waynestalk.androidboundserviceexample import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.os.* import android.util.Log import androidx.appcompat.app.AppCompatActivity import com.waynestalk.androidboundserviceexample.databinding.DownloadRemoteBoundActivityBinding class DownloadRemoteBoundActivity : AppCompatActivity() { companion object { private const val TAG = "RemoteBoundActivity" } inner class IncomingHandler(looper: Looper) : Handler(looper) { override fun handleMessage(msg: Message) { when (msg.what) { DownloadRemoteBoundService.RES_UPDATE_PROGRESS -> { val progress = msg.arg1 Log.d( TAG, "progress=$progress => pid=${Process.myPid()} ${Thread.currentThread()}" ) binding.progress.text = "$progress %" } DownloadRemoteBoundService.RES_UPDATE_COMPLETE -> { unbindService(connection) } else -> super.handleMessage(msg) } } } private var _binding: DownloadRemoteBoundActivityBinding? = null private val binding: DownloadRemoteBoundActivityBinding get() = _binding!! private var service: Messenger? = null private var isBound = false private var messenger: Messenger? = null private val connection = object : ServiceConnection { override fun onServiceConnected(className: ComponentName, binder: IBinder) { service = Messenger(binder) messenger = Messenger(IncomingHandler(mainLooper)) isBound = true } override fun onServiceDisconnected(className: ComponentName) { service = null messenger = null isBound = false } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) _binding = DownloadRemoteBoundActivityBinding.inflate(layoutInflater) setContentView(binding.root) binding.downloadWithRemoteBoundService.setOnClickListener { if (isBound) { val msg = Message.obtain(null, DownloadRemoteBoundService.CMD_UPDATE, 0, 0) msg.replyTo = messenger try { service?.send(msg) } catch (e: RemoteException) { // The service is crashed. e.printStackTrace() } } else { Log.d(TAG, "Service is not bound yet") } } } override fun onStart() { super.onStart() val intent = Intent(this, DownloadRemoteBoundService::class.java) bindService(intent, connection, Context.BIND_AUTO_CREATE) } override fun onStop() { super.onStop() if (isBound) { unbindService(connection) isBound = false } } }
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.waynestalk.androidboundserviceexample"> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.AndroidBoundServiceExample" tools:targetApi="31"> <activity android:name=".MainActivity" android:exported="true" android:label="@string/app_name" android:theme="@style/Theme.AndroidBoundServiceExample.NoActionBar"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <meta-data android:name="android.app.lib_name" android:value="" /> </activity> <activity android:name=".DownloadRemoteBoundActivity" /> <service android:name=".DownloadRemoteBoundService" android:exported="true" android:process=":downloadRemote"> <intent-filter> <action android:name="com.waynestalk.androidboundserviceexample.Download" /> </intent-filter> </service> </application> </manifest>
In the above example, client and server are executed in the same app but in different processes. In the following example, we will send a request to DownloadRemoteBoundService from another app.
Let’s take look at where the service is declared in AndroidManifest.xml again. We also set the attribute android:exported to true to expose to other app. And declare the intent-filter, so that other apps can call the service through the declared action.
In the following example, we create an intent and set the action and package of the service. Then call bindService() and pass in the intent.
package com.waynestalk.androidboundserviceclientexample import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.os.* import android.util.Log import androidx.appcompat.app.AppCompatActivity import com.waynestalk.androidboundserviceclientexample.databinding.MainActivityBinding class MainActivity : AppCompatActivity() { companion object { const val CMD_UPDATE = 1 const val RES_UPDATE_PROGRESS = 2 const val RES_UPDATE_COMPLETE = 3 private const val TAG = "RemoteBoundActivity" } inner class IncomingHandler(looper: Looper) : Handler(looper) { override fun handleMessage(msg: Message) { when (msg.what) { RES_UPDATE_PROGRESS -> { val progress = msg.arg1 Log.d( TAG, "progress=$progress => pid=${Process.myPid()} ${Thread.currentThread()}" ) binding.progress.text = "$progress %" } RES_UPDATE_COMPLETE -> unbindService(connection) else -> super.handleMessage(msg) } } } private var _binding: MainActivityBinding? = null private val binding: MainActivityBinding get() = _binding!! private var service: Messenger? = null private var isBound = false private var messenger: Messenger? = null private val connection = object : ServiceConnection { override fun onServiceConnected(className: ComponentName, binder: IBinder) { service = Messenger(binder) messenger = Messenger(IncomingHandler(mainLooper)) isBound = true } override fun onServiceDisconnected(className: ComponentName?) { service = null messenger = null isBound = false } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) _binding = MainActivityBinding.inflate(layoutInflater) setContentView(binding.root) binding.downloadWithRemoteBoundService.setOnClickListener { if (isBound) { val msg = Message.obtain(null, CMD_UPDATE, 0, 0) msg.replyTo = messenger try { service?.send(msg) } catch (e: RemoteException) { // The service is crashed. e.printStackTrace() } } else { Log.d(TAG, "Service is not bound yet") } } } override fun onStart() { super.onStart() val intent = Intent("com.waynestalk.androidboundserviceexample.Download") intent.setPackage("com.waynestalk.androidboundserviceexample") bindService(intent, connection, Context.BIND_AUTO_CREATE) } override fun onStop() { super.onStop() if (isBound) { unbindService(connection) isBound = false } } }
Conclusion
Bound Service allows us to easily communicate with Service using IBinder. In addition, it also provides IPC, which allows apps to provide some functionality for other apps to use.