Scheduling operations via JobScheduler in Android

Scheduling operations via JobScheduler in Android

In this note, we will discuss scheduling jobs which are not required to be completed immediately, but can be done later when more resources are available.

Currently, Android has two APIs with which we can schedule jobs:

In AlarmManager API, we can schedule a task to be done by the system at a given time in the future. The AlarmManager requires a broadcast receiver to capture the task and perform the required action. We can also schedule repetitive jobs which are to be performed at certain intervals. While we are scheduling an alarm, we must pass the type, the time when the alarm is scheduled, and a PendingIntent.

JobScheduler API was introduced in Lollipop and its the most efficient way to perform background work, especially networking. It performs background work based on conditions, not on time. These conditions may be whether the device is connected to a network, charging or idle.

The JobScheduler API operates at the system level, so it has many features by which it can intelligently schedule and trigger the jobs that have been assigned.

Modern Android applications should use the JobScheduler API. Apps can schedule jobs while letting the system optimize based on memory, power, and connectivity conditions.

The JobScheduler API does not work solely on time scheduled tasks, but checks whether the correct conditions are met before triggering a task. The JobScheduler API runs on the main thread of the application, so it can interact with the app more easily and reduce the chances of being stopped by the system to free memory.

Let's look at advantages of the JobScheduler API. Compared to a custom SyncAdapter or the AlarmManager, the JobScheduler supports batch scheduling of jobs. The Android system can combine jobs so that battery consumption is reduced. JobManager makes handling uploads easier as it handles automatically the unreliability of the network. It also survives application restarts. Here are example when you would use this job scheduler:

  • Tasks that should be done once the device is connect to a power supply.
  • Tasks that require network access or a Wi-Fi connection.
  • Task that are not critical or user facing.
  • Tasks that should be running on a regular basis as batch where the timing is not critical.

The JobScheduler consists of three main sections:

  • JobInfo
  • JobService
  • JobScheduler

Let us look into each one of them in detail.

JobInfo

A unit of work is encapsulated by a JobInfo object. This object specifies the scheduling criteria. The job scheduler allows to consider the state of the device, e.g., if it is idle or if network is available at the moment. You can schedule the task to run under specific conditions, such as:

  • Device is charging
  • Device is connected to an unmetered network
  • Device is idle
  • Start before a certain deadline
  • Start within a predefined time window, e.g., within the next hour
  • Start after a minimal delay, e.g., wait a minimum of 10 minutes

These constraints can be combined. For example, you can schedule a job every 20 minutes, whenever the device is connected to an unmetered network. Deadline is a hard constraint, if that expires the job is always scheduled.

So, all the parameters that are to be used while scheduling the Job by the scheduler are defined in the JobInfo class. We can use JobInfo.Builder to create an instance of JobInfo. This builder requires the jobId and the service component as its parameters. JobId can be used to uniquely identify jobs that are scheduled to perform a task. They can be also used to monitor whether the job has been successfully called or not by the app when the conditions are met.

Let’s take a moment and talk about the potential criteria you can include in your JobInfo object.

  • Network type (metered/unmetered). If your job requires network access, you must include this condition. You can specify a metered or unmetered network, or any type of network. But not calling this when building your JobInfo means the system will assume you do not need any network access and you will not be able to contact your server.
  • Charging and Idle. If your app needs to do work that is resource-heavy, it is highly recommended that you wait until the device is plugged in and/or idle. (Note that idle here is not the same as the Doze idle mode, but rather just means the screen is off and it hasn’t been used in a little while.)
  • Content Provider update. As of API 24, you can now use a content provider change as a trigger to perform some work. You will need to specify the trigger URI, which will be monitored with a ContentObserver. You can also specify a delay before your job is triggered, if you want to be certain that all changes propagate before your job is run.
  • Backoff criteria. You can specify your own back-off/retry policy. This defaults to an exponential policy, but if you set your own and then return true for rescheduling a job (with onStopJob(), for example), the system will employ your specified policy over the default.
  • Minimum latency and override deadline. If your job cannot start for at least X amount of time, or cannot be delayed past a specific time, you can specify those values here. Even if all conditions have not been met, your job will be run by the deadline (you can check the result of isOverrideDeadlineExpired() to determine if you’re in that case). And if they are met, but your minimum latency has not elapsed, your job will be held.
  • Periodic. If you have work that needs to be done regularly, you can set up a periodic job. This is a great alternative to a repeating alarm for most developers. Because you sort it all out once, schedule it, and the job will run once in each specified period.
  • Persistent. Any work that needs to be persisted across a reboot can be marked as such here. Once the device reboots, the job will be rescheduled according to the conditions. (Note that your app needs the RECEIVE_BOOT_COMPLETED permission for this to work, though.)
  • Extras. If your job needs some information from your app to perform its work, you can pass along primitive data types as extras in the JobInfo.

The following are the parameters that can be set using the JobInfo class:

  • setBackoffCriteria(long initialBackoffMillis, int backoffPolicy). Back-off Criteria is the policy that checks when a job is finished or a retry is requested. You can set the initial back off time and whether it is linear or exponential. The default is 30 sec and exponential. The max back-off capacity is five hrs. Also, setting this method for a job along with setRequiresDeviceIdle(boolean) will throw an exception when you call build(), as back off typically does not make sense for these types of jobs. The back off policy can be set one of the two values:
    • JobInfo.BACKOFF_POLICY_LINEAR will use the same back off time for the next retry attempt.
    • JobInfo.BACKOFF_POLICY_EXPONENTIAL will retry after exponentially increasing the back off time.
  • setExtras(PersistableBundle extras). A Bundle of extras. This lets you send specific data to your job. As this is persisted, only primitive types are allowed.
  • setMinimumLatency(long minLatencyMillis). A minimum amount of time your job should be delayed. Calling this method for a periodic job will throw an exception when you call build().
  • setOverrideDeadline(long maxExecutionDelayMillis). A maximum amount of time to wait to execute your job. If you hit this time, your job will be executed immediately regardless of your other parameters. Calling this method for a periodic job will throw an exception when you call build().
  • setPeriodic (long intervalMillis). If you want the job to be repeated, you can specify the interval between repeats. You are guaranteed to be executed within an interval but cannot guarantee at what point during that interval this willl occur. This can sometimes lead to jobs being run closely together. Setting this function on the builder with setMinimumLatency(long) or setOverrideDeadline(long) will result in an error.
  • setPersisted(boolean isPersisted). You can persist the job across boot. This requires the RECEIVE_BOOT_COMPLETED permission to be added to your manifest.
  • setRequiredNetworkType(int networkType). The network type you want the device to have when your job is executed. You can choose between:
    • NETWORK_TYPE_NONE. No network connection is required for the job. This is the default value set for the job.
    • NETWORK_TYPE_ANY. This specifies that the Job requires an internet connection, but it can be of any type, such as Wi-Fi, mobile network, or any other type of connection.
    • NETWORK_TYPE_UNMETERED. This is specified when a job has to be triggered when the network type is unmetered, for example, Wi-Fi.
    • NETWORK_TYPE_NOT_ROAMING. This is added in API 24. It is specified when network should not be roaming to trigger the Job.
  • setRequiresCharging(boolean requiresCharging). Whether or not the device should be charging.
  • setRequiresDeviceIdle(boolean requiresDeviceIdle). If the device should be idle when running the job. This is a great time to do resource heavy jobs.

Here is a code example of how a JobInfo object can be initialized.

ComponentName serviceComponent = new ComponentName(context, MyJobService.class);
JobInfo jobInfo = new JobInfo.Builder(0, serviceComponent)
.setBackoffCriteria(30*1000, JobInfo.BACKOFF_POLICY_EXPONENTIAL)
.setMinimumLatency(5 * 1000) // wait at least
.setOverrideDeadline(10 * 1000) // maximum delay
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) // require unmetered network
.setRequiresDeviceIdle(true) // device should be idle
.setRequiresCharging(false) // we don't care if the device is charging or not
.build();

Next, we will see about the JobService.

JobService

The JobService is the actual service that is going to run our Job. The new JobService must be registered in the AndroidManifest with the BIND_JOB_SERVICE permission. This service has different methods to implement than a normal service:

  • onStartJob(JobParameters params). This method is what gets called when the JobScheduler decides to run your job based on its parameters. You can get the jobId from the JobParameters and you will have to hold on to these parameters to finish the job later. The jobId can be used to identify each job. The onStartJob is performed in the main thread, if you start asynchronous processing in this method, return true otherwise false.
  • onStopJob(JobParameters params). This will get called when your parameters are no longer being met. In our previous example, this would happen when the user switches off of Wi-Fi, unplugs, or turns the screen off on their device. If the job fails for some reason, return true from on the onStopJob to restart the job.

Here are some important things to know when using a JobService:

  • The JobService runs on the main thread. It is your responsibility to move your work off-thread. If the user tries to open your app while a job is running on the main thread they might get an Android Not Responding (ANR) error. This can be done by performing the tasks to be done in the service in a background thread within the service itself, using a Handler or an AsyncTask.
  • You must finish your job when it is complete. The JobScheduler keeps a wake lock for your job. If you don't call jobFinished() with the JobParameters from onStartJob() the JobScheduler will keep a wake lock for your app and burn the device's battery. Even worse, the battery history will blame your app. So, remember to call jobFinished when the job has completed its work. jobFinished() requires two parameters: the current job, so that it knows which wakelock can be released, and a boolean indicating whether you’d like to reschedule the job.
  • You have to register your job service in the AndroidManifest. If you do not, the system will not be able to find your service as a component and it will not start your jobs. You'll never even know as this does not produce an error.

The new JobService must be registered in the AndroidManifest with the BIND_JOB_SERVICE permission.

<service
    android:name=".MyJobService"
    android:permission="android.permission.BIND_JOB_SERVICE">
</service>

Here is a code example to show how the JobService class looks:

public class MyJobService extends JobService {
    private static final String TAG = MyJobService.class.getSimpleName();
    JobParameters params;

    @Override
    public boolean onStartJob(JobParameters params) {
        Log.d(TAG, "Job Started");
        this.params = params;
        doTask();

        // return true if using background thread else return false
        return true;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        Log.d(TAG, "Job Force Stopped");

        // reschedule job return true else return false
        return true;
    }

    private void doTask() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        jobFinished(params, false);
    }
}

Next, we will see how to schedule the Job using the JobScheduler class.

JobScheduler

We now have our JobInfo and our JobService, so it is time to schedule our job. All we have to do is get the JobService the same way you would get any system service and hand it our JobInfo with the schedule(JobInfo job) method.

Here is a code example showing how the job has to be scheduled:

JobScheduler jobScheduler = (JobScheduler)getSystemService(Context.JOB_SCHEDULER_SERVICE);
jobScheduler.schedule(jobInfo);
comments powered by Disqus