Different ways Activities communicating with Services on Android

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:

  1. When the service is running the the same process, calling an AIDL interface is simply a direct function call.
  2. 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:

  1. 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.

  2. 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!


See also