As a newcomer to Android, one of the things that’s surprisingly tricky is communicating with a Service (an application component for long-running operations) from other parts of your application. You can’t simply call methods on it – you have to come up with a way to broadcast messages.

Background

On the Services guide, there’s three types of services: Scheduled, Started, and Bound. Scheduled services are for periodic operations, which isn’t relevant to my interests right now.

My use case is this: I want the phone to open a socket and run a TCP server for an indeterminate amount of time. The application should be able to send messages to clients connected to this server. The server shouldn’t go down when the application is minimized. So which type of service do I want?

At first, a bound service seems like it might be what I want because it’s easy to send messages to:

A bound service offers a client-server interface that allows components to interact with the service (good), send requests (good), receive results (good), and even do so across processes with interprocess communication (IPC) (eh). A bound service runs only as long as another application component is bound to it (bad). Multiple components can bind to the service at once (eh), but when all of them unbind, the service is destroyed (bad).

Well, what about the Started type?

After it’s started, a service can run in the background indefinitely (good), even if the component that started it is destroyed (good). Usually, a started service performs a single operation (eh) and does not return a result to the caller (eh). For example, it can download or upload a file over the network. When the operation is complete, the service should stop itself (good).

I’m not sure if starting a long-running server counts as a single operation, but perhaps we could consider it to be.

At this point, I’m thinking that while the interface that a bound service provides sound enticing, overall it has too many undesirable characteristics. So that leaves the Started type. How do we communicate with it?

LocalBroadcastManager

As you probably know, Android has this thing called BroadcastReceiver, which I’ve taken to be basically applications shouting to each other. But we don’t want applications conversing, but rather parts of the same application conversing with other parts. LocalBroadcastManager facilitates exactly this.

Concept

Here’s the concept and procedure we’re going to follow in the next steps. There’s a Service and one or more Activities that want to communicate with the service. The Service and Activities will be decoupled – neither will retain a reference to the other.

  1. Activity start the Service.
  2. Service signals to the Activity that it’s ready to go.
  3. Activity starts sending messages to the Service.
  4. (Optional) Service sends messages back.

Code

Alright, lets get to the code. The following examples are in Kotlin, but the concept should hopefully be clear enough.

Activity

class MyActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    // Get a reference to the LocalBroadcastManager
    val localBroadcastManager = LocalBroadcastManager.getInstance(this)

    // Bind our "serverReady" listener BEFORE we start the Service,
    // in case it happens to initialize quickly
    localBroadcastManager.registerReceiver(object : BroadcastReceiver() {
      override fun onReceive(context: Context, intent: Intent) {
        // We can stop listening immediately
        localBroadcastManager.unregisterReceiver(this)

        // Now that the Service is ready, we can start sending it messages.
        // You can do this from any activity
        localBroadcastManager
          .sendBroadcast(Intent(ServerService.ACTION_SERVER_MESSAGE)
          .putExtra("operation", "doSomething"))
      }
       // defined in ServerService below
    }, IntentFilter(ServerService.ACTION_SERVER_READY))

    // If we wanted to listen for other events emitted by the service
    // we'd bind to them here in the same way as we registered for
    // serverReady events above.
    // localBroadcastManager.registerReceiver(..., IntentFilter(...))

    // Finally we start the Service
    startService(Intent(this, ServerService::class.java))
  }
}

So with that, we’ve started the Service. Let’s look at the Service implementation now.

Service

class ServerService : Service() {
  override fun onCreate() {
    // Service needs the LocalBroadcastManager, too
    val localBroadcastManager = LocalBroadcastManager.getInstance(this)

    // Start listening for relevant events BEFORE announcing readiness...
    // that's basically the whole point of announcing ready.
    localBroadcastManager.registerReceiver(object : BroadcastReceiver() {
      override fun onReceive(context: Context, intent: Intent) {
        // Service can take appropriate action now
        Log.i("APP", "Service got intent with action: ${intent.action} and operation ${intent.getStringExtra("operation")}")
      }
    }, IntentFilter(ACTION_SERVER_MESSAGE))

    // Everything is squared away, let's signal we can start handling messages.
    localBroadcastManager.sendBroadcast(Intent(ACTION_SERVER_READY))
  }

  override fun onBind(intent: Intent): IBinder = null

  // Keep our actions in constants
  companion object {
    const val ACTION_SERVER_READY = "serverReady"
    const val ACTION_SERVER_MESSAGE = "serverMessage"
  }
}

So there you have it – a simple Started service and bidirectional communication with an Activity.