Android Bound Service Tutorial

Photo by Hybrid Storytellers on Unsplash
Photo by Hybrid Storytellers on Unsplash
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).

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 .

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like