TL;DR: This is how your onStartCommand() probably should look like

In my previous post I covered how binding to a Service worked. In this post we will look at the other way of interacting with a Service, namely starting and stopping.


Context.startService()

The most common way we interact with a Service today is usually by calling startService() and passing it an Intent. While binding involves a more complex chain of asynchronously receiving a IBinder object and invoking methods on it in an RPC style, starting is more of a fire-and-forget approach. We create an Intent targeting the Service, and we can pass additional parameters as extras.

val startServiceIntent = Intent(context, MyService::class.java)
startServiceIntent.putExtra(EXTRA_DEVICE_UUID, deviceUuid)
context.startService(startServiceIntent)

The code above is probably something you have written before a few times. From the client side, interacting with a Service by starting it is really not more complicated than this.

From API level 26, we also got the method Context.startForegroundService(). The difference between this and the old one is due to a change in when an app can start a Service that was introduced in API level 26. From that version you can no longer call Context.startService() unless your application is in the foreground. This is usually the case when you want to start a Service from a BroadcastReceiver or some other event that was triggered when your app isn’t running in the foreground.

When you use this new method, you must also call Service.startForeground() within the same amount of time as you would get an ANR if you would block the main thread. This is to prevent apps from running in the background without notifying the user, since Service.startForeground() requires a Notification that will be displayed (more on this later in this post).

onStartCommand()

When a Service is started, it triggers a call to onStartCommand() with three parameters; an Intent (which can be null, more on this in a moment), an int containing the flags for this start request, and another int defining the ID for this start request.

The method also returns an int that can be one of the following three (I’m not including the compatibility constants): START_NOT_STICKY, START_STICKY and START_REDELIVER_INTENT. Which of these you return determines if and how your Service will be restarted if you app gets killed by the system.

START_NOT_STICKY basically means you don’t want your Service restarted by the system. This is a good option if you want full control of when your Service is started. However, since you won’t be restarted by the system automatically, it is usually not a useful method for indefinite or very long-running tasks.

START_STICKY means the system will eventually restart your Service after it has been killed by the system. When it gets restarted, the Intent parameter to onStartCommand() will be null. This lets you detect when you get restarted as the Intent will otherwise be the one you passed to startService(). Most likely, this is the one you want to use today.

START_REDELIVER_INTENT is like START_STICKY, but the system will retain the Intent used for starting the Service and redeliver it when restarting. It might be tempting to use this to ensure you get the same info to your Service once it is restarted, but I recommend to use START_STICKY instead and keep track of the progress inside your Service instead.

Which one of these that you choose depends on your use case, but I usually recommend to use START_STICKY for a started Service that needs to keep running in the background indefinitely. My recommendation is to persist any information that needs to survive a low-memory kill, either in a SharedPreference or a Room database. This also lets you resume work that wasn’t completed, like processing of data that doesn’t have to start from scratch.

START_NOT_STICKY is mostly only useful when yo don’t care if the work your Service will do is going to complete successfully or not. I don’t recommend using this today as the Work Manager can replace most of these cases, and it is a much better API for those use cases.

The flags parameter in onStartCommand() can have three different values, 0, START_FLAG_REDELIVERY, and START_FLAG_RETRY. START_FLAG_REDELIVER is set if the Service was restarted before you had explicitly stopped it (see below) and your onStartCommand() returned START_REDELIVER_INTENT.

START_FLAG_RETRY is useful to keep track on, because it means you got restarted but the last call to onStartCommand() never returned before the process was killed. This can happen for two reasons, your apps process got killed by the system before it managed to complete the execution of that method, or it crashed due to a bug in your code.

The startId flag is useful when stopping the Service, as I will explain more about next.

Stopping a Service

There are four ways to stop a Service, externally using Context.stopService() which takes the same Intent you used for starting it, or by calling Service.stopSelf() (which comes in two variants) or Service.stopSelfResult().

The first method will simply stop the Service and destroy it as long as it doesn’t have any bindings.

The second method, Service.stopSelf() without parameters, is the same as the first but doesn’t require an Intent as you call it directly on the Service you want to stop.

The third method, Service.stopSelf(int startId), will stop a specific start request based on the startId parameter you received in onStartCommand(). This is where it gets interesting. You can start a Service several times. Each time onStartCommand() is called, you get a new startId.

This can be useful if your Service is responsible for performing multiple different tasks. Once each task is done, you call stopSelf() with that particular startId. Only once all start requests have been stopped will the Service be destroyed.

Again, most of the use cases for this has been replaced by the Work Manager today, but this is still a valid method for running multiple independent background jobs from a single Service. However, note that you will still only have one instance of your Service class when started multiple times.

