How to Service on Android — part 3

How, when and when not to use the Android Service component in modern Android applications.

How to Service on Android — part 3

In my last two posts we looked at how a Service works and behaves depending on how you use it. You can either bind or start it, and the actual implementation in the Service also affects its lifecycle and wether it will be restarted by the system after being killed.

In this final post we will look at best practices for the Service component. When should you use it, when you shouldn’t, and how you should use it in the relevant cases.


Keeping your app alive

For the vast majority of cases, a Service for modern Android applications is only needed if you want to keep your app alive when it is running in the background. I covered how to do this already in my previous post, but the gist of it is:

  1. Start your Service with Context.startService()
  2. Call Service.startForeground() as soon as possible in onStartCommand().
  3. Return START_STICKY from onStartCommand() to make sure you get restarted by the system in case your app still gets killed at a low-memory situation.

Many apps do exactly this, but there is one things that many developers miss when it comes to implementing a Service. A Service doesn’t have to perform any actual work at all. In fact, it will often be easier to simply use a Service as a flag to the Android OS and perform any background work in regular classes that doesn’t extend an Android component.

class MyKeepAliveService : Service() {
    override fun onBind(intent: Intent?): IBinder? = null

    override fun onStartCommand(intent: Intent?, flags: Int, 
                                startId: Int): Int {
        startForeground(NOTIF_ID, defaultNotification())
        return START_STICKY
    }

    companion object {
        const val NOTIF_ID = 101

        fun defaultNotification(context: Context): Notification {
            // Create the default notification for your app here... 
        }
    }
}

The code above shows you how a keep-alive Service would look like. It doesn’t need to do more than this, and it could save you a lot of mocking when you write tests for your app, especially when the background work doesn’t actually touch much of the Android framework.

When your app wants to update the notification, it can do so using the same notification ID as the Service used.

By looking at the Service component as less than something that performs an actual service and more as a “flag” for the Android OS, you can reduce the complexity of your app and save yourself lots of boilerplate code needed for testing.

When your app no longer needs to run in the background, simply call Context.stopService() and you’re done.

Auto-starting and restarting

Two cases where we might want to add additional code to our keep-alive service is when we want to be started automatically as the device boots up or when the app gets restarted after a low-memory kill.

For auto-starting, you need to register a BroadcastReceiver in your application manifest that is registered for ACTION_BOOT_COMPLETED. This also requires the RECEIVE_BOOT_COMPLETED permission, but apart from that you got everything you need.

class AutoStartReceiver : BroadcastReceiver() {
  override fun onReceive(context: Context?, intent: Intent?) {
    if (intent?.action == Intent.ACTION_BOOT_COMPLETED) {
      val svcIntent = Intent(context, 
                             MyKeepAliveService::class.java)
      ContextCompat.startForegroundService(context!!, svcIntent)
    }
  }
}

The code above shows how to auto-start your Service when the device boots up. Note the use of ContextCompat.startForegroundService(). This is needed from Android O and later if you want to start a Service from the background, like from a BroadcastReceiver that can trigger when your app isn’t running in the foreground. You still need to kick off any actual background work as well, but I’ll leave that as an exercise to the reader.

When it comes to restarting your application you don’t have to do anything extra as long as you returned START_STICKY in onStartCommand(). You can then detect if you were restarted by checking if the Intent parameter was null or not. Again, I’ll leave it to the reader to decide where to implement the actual background work.

Service alternatives

The Service component used to be more relevant in Android before we got all the new AndroidX libraries and new framework APIs. For instance, the Work Manager effectively replaces the need a Service for a lot of background work. If the background work is a recurring synchronisation operation or similar, the Work Manager is what you should use instead of your own Service.

For more persistent use-cases a Service might be needed, but today there are plenty of specialised APIs that covers most of these. One of the most common cases is playing audio in the background (e.g., music players, podcasts etc.). We still need a Service for keeping our app alive in the background for this, but today you should use MediaBrowserServiceCompat, which extends Service and provides a much better API for these cases. Since this one is in the AndroidX APIs, it works on all relevant API levels today.

