JobIntentService for background processing on Android O

One of Android’s greatest strengths is its ability to use system resources in the background regardless app execution. However, sometimes it became the behavior to use system resources excessively.

Past few years, Google has been working on a number of tasks to reduce system resource consumption on Android. Main changes such as Doze and App Standby are to limit resource consumptions in the background which are not recognized by the user. In addition to these changes, lots of indicators in setting is giving users usage of system resource in detail.

Two levels of Doze (Source: Android Developer Site ) included in Android Nougat

There are a number of changes that are made to Android O, but the biggest part of it is the execution limits and location restrictions that have already been announced.

Let's take a look at background execution limits.

In this article, we will look into limitations of the background service and limitations of implicit broadcast intents .

Background service limitations

The background services can run at any time, but there is a problem that users find it too hard to recognize it. From a user’s point of view, this can be an issue because apps are able to be constantly using resources even when users are not using the app.

Note: Games are a typical genre that consumes a great deal of battery, network, and memory, but users don’t worry about this because they understand the reason for it i.e playing a game. But what if the game is not played? This is a reason background-behavior is a kind of UX scenario for system resources in the background.
When you do googling, you can see that a lot of people are really interested in battery consumption.

On Android O, a background service that will run a few minutes after the app enters the background, and will automatically stop and onDestroy() will be called. In addition, a call to startService() to run the service in the background state is not allowed because an IllegalStateException is thrown.

Implicit broadcast intent limitations

Implicit Broadcast Intent is a broadcast intent that does not target specific apps. This is problematic because it doesn’t have the specific target for delivery. It’s like calling everyone in your neighborhood at midnight to find your friends.

"Excuse me. Will you move the car, please? if it’s yours."- Implicit Broadcast is never such a gentle situation.

These implicit broadcast intents can wake up any Receiver or Service that describes it in AndroidManifest.xml. An inactive application component must be loaded and executed for processing, which can be problematic because it is a resource-intensive operation.

In Android N, Google introduced the ability to limit the behavior of certain intents to reduce this. This year's Android O limits the use of resources for more aggressive restrictions that prevent calling the implicit broadcast recipients which registered in the app manifest.

TargetSDK ≥ 26

These background execution limits apply when the target SDK is Android O (API Level 26) or later. If you target Android N or pre-N, you can run the service without limitations. And sometimes it can be relied on app setting, even though your application has been built by under the pre-O SDK.

Note: In other words, notice that the startService may raise an IllegalStateException exception, depending on the user settings, even though the target SDK is set to Android N or lower, and the broadcast intent may not be delivered. See here for more information on this .

Nonetheless available features

I mentioned that the background service throws an exception when the application is in the background, and some implicit broadcast intents are not received in the manner that describes intent filters in AndroidManifest.xml. If so, there is a question to be asked.

"Then what can I use on Android O?"

There are still many things available.

Running the foreground service

Foreground services are available without restrictions. However, it is not possible to set it up as a foreground service because the service can not be started in the background state in the first place.

It can be solved using Context.startForegroundService()API added to 26. This API allows services to start unrestricted, but it also stops the service immediately if you don’t bind to the Notification with Service.startForeground() within 5 seconds.

Media play, download and upload are examples of typical foreground services.
Note: In the guide says that you can start the foreground service using the NotificationManager.startServiceInForeground()API, but that API has been removed from Developer Preview 2 .

If users are already familiar with existing foreground service behavior, or if it is reasonable to pass it through notification as appropriate, foreground service on Android O can be a good choice.

Implicit Broadcast Exceptions

Fortunately, not all implicit broadcast intents are restricted, and many essential intents can be used in the same way as before. Obviously, you can register an explicit broadcast in AndroidManifest.xml.

Implicit broadcasts through dynamically registered receivers

You can still use Context.registerReceiver()to to register the receiver dynamically, and the registered receivers can receive implicit broadcast intent without any restriction.

Background behavior through JobScheduler

There is still a shortage of background behavior between the limitations and allowable features described above. This is because we can not completely rule out scenarios like when you need to sync contacts or photo data in the background. In such cases, JobScheduler can be good alternative for the scenarios.

What is JobScheduler?

Android Lollipop introduced JobScheduler as a part of optimizing background behavior. This task required conditions and factors ( JobInfo) and the operation of the condition ( JobService) running in the background. It will be executed at the appropriate time by the Android framework.

Using the Android Job Scheduler — Google Developers

For example, to sync large data, you should receive the ACTION_POWER_CONNECTED broadcast intent and continuously check the network status or storage. If you use JobScheduler, these conditions will be described on a JobInfo object.

The job implementation consists of the following three steps.

  1. JobInfo: Sets the conditions under which the job will run, JobSchedulerand registers it in the system via.
  2. JobService: implements the necessary action when the job is executed.
  3. android.permission.BIND_JOB_SERVICE: Grants permission of your sub-class of JobService for job execution .

