Exploring Background Execution Limits on Android Oreo
Last week came the official announcement of the Android O release. This release of Android comes with some really cool new additions, and in this post I want to look specifically at the new restrictions that have been introduced for background services. If you’re using background services in your app then changes could affect your app, so let’s take a look at what these changes are and some alternative approaches we can take in our implementation.
There could be a bunch of reasons why our application is utilising background services — maybe it’s keeping our application data up-to-date with what’s on the server, maybe it’s shifting tasks to the background to unblock the UI or maybe we’re just carrying out some other long running task in the background. Whatever it is we’re doing, we’re going to be affected by these new changes in Android Oreo. But why has this change been introduced?
Well to begin with, Background Services are battery drainers — some applications can constantly be running services in the background (maybe to check your location for example), and this constant running of the service uses up a lot of battery on the device which isn’t really what you want for your users. But the main reason that this change was introduced was because background services can also adversely affect application performance — so if numerous applications are running processes in the background, then the application in the foreground may suffer in terms of performance due to the processes being carried out in the background.
I think these are two good enough reasons to make a change in this direction, ensuring that our users can gain a smoother experience in our applications whilst also ensuring their devices aren’t affecting at the same time.
So to put these changes into a nutshell, our applications no longer have the ability to freely run in the background — this means that if our application is either launched from an implicit broadcast, or if our application is launching services in the background, then these implementations will break with the introduction of Android Oreo.
Whilst this holds true for all applications that are targeting any version of Android— users on older versions of Android can still choose to opt-in to apply these restrictions within application settings, so even if we’re not targeting O yet then we still need to be proactive about these changes.
So the first component affected by these changes are Services. To begin with the most important thing to note is that if you attempt to call startService() when your application is not in the foreground then an IllegalStateException will be thrown. This means you can no longer use the startService() method when your app is not visible / being interacted with by the user. These change affects all services except those that:
- Have been started when the application is currently in the foreground
- Foreground Services
- Bound Services
If your application is still going to be calling startService() from the foreground, then when your application moves to the background there is a small amount of time that that your service is given before it is shutdown. Whilst this shows that this approach can still be used, it’s probably not something you should rely on — for example for services using the network, if the user is experiencing a slow connection and your task is taking longer to complete, if your app moves to the background then the service could be destroyed before your task has been completed (and the user possibly being unaware that the task didn’t complete). Because of this, be mindful of the tasks you are carrying out in these cases and don’t rely on the task being completed within this grace period.
When the service does reach the point where it is to be shut down by the system in this situation, then the service is stopped as if the stopSelf() method has been called.
To avoid running into any issues with these changes now in place, there are a number of different approaches that we can take. These include:
So to begin with, using a Job Scheduler allows us to move away from our background service implementation and let the system take care of the execution for us. The system will also be intelligent with the jobs that we schedule, so scheduling multiple jobs means that they can be grouped and executed at the same time. We can also schedule jobs to run based on certain criteria, such as network connectivity and device power status — so not only does adapting to these changes avoid issues with the way things are now, but it also allows our application tasks to be run more efficiently.
You can check out the documentation for the Android Job Scheduler here. There is also the Firebase Job Dispatcher which allows you to achieve similar results when targeting API versions lower than Android 5.0.
Firebase Cloud Messaging and Service Whitelist
When it comes to services running in the background, there is a whitelist which is used to allow applications to temporarily run in the background as if they were running in the foreground. Applications will be included on this whitelist in situations when they are:
- Being triggered from a high priority FCM/GCM notification
- Performing SMS/MMS delivery
- Acting from a Notification Action
So for example, in cases where we may need our application data to be refreshed based off of changes on the server, we can use FCM to send our application a notification that can trigger a service to fetch the latest set of data. For this, using High Priority messages allows our application to be added to a service whitelist which allows our services to run as if they were in the foreground. High Priority messages from FCM will be received immediately, even when the system is in the Doze state —and this is the point that the application will also be added to the temporary service whitelist. This means that we can start our service to update our application’s data as if our application was running in the foreground.
It’s important to note that this will only be the case for High Priority messages, other priority messages will be received when the device screen is turned back on or during the Doze maintenance window.
Starting a service in the foreground
In some cases, we may be running a service which is carrying out a task that the user may need to interact with or monitor the task that is being executed. Some examples of this could be when the user is downloading some new content in your app, using a timer to perform some time based operation, or maybe they’re receiving navigational directions from your application — these are all situations where the user needs to be aware of the task at hand.
For these kind of situations, we can run our service in the foreground — this is because when running, foreground services use a persistent notification to make the user aware that they are currently running.
Previously, we were able to start foreground services by simply calling startForeground() directly — using this method directly no longer works, so we need to make use of the new startForegroundService() method. The method itself is the same as calling startService(), but with the contract that startForeground() will be called. The difference with using this method as compared to startService() is that we can call it at anytime, even if our application is not currently in the foreground.
We can implement this by:
- First using the startForegroundService() method, passing an intent for our service task to be carried out. This method will create our background service that we must immediately promote to the foreground.
- Within this service we need to create a notification to be displayed for our foreground service. This must be of at least Low priority or higher so that is shown to the user on screen — if the priority is set to PRIORITY_MIN then the notification will not be displayed to the user.
- Next, we can call startForeground(id, notification) from within the service — this will promote our service to the foreground.
Defer task to the foreground
If none of the above are appropriate solutions for your application, then another approach that can be taken is to simply defer the task to the foreground of the application. Personally I’d advise exploring the above solutions first as they will help you to avoid blocking your user interface and create a smoother experience for your user.
The second set of changes that comes with Android Oreo are related to Broadcast Receivers. So essentially, any broadcast receivers that we have defined statically within our application manifest that are listening for implicit broadcasts will no longer receive those broadcasts.
The reason for this change is that implicit broadcasts would previously trigger any component that was listening for them within the manifest— this could have an adverse effect on application and device performance due to large numbers of applications registered to receive specific broadcasts all being triggered at the same time.
To avoid running into any issues with these changes now in place, there are a number of different approaches we can think about. These include:
To begin with, there is a ‘short’ list of exceptions when it comes to implicit broadcasts — this means that there are still some which you can register to receive broadcasts for. They are all listed below:
So if the broadcast that you have registered receivers for is on this list, then it will still function as it did previously (phew!). However, if the broadcast that you have registered to receive is not on this list then an alternative solution that we can take is to register a Job to be executed when certain broadcast events take place.
We touched on the JobScheduler slightly earlier, but with this API we have the ability to schedule jobs to be triggered intelligently when certain conditions are met, this could be in conditions such as:
- When the device connectivity status changes, such as becoming connected to a network or more specifically a WiFi network
- When the device has entered a power charging state
- When there has been content provider changes
And finally, if our requirements are not satisfied by either of these solutions then we still have the ability to register broadcast receivers programmatically within our classes.
In this situation, we can simply use the registerReceiver() method call to register a dynamic broadcast receiver within our activity — followed by unregisterReciever() to ensure we unregister subscriptions based on the lifecycle of our activity. The only difference here is that we’re registering these listeners programatically and clearing up their subscriptions when our activity no longer needs them, rather than them being defined in our manifest to receive events just as they please.
So as you can see from this, if your application is currently using any of the approaches above, then you will need to shift some implementations to ensure that things don’t break for your users.
Whilst all of these changes are related to background tasks, things your user might not directly be aware of, it’s important to remember that these will have a positive effect whilst using applications by providing a smoother experience from a more efficient process flow for background tasks.
Like always, do drop me a tweet or leave a comment below if you have any questions or suggestions for these changes to background tasks 🙂
The latest Tweets from Joe Birch (@hitherejoe). Android Engineer @Buffer. Instructor @caster_io & @Pluralsight. Public…twitter.com
You can check out more of my work at joebirch.co