In fact, if you check the documentation for the Service class, you’ll see there are plenty of APIs that extend it and provide a specialised API for many different use-cases.

The Service class is extended by several specialised APIs.

For instance, the ConnectionService class is an abstract class that extends Service and which should be used when you want to build your own VoIP app (or a standalone calling app).

Documentation for the ConnectionService class.

These sub-classes cover a wide variety of use-cases and is probably what you want to use instead of a basic Service. The only problem is that some of them are introduced in an API level that is higher than what you can put as minSdkVersion. The ConnectionService was introduced in API level 23, so if you need to support earlier versions, you still need to use regular Service.

However, just because you need to support an API level that is lower than 23, it doesn’t mean you cannot use the ConnectionService. You can have one LegacyVoipService that keeps your app running on lower API levels where the ConnectionService isn’t available, and then use a class that extends ConnectionService on API version 23 and later.

This can easily be achieved by using the following definitions in your AndroidManifest.xml:

<service android:name=".MyVoipService"
         android:label="@string/connection_service_label"
         android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
         android:enabled="@bool/enable_voip_service">
    <intent-filter>
        <action android:name="android.telecom.ConnectionService"/>
    </intent-filter>
</service>
<service android:name=".MyLegacyVoipService"
         android:enabled="@bool/enable_legacy_voip_service"/>

The way this works is by having a two booleans in your resources (see the code marked in bold above). One in the regular values folder and another in values-23. The content of the default (pre API level 23) looks as follows.

<resources>
    <bool name="enable_voip_service">false</bool>
    <bool name="enable_legacy_voip_service">true</bool>
</resources>

The second file in values-23 is the inverse of this.

<resources>
    <bool name="enable_voip_service">true</bool>
    <bool name="enable_legacy_voip_service">false</bool>
</resources>

By using these two bools for enabling or disabling the two services, you can have the new one for API level 23 and later and still provide the legacy version for earlier Android versions. The rest is just the regular API level checks in code that we’re all used to do already.

This approach provides a better integration with the Android OS on relevant versions, while at the same time lets you do a graceful degradation by using a legacy implementation.

Official documentation

Although the Service component is such a central part of Android, the documentation on developer.android.com is often outdated and in some cases wrong (tip; check when mBoundService is cleared). This is probably one of the main reasons why the Service class is so misunderstood and misused. I have submitted an issue on the official bug tracker for the Android documentation where I suggest that it gets updated to reflect how and when a Service should be used today (please star it if you agree!).

The Service class is not the only API in Android that suffers from outdated and sometimes erroneous documentation, but since this component is often so important to get right it should be a priority to fix before other APIs.

My opinion is that as the AndroidX libraries becomes more opinionated on who we should build Android application, the old documentation for things like Service should be deprecated and updated ASAP. The current guide for this topic is very outdated and not something I would recommend to anyone beginning with Android development today.

Summary

So let’s summarise how and when you should use a Service today.

  • You only need a Service if your app must be kept alive indefinitely.
  • Your Service should only be started and never bound, and you should always call startForeground() as soon as the you get a call to onStartCommand().
  • Investigate if there is a specialised API, like the MediaBrowserServiceCompat, that you can use instead of a basic Service.
  • Even if a better alternative is only available on later API levels, you can still use it by providing a legacy Service for older API levels and provide two booleans in your resources that enables only one of them.
  • Read the documentation carefully, but also check the bug tracker for developer.android.com for any error that might exist there.

As usual, there are always exceptions to these guidelines. Your app might be special enough to warrant a multi-process design where you communicate using a bound Service. However, these are very rare cases and probably not relevant to the very vast majority of apps published on Google Play Store.

In order to gain a better understanding of how the Service component works I’ve created a simple app for testing it. Simply clone this project, launch the app and check the logcat output to discover how a Service behaves depending on if you bind or unbind, start or stop, or if it is local or remote.

ErikHellman/AndroidServiceTester
A simple Android app for testing the behaviour of the Android Service component - ErikHellman/AndroidServiceTestergithub.com

Hope this post and the two previous will help you avoid the many pitfalls with the Service component and create better Android applications.