Android Oreo Background Execution Limits

Roy Ng @ Redso
AndroidPub
Published in
6 min readAug 20, 2018

Android 8 (API level 26) imposes limitations on what apps can do while running in the background. This introduces big impacts to those applications that rely heavily on background services. More and more devices on the market start to upgrading to Android 8, as a developer, you may start to receive the crashes of followings:

java.lang.IllegalStateException: Not allowed to start service Intent

In this story, the main goal is to make the background services work on Android 8. Secondary, there are some weird cases testing. You may just skip to the summary at the bottom if you want to have a quick read.

This sharing based on a device of Asus (Z012DA) 8.0.0 version. The behaviours may different for devices (even for the same device).

Source Code

Official Documentation:
https://developer.android.com/about/versions/oreo/background

Test 1: Do your device allow background tasks?

The answer should be easy to find but it’s not. I wish to find this answer by using IntentService. From the official documentation:

IntentService is subject to these restrictionsIntentService is a service, and is therefore subject to the new restrictions on background services. As a result, many apps that rely on IntentService do not work properly when targeting Android 8.0 or higher. For this reason, Android Support Library 26.0.0 introduces a new JobIntentService class, which provides the same functionality as IntentService but uses jobs instead of services when running on Android 8.0 or higher.

Therefore, It is expected that IntentService not allowed to run when targeting Android 8.0.
In the demo app, the first item in the list is to schedule an action using AlarmManager and BroadcastReceiver and then start a IntentService in background.

The demo app

This is the code:

if (action == "testBackgroundServiceLimitation") {
try {
context?.startService(Intent(context, Wait10IntentService::class.java))
} catch (e: Exception) {
context?.let { Common.showNotification(it, "Wait10Receiver failed") }
}
}

As Wait10IntentService is just a normal IntentService, the service should not allowed to start when the app in background. But the behaviour for my device is that:

1) The testing IntentService succeed to run

or

2) The testing IntentService failed to run and cause an exception

Yes. The two opposite behaviors exist on the same device even I turned on the setting of Background process limit (No background processes) in Developer options.

This log shows that the IntentService succeed to run even the app terminated (App onCreate is called again when the BroadcastReceiver run).

2018-08-20 14:36:55.636 32055-32055 I: App onCreate
2018-08-20 14:36:57.520 32055-32055 I: action testBackgroundServiceLimitation
2018-08-20 14:39:57.681 1740-1740 I: App onCreate
2018-08-20 14:39:57.683 1740-1740 I: ActionReceiver onReceive testBackgroundServiceLimitation
2018-08-20 14:39:57.693 1740-1783 I: Wait10IntentService onHandleIntent
2018-08-20 14:40:07.693 1740-1783 I: Wait10IntentService finish
2018-08-20 14:40:07.693 1740-1783 I: showNotification Wait10IntentService finish

When the app is allowed to run old background jobs (e.g. IntentService) under some uncertain conditions, I got no way to prevent the app to run jobs in backgrounds (e.g. turn on power saving mode).

In some case, the ActionReceiver can’t receive broadcast from system even it’s explicit. But the documentation say this:

Apps can continue to register for explicit broadcasts in their manifests.

But I got this (as same as when Auto-Start Manager blocking my app) :

2018-08-20 15:26:28.086 2490-2522 W: Reject to launch app com.redso.backgroundjobdemo/10281 for broadcast: App Op 73

But no matter how, you should obey the new rules for background jobs.

Test 2: Simple Job Intent Service

This is a very simple JobIntentService.
1) Add a class name to testData array when enqueueWork
2) Wait 5 seconds and show a local notification when handle the work.

class Wait5JobIntentService : BaseJobIntentService() {

companion object {
fun enqueueWork(context: Context, intent: Intent) {
enqueueWork(context, Wait5JobIntentService::class.java, 4000, intent)
App.shared.testData.add("$this")
}
}

override fun onHandleWork(intent: Intent) {
super.onHandleWork(intent)
val time = Date().time
while (Date().time - time < 1000 * 5) {
}
val testData = "${App.shared.testData.size}"
Common.showNotification(baseContext, "$className finish [$testData]")
}
}

From the log of 2 trials:

2018-08-20 15:51:07.211 12418-12418 I: ActionReceiver onReceive testJobIntentService
2018-08-20 15:51:07.221 12418-12418 I: Wait5JobIntentService onCreate
2018-08-20 15:51:07.230 12418-14709 I: Wait5JobIntentService onHandleWork
2018-08-20 15:51:12.230 12418-14709 I: showNotification Wait5JobIntentService finish [3]
2018-08-20 15:51:12.238 12418-12418 I: Wait5JobIntentService onDestroy

The app is killed by me manually just after scheduled the job:

2018-08-20 15:52:23.298 2518-3190 I: Killing 12418:com.redso.backgroundjobdemo/u0a282 (adj 100): remove task
2018-08-20 15:52:27.396 15445-15445 I: App onCreate
2018-08-20 15:52:27.399 15445-15445 I: ActionReceiver onReceive testJobIntentService
2018-08-20 15:52:27.410 15445-15445 I: Wait5JobIntentService onCreate
2018-08-20 15:52:27.416 15445-15467 I: Wait5JobIntentService onHandleWork
2018-08-20 15:52:32.416 15445-15467 I: showNotification Wait5JobIntentService finish [2]
2018-08-20 15:52:32.428 15445-15445 I: Wait5JobIntentService onDestroy

You can see that the job is succeeded to run even the app is killed manually. But the objects stored in memory will be cleared when the app is killed. Therefore, you should not rely on in-memory to store your objects.

Test 3: Async code in JobIntentService

class NetworkCallJobIntentService : BaseJobIntentService() {
companion object {
fun enqueueWork(context: Context, intent: Intent) {
enqueueWork(context, NetworkCallJobIntentService::class.java, 4001, intent)
}
}

override fun onHandleWork(intent: Intent) {
super.onHandleWork(intent)
mockNetworkCall(3) {
Common
.showNotification(baseContext, "NetworkCallJobIntentService finish 3")
}
mockNetworkCall(5) {
Common
.showNotification(baseContext, "NetworkCallJobIntentService finish 5")
}

}

fun mockNetworkCall(sec: Int, completion: () -> Unit) {
Runnable {
val time = Date().time
while (Date().time - time < 1000 * sec) {
}
completion()
}.run()
}
}

It’s just ok to do some async tasks within the JobIntentService.

Test 4: Step Counter using repeating alarm

This is a bit off-topic but it’s a good use case of JobIntentService. And I test it for long time and it’s just work. You can check the sources:
StepCounterJobIntentService.kt

The service is scheduled to run on every 5 minutes.

testRepeatAction("testStepCounterJobIntentService", 3, 60 * 5)
...
alarmMgr.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + 1000 * sec, interval * 1000, alarmIntent)

To ensure to service is running even on reboot:

<receiver android:name=".service.BootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
...
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
StepCounterJobIntentService.resetData()
App.shared.testRepeatAction("testStepCounterJobIntentService", 5, 60 * 5)
}

}

From the doc of Step Counter Sensor, you can continuously track the number of steps over a long period of time by NOT unregister the sensor. So you can see that I register for Sensor.TYPE_STEP_COUNTER every time the service run. But the value returned from the sensor is the total sum of step counts since the system reboot, it’s meaningless for the value when you read it for the first time.

In the first run of the service, I save the current time and the counter value at that moment. Then after a period of time (e.g. 30 minutes in the example), we read the count again and we know the steps the user moved during this 30 minutes. During this 30 minutes, the service is up and down for a short time by 6 times (5 minutes repeating). The benefit of this usage:

  1. You don’t need to use foreground service to run a long alive job.
  2. Low power consuming as both sensor is implemented in hardware and only 6 run of very short alive intent service.

Test 5: Location tracking using JobIntentService

JobIntentService seems so good to run code in background without user’s notice (comparing to foreground service). Can we use it to listen the location updates from LocationManager?

The answer is NO. You may just only get 1 updates.

Test 6: Location tracking using foreground service

This is just the proper way to use location tracking after Android 8.0. It should be simple to change your code. Just need to claim that your service is foreground.

startForeground(500, builder.build())

Of cause, the down side (to developers) is that you should tell your users properly why your app is keep running code in background. The user should have right to not using it. This is also the main reason why Goggle introduce all these limitations to Android.

Summary:

  • Under some unknown situation, the app is still able to run *IntentService*
  • Under some unknown situation, the app can’t receive broadcast even it’s explicit broadcasts
  • Use JobIntentService instead of IntentService for the tasks that only need short time to run (e.g. Step Counter Sensor, Network calls)
  • Use Foreground Service for location tracking and other tasks that need real time executions (e.g. Getting updates from Accelerometer for every seconds)

--

--

Roy Ng @ Redso
AndroidPub

Redso: Expert in building iPhone apps, Android apps, Mobile Web, scalable backend on Cloud Platforms, and integrating them all together.