Setting and registration of execution condition

JobInfo manages the conditions such as the network connection types, charge status, device idle time, etc. for deciding a good time to execute JobService. From the article “Scheduling jobs like a pro with JobScheduler”, you can set these conditions as follows:

  • Connected network type
  • Charging status
  • Device idle state
  • Updating the content provider
  • Clip data
  • Execution cycle
  • Minimum latency
  • Deadline setting
  • Retry Policy
  • Whether the current condition is maintained when rebooting

You can describe your JobInfo with a single condition, or define more complex job with several conditions.

Processing jobs

Real works for Job will be written as a sub-class of JobService. JobService has an easy structure that can handle the operation start, finish and notifying the end of the job by calling at any time if necessary. It provides two callbacks and one method:

onStartJob()

This callback is called by the system at the time the job starts. However, since JobService runs on the main thread, we will consider Thread or AsyncTask as needed.

  • onStartJob()Return true if it need more time to finish your work such as AsyncTask or Thread thing, and false if the action is finished at this method.
  • If you return true, you should explicitly notify the end of the job by invoking finishJob(). In this case, the system can call onStopJob() as needed to interrupt the work before the finish of the job.

onStopJob()

Callback invoked by the system if it needs to be canceled before the job is finished. Call to this method doesn’t mean your work is failed, so you have to check progress of works to resume and complete it quickly at next chance.

  • By returning true, you can re-schedule the job that are currently running. it will be run on the next chance with little more latency.
  • Alternatively, return false to prevent this job from being scheduled again.

jobFinished()

Called when all the work on the job is completed and inform the JobManager you’ve finished executing.

Granting a permission to run JobService

Grants android.permission.BIND_JOB_SERVICEpermission to execute the service in AndroidManifest.xml.

<service
android:name=".SyncJobService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true"/>
For questions about JobScheduler, see Joanna Smith ‘s “ Scheduling jobs like a pro with JobScheduler “.

General Considerations

JobScheduler allows you to use background executions with less impact on system resources such as battery or memory, but this is not all. Before you use it, keep the followings in mind.

  • By default, JobService runs on the main thread.
Therefore, if you need a relatively long runtime or complex operation, you must create a new thread or some other way to implement asynchronous behavior.
  • id is a important key for identifying your job.
Whether you use a static id or a dynamic id, it depends on the scenario, but in most cases a static id is enough.
  • Changing any of the execution conditions would stop current Job.
More complex conditions of a job are make job harder to be executed and maintained . It is important to determine the appropriate scenario between the consumption of the battery and the frequency of its execution.
In addition, you should have a clear pause / resume scenarios to implement functionality like a transaction, or to be able to resume at the last point in progress, in case the job is canceled suddenly.
  • Job execution shares their life with Wakelock.
The number of WakeLock and the time are critical value to calculate the battery drain for that app. This can result in a negative experience in the battery consumption that is monitored by the user if the job is eventually triggered too often. It’s same on requests updates of network and location. (Of course there are limitations on location updates …)

The most critical issue is backwards compatibility

Of course, implementing a background service using JobService is a very nice approach. However, JobService is a feature supported by API level 21, so there is a backward compatibility issue.

In particular, if target SDK of app addresses O(26) on Android O device, executing service in background wouldn’t be allowed. To clear this limitation, we should apply Job things. However, JobScheduler was introduced at API 21. So, we have to write a backward compatible code for devices not having JobScheduler.

Now, you can understand that we need an alternative way to support Android O and pre-O versions with same code which supports normal background service on pre-O and also supports JobScheduler on O and later.

Let’s take a look JobIntentService.

SupportLib: JobIntentService

Thankfully, Support Library addedJobIntentService for more convenient approach to this. It uses the JobScheduler function on Android O, but runs as a background service to emulate JobScheduler on pre-O. The JobIntentService has the following characteristics:

  • On Android N and below devices, the background service starts immediately and the loaded jobs are executed sequentially. This ensures backwards compatibility with the same code.
  • In Android O and later, jobs loaded through JobScheduler are executed sequentially. Internally, it is scheduled byjobInfo.setOverrideDeadline(0).build(). If you want other conditions, JobIntentService doesn’t fit it.
  • You don’t need WakefulBroadcastReceiver to use Wakelock when running on pre-O, because JobIntentService would manage Wakelock for you. This prevent writing wrong code such as battery drain due to not releasing Wakelocks.
Background Check and Other Insights into the Android Operating System Framework — Google I / O ‘17
Note: You should use JobScheduler or alternative approaches instead of JobIntentService if you want to use JobScheduling things on devices before Android O or if you need more detailed execution condition. (The complexity of final code is a gift …)

Applying JobIntentService to your project

Gradle

To use JobIntentService, you must include the following maven repository URL in build.gradle:

allprojects { 
repositories {
jcenter ()
maven {
url "https://maven.google.com"
}
}
}

In addition, add the support-compat in dependencies. (The latest version is 26.0.0-beta2 as of June 25, 2017. Please see here for the latest version .)

dependencies { 
...
compile 'com.android.support:support-compat:26.0.0-beta2'
}

JobIntentService is similar to IntentService in the part where reserved works by enqueueWork() are sequentially processed at onHandleWork(). However, it should be noted that onStopCurrentWork() is called at any time when the system need to stop the job.

Structure of JobIntentService

JobIntentService also takes a simple structure that provides two callbacks and one method:

EnqueueWork (Context context, Class cls, int jobId, Intent work)

Added the work to be executed into the queue.

  • On Android O and later, the deadline is set to 0, but it can be started at any time, due to issues such as doze state or running out of memory.
  • On pre-O, the background service will start by calling to startService(). Since it operates irrespective of the doze, the network timeout caused by Doze should be managed by yourself.
  • All jobIds targeted to the same class (cls) must all have the same value. If you want to assign a different jobId, you need to define it as a new class. If you do not follow this rule, you will not see any problem on pre-O, but Android O and later will throw IllegalArgumentException .
  • Of course, the work argument can not be null. If null, an IllegalArgumentException is thrown.
Again, since JobIntentService uses JobScheduler in Android O or later, the execution condition of Job setOverrideDeadline(0)is fixed as below. If you need changing execution condition, you should use JobScheduler. This rule can be seen that the decompiled code of JobIntentService included in support library 26.0.0-beta2 is implemented as below:
@RequiresApi (26) 
static final class JobWorkEnqueuer extends JobIntentService.WorkEnqueuer {
private final JobInfo mJobInfo;
private final JobScheduler mJobScheduler;
JobWorkEnqueuer (Context context, Class cls, int jobId) { 
...
JobInfo.Builder b =
new JobInfo.Builder(jobId, mComponentName);
        mJobInfo = b.setOverrideDeadline(0).build(); 
mJobScheduler =
(JobScheduler)context.getApplicationContext ()
.GetSystemService (Context.JOB_SCHEDULER_SERVICE);
}
...
}

OnHandleWork (Intent intent)

A method that receives dequeue work from the queue.

  • The intents loaded by enqueueWork() are passed to this method when the service is executed. Like IntentService, loaded intents are delivered sequentially.
  • Wakelock is automatically maintained from the first intent to the completion of the final intent, so you only have to manage the actual work.
  • When onHandleWork() is completed, the next pending work that is loaded from the work queue is dispatched.
  • This method is called in the background thread, so blocking operations would be great for this, but it can be stopped by JobScheduler on Android O and later.
The logic to dequeue works from the queue is implemented within the AsyncTask, and each of works pass to onHandleWork() in order. This code is also seen in the decompile code of JobIntentService included in the support library 26.0.0-beta2, as shown below.
final class CommandProcessor extends AsyncTask <Void, Void, Void> { 
@Override
protected Void doInBackground (Void ... params) {
GenericWorkItem work;
        if (DEBUG) Log.d(TAG, "Starting to dequeue work"); 
        While ((work = dequeueWork ())! = Null) {
if (DEBUG) Log.d(TAG, "Processing next work:" + work);
OnHandleWork (work.getIntent ());
if (DEBUG) Log.d(TAG, "Completing work:" + work);
Work.complete ();
}
        if (DEBUG) Log.d(TAG, "Done processing work!");
        return null; 
}
    ... 
}

OnStopCurrentWork ()

Called when the current job must be stopped.

  • As with JobService.onStopJob (), this is called on system shutdown of the job, and you must stop the work and store current states for resuming them at the next time.
  • Return true if you need to re-schedule works. If you do not need to schedule again, you can terminate the job by returning false.

Let’s create a simple example that download the album cover via JobIntentService.

Code for JobIntentService

Other options

In addition to Job, there are so many suitable options. See “ Background Check and Other Insights into the Android Operating System Framework “ at Google I/O 2017.

Background Check and Other Insights into the Android Operating System Framework — Flowchart

Defer your works as long as possible and do them at the last minute

In fact, whether you use JobScheduler or not, the important thing is that it is not a good user experience if user recognizes how much battery or memory your app consuming even though user doesn’t execute app. If possible, defer your background works and do them at the last and last minute. JobIntentService can be a good starting point for easy implementation.

This is a translation of my article. If you find wrong part, typo error or any strange part. Feel free to address them. I always welcome your comment or email! :)
Thanks to Hassan Abid for comments on earlier versions of this article.