In this article, we present different ways Activities could use to communicate with Services on Android.
1. Using Intent
1.1. Context.startService()
The simplest way to communicate with a Service
is to send an Intent
through Context.startService()
, e.g.:
class MyActivity : Activity() {
override fun onStart() {
super.onStart()
startService(Intent(this, MyService::class.java).putExtra("key", 777))
}
}
class MyService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
println("onStartCommand: ${intent?.getIntExtra("key", -1)}")
return super.onStartCommand(intent, flags, startId)
}
}
This works when the service is running in the same process, or in a separate process. However, Service
can’t use Context.startActivity()
to send an intent back to the same activity instance. Also, it is an expensive operation, which can take several milliseconds of CPU time.
1.2. Context.sendBroadcast()
A similar way is to use Context.sendBroadcast()
to send an intent, e.g.:
class MyActivity : Activity() {
override fun onStart() {
super.onStart()
// assume the service is started, and already registered the receiver
sendBroadcast(Intent("test_action").putExtra("key", 777))
}
}
class MyService : Service() {
private val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
println("onReceive: ${intent.getIntExtra("key", -1)}")
}
}
override fun onCreate() {
super.onCreate()
registerReceiver(broadcastReceiver, IntentFilter("test_action"))
}
}
This also works when the service is running in the same process, or in a separate process. Service
can also send an intent back to activity using the same approach.
If the service is running in the same process, an alternative is to use LocalBroadcastManager or other observer pattern tools.
However, using broadcast or event bus is a layer violation that any component in the app could listen to events from anywhere. Use with caution.
2. Bound Service
2.1. Extending Binder
We can implement own Binder
class to provide direct access to the service instance:
class MyService : Service() {
// this class will be given to the client when the service is bound
// client can get a reference to the service through it
class MyBinder(val service: MyService): Binder()
private val binder = MyBinder(this)
override fun onBind(intent: Intent?): IBinder? = binder
fun doSomething() {
println("do something...")
}
}
class MyActivity : Activity() {
private var myService: MyService? = null
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
// the service is connected, we can get a reference to it
myService = (service as MyService.MyBinder).service
// call a public method
myService?.doSomething()
}
override fun onServiceDisconnected(name: ComponentName) {
myService = null
}
}
override fun onStart() {
super.onStart()
// bind the service, and start it if needed
bindService(Intent(this, MyService::class.java), connection, BIND_AUTO_CREATE)
}
override fun onStop() {
// disconnect from the service, and nullify the reference
unbindService(connection)
myService = null
super.onStop()
}
}
This is very efficient (it’s direct function calls), and easy to build two-way communications through listeners or callbacks. However, this approach only works when the service is running in the same process.
2.2. Using Messenger
We talked about this approach in a previous article, Loopers and Handlers in Android. Here I’m extending it to support a two way communication:
class MyService : Service() {
// the Handler to handle incoming messages
private val handler = object : Handler() {
override fun handleMessage(msg: Message) {
println("MyService handleMessage: msg - $msg")
msg.replyTo.send(Message.obtain().apply { what= 777777 })
}
}
// the created Messenger will dispatch incoming messages to the handler
private val incoming = Messenger(handler)
// when someone binds to the service, we return the IBinder backing the
// messenger, so that the peer can use to send messages
override fun onBind(intent: Intent?): IBinder? = incoming.binder
}
class MyActivity : Activity() {
// handle incoming messages
private val handler = object : Handler() {
override fun handleMessage(msg: Message) {
println("MyActivity handleMessage: msg - $msg")
}
}
private val incoming = Messenger(handler)
private var outgoing: Messenger? = null
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
// when the service is connected, we create the Messenger to send messages
outgoing = Messenger(service)
// send a message
outgoing?.send(Message.obtain().apply {
replyTo = incoming // passing the messenger to the service
what = 777
})
}
override fun onServiceDisconnected(name: ComponentName) {
// nullify the messenger in case the service is disconnected unexpectedly,
// e.g. the remote process crashes
outgoing = null
}
}
override fun onStart() {
super.onStart()
// bind the service, and start it if needed
bindService(Intent(this, MyService::class.java), connection, BIND_AUTO_CREATE)
}
override fun onStop() {
// disconnect from the service, and nullify the messenger
unbindService(connection)
outgoing = null
super.onStop()
}
}
If we want to allow the service to actively push messages to the client, we can e.g. put the Messenger
in the Intent
and send it to the service using Context.startService()
:
class MyService : Service() {
private var outgoing: Messenger? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
outgoing = intent?.getParcelableExtra("messenger")
return super.onStartCommand(intent, flags, startId)
}
}
class MyActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startService(Intent(this, RemoteService::class.java).putExtra("messenger", incoming))
}
...
}
Using Messenger
works when the service is running in the same process, or in a separate process. However, you will need to maintain the protocol for the messages passed back and forth.
2.3. Using AIDL
To use AIDL, we first need to create an .aidl file:
package com.example.myapplication;
interface IMyInterface {
long getTid();
}
When building the project, it will generate a helper abstract class IMyInterface.Stub
for us to implement:
class MyService : Service() {
private val binder = object : IMyInterface.Stub() {
override fun getTid(): Long = Thread.currentThread().id
}
override fun onBind(intent: Intent?): IBinder? = binder
}
Finally, we can use it in the activity:
class MyActivity : Activity() {
private var myInterface: IMyInterface? = null
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
// get a reference to IMyInterface
myInterface = IMyInterface.Stub.asInterface(service)
// call a public method
println("Thread info: ${myInterface?.threadInfo}")
}
override fun onServiceDisconnected(name: ComponentName) {
myInterface = null
}
}
override fun onStart() {
super.onStart()
// bind the service, and start it if needed
bindService(Intent(this, MyService::class.java), connection, BIND_AUTO_CREATE)
}
override fun onStop() {
// disconnect from the service, and nullify the reference
unbindService(connection)
myInterface = null
super.onStop()
}
}
Similarly, if we create another AIDL interface, and pass it from the activity to the service, we can enable the service calling methods in the activity.
Using AIDL works when the service is running in the same process, or in a separate process. However, we need to be careful with the threading:
- When the service is running the the same process, calling an AIDL interface is simply a direct function call.
- However, when the service is running in a separate process, the call will be dispatched on one of the binder threads.
Conclusion
In this article, we discussed different approaches to enable communications between activities and services. We can use the following rules to decide which approach to use:
When we need to occasionally send a command to a service, use
Context.startService()
. For example, a workout tracking app can use this approach to start or stop a workout.When we need to communicate between activities and services frequently. For example, we want to regularly receive stats update from the workout tracking app.
if the service is running in the same process, extend the
Binder
class.if the service is running in a separate process…
if we don’t want to manage threads, use
Messenger
.if we don’t want to maintain the protocol for messages sent back and forth, use AIDL.
Hope this helps and happy coding!