The fourth option is Service.stopSelfResult(). This one can be a bit tricky and I don’t recommend anyone to use it. It takes startId as a parameter, but it will only stop the Service if that startId was the latest one received in onStartCommand(). I have personally never found a good use for this, but it exists in the API so I assume someone needed it at some time. Since it depends on the order of how onStartCommand() is called for different start requests, it can easily get really hard to verify this code.

Foreground services

In order for a started Service to continue running once your application goes into the background, you need to call startForeground(). This methods takes a ID and a Notification as parameters. The notification will be displayed as usual and the system will now prioritise your app as if it was running in the foreground with a resumed Activity.

If your app needs a Service that runs indefinitely, this is the method you must use. Too often have I seen weird hacks to bypass any background restrictions that the Android OS does on apps. Don’t do that, just use Service.startForeground() instead. The best time to call it is immediately when onStartCommand() gets called. If the Notification needs to be changed later, you can do so later using the normal way for modifying them with the NotificationManager.

override fun onStartCommand(intent: Intent?, 
                            flags: Int, 
                            startId: Int): Int {
    val notification = initialNotificiaton()
    startForeground(ID, notification)
    initiateBackgroundWork(intent, flags)
    return START_STICKY
}

The code shows how your onStartCommand() method should look like. Basically, create an initial notification, call startForeground(), start any background work, and finally return START_STICKY.

Another common error I’ve seen is placing startForeground() in a conditional block which sometimes doesn’t get executed and your Service will then be destroyed soon after your app goes into background.

Note that I pass the Intent and flags parameters to the initiateBackgroundWork() method. This lets me detect if the Service was restarted by the system or not, and if onStartCommand() successfully returned the last time. I highly recommend dealing with both of these cases.

There is also the possibility to stop a Service from running in the foreground, but still have it started. This is done using stopForeground(). There are two version, with the second one introduced in API level 24. For that reason, I’m only going to go into the first one since it usually does the job good enough.

You want to stop a Service from running in the foreground when it is done with its job, but you don’t want to stop the Service itself since it might be doing more work soon. This will result in the Service being destroyed by the system eventually though.

stopForeground() takes a boolean as its single parameter. This will decide if the Notification that you created for your call to startForeground() will be removed or not.

The typical example for this would be a music player (or perhaps a podcast app?) where the music has stopped (you reached the end of the playlist). You would then call startForeground(false) since you want the Notification to remain but not keep the Service in the foreground. However, since we today have nice class named MediaBrowserServiceCompat for implementing these kinds of apps, you usually don’t need to implement your own Service for this anymore (more about this in the next post).

If you no longer need your Service to run in the background and there is no need for the Notification to be shown, you can use stopForeground(true). This will remove the Notification as well as take the Service out of foreground mode. However, in these cases you should probably also call stopSelf() or your Service will be automatically restarted by the system after a while. This is another cause of bugs that I’ve seen, where the Service is unexpectedly starting again.

Another cause for bugs that I’ve encountered are apps that toggle the foreground state of their Service when the app is resumed (that is, an Activity is resumed). This is done to remove the notification when the app is in the foreground. While there might be some value on the UX side for this, I strongly discourage you from using this pattern.

Keep it simple and keep your foreground Notification visible even when the UI of the app is in the foreground. This reduces the complexity of your code and you’ll avoid a common cause for bugs when a necessary startForeground() is missed. Basically, this is the same as having a startForeground() call in a conditional statement in onStartCommand().

Summary

Let’s try to summarise how a started Service works and how to use it.

  • Today, a started Service is probably the only relevant use case for most apps. Exceptions are rare, or just a sign of legacy code.
  • Keep your onStartCommand() simple. Avoid calls to Service methods in conditional statements here, since that is an easy way to introduce bugs and this class is hard to unit test (requires lots of mocking).
  • Most likely, you want to return START_STICKY from your onStartCommand(). Most of the other times, you can replace your Service with the Work Manager.
  • Check the Intent and flags parameters in the onStartCommand() method to determine how you got started and if onStartCommand() successfully returned the last time or not.
  • Also most likely, you want your Service to run in the foreground so it doesn’t gets destroyed by the system once your app goes into the background. Call startForeground() as soon as possible in onStartCommand().
  • Avoid toggling the foreground notification on and off when the UI of your app is in the foreground or not. It is really hard to test this and you risk missing a necessary startForeground() call.
  • When your Service is no longer needed, call both stopForeground() and stopSelf() to remove the Notification and avoid getting restarted by the system at a later point.

In the next post I will explore where the Service component serves a purpose today and when you shouldn’t use it. We’ve already glimpsed at one case where the AndroidX libraries provide a ready-made alternative. I will show how you should implement your Service to keep it as simple as possible and avoid having to resort to some complex Android acrobatics.

Hopefully this post cleared up som important points about the Service and how to use it. Good luck with your Service code!