The JobScheduler on Android

7 min read

I recently wrote an article about using a Sync Adapter on Android, while it is true that it can do what it was meant to do in an efficient manner, there are certainly better and less robust alternatives out there. Today we are going to talk about one of my personal favorites, the JobScheduler.

Like many Android APIs there are things that no matter how straightforward they may seem, theres is always something, even if its one small tiny thing, that makes it slightly painful. The JobScheduler has 1 or 2 of these, so i thought id talk about them a bit so you guys don’t have to spend time painfully trying to figure it out for yourselves.

Ok, first lets start talking about what a JobScheduler is anyways. Straight from the documentation:

This is an API for scheduling various types of jobs against the framework that will be executed in your application’s own process.

See JobInfo for more description of the types of jobs that can be run and how to construct them. You will construct these JobInfo objects and pass them to the JobScheduler with schedule(JobInfo). When the criteria declared are met, the system will execute this job on your application’s JobService. You identify which JobService is meant to execute the logic for your job when you create the JobInfo with JobInfo.Builder(int, android.content.ComponentName).

The framework will be intelligent about when you receive your callbacks, and attempt to batch and defer them as much as possible. Typically if you don’t specify a deadline on your job, it can be run at any moment depending on the current state of the JobScheduler’s internal queue, however it might be deferred as long as until the next time the device is connected to a power source.

You do not instantiate this class directly; instead, retrieve it through Context.getSystemService(Context.JOB_SCHEDULER_SERVICE).

Very straight forward and easy to understand. Couldn’t have said it better my self. Alright, lets start by getting an instance of the JobScheduler system service.

    private void scheduleJob() {
        final JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
    }

Exactly like the Android docs say, we get can get an instance of the JobScheduler through the Context.getSystemService(SERVICE) call.  The JobScheduler API is pretty simple actually. The JobScheduler has 5 methods that we can use

  • void cancelAll()  Cancels all jobs that have been registered with the JobScheduler by this application
  • void cancel(int jobId) Cancel a job that is pending in the JobScheduler with this jobId
  • int schedule(JobInfo job) Schedules a “Job” created from the JobInfo object passed in
  • List<JobInfo> getAllPendingJobs() Retrieve all jobs for this application that are pending in the JobScheduler, registered by the current application that have not yet been scheduled.
  • JobInfo getPendingJob(int jobId) Retrieve a specific job for this application that is pending in the JobScheduler registered by this application that has not yet been executed.

You may be thinking that this is too simple/good to be true. You are right, it is!

Ok, before moving on, lets clarify what kind of job we want to schedule. Lets go for a job that runs every 6 hours, persists through boot (meaning that even if the device is restarted, it should still run), and on any network type. For this tutorial, we don’t care about the network type.

Unique ids are required to run jobs. if another job with the same id is started then the current already scheduled job

Subsequent calls to cancel, or jobs created with the same jobId, will update the pre-existing job with the same id. This ID must be unique across all clients of the same id (not just the same package). You will want to make sure this is a stable id across app updates, so probably not based on a resource ID.

Keeping this in mind our unique id will 123 :). Lets go ahead and create our job. The JobInfo takes two inputs in its builder, an id and a ComponentName.

final ComponentName name = new ComponentName(this, MyJobService.class);

final JobInfo jobInfo = new JobInfo.Builder(123, name)
        // other builder calls that we will get to in a bit.
        .build();

Identifier for a specific application component (Activity, Service, BroadcastReceiver, or ContentProvider) that is available. Two pieces of information, encapsulated here, are required to identify a component: the package (a String) it exists in, and the class (a String) name inside of that package.

A ComponentName is pretty much what we use to say hey, then the job runs, this is the service we want to use (MyService.class). We will get to the actual service in a second. Lets finish creating out JobInfo object first, but first lets go over the available builder methods available to use.

  • setExtras(PersistableBundle extras) Set optional extras. This is persisted, so only primitive types allowed.
  • setRequiredNetworkType(int networkType) Set some description of the kind of network type the job needs to have. Not calling this function means the network is not necessary.
  • setRequiresCharging(boolean requiresCharging) Specify that to run this job, the device needs to be plugged in. This defaults to false.
  • setRequiresDeviceIdle(boolean requiresDeviceIdle) Specify that to run, the job needs the device to be in idle mode. This defaults to false. Idle mode is a loose definition provided by the system, which means that the device is not in use, and has not been in use for some time. As such, it is a good time to perform resource heavy jobs. Bear in mind that battery usage will still be attributed to your application, and surfaced to the user in battery stats.
  • addTriggerContentUri(@NonNull TriggerContentUri uriMonitors a URI with a ContentObserver. If something changes on the monitored URI, the Job will automatically trigger and run.This is pretty awesome. Oh, and it cannot be used with setPeriodic(long) or setPersisted(boolean). To continually monitor for content changes, you need to schedule a new JobInfo observing the same URIs  before you finish execution of the JobService handling the most recent changes.
  • setTriggerContentUpdateDelay(long durationMs) Sets the delay (in milliseconds) from when a content change is detected until the job is scheduled
  • setTriggerContentMaxDelay(long durationMs) Set the maximum total delay (in milliseconds) that is allowed from the first time a content change is detected until the job is scheduled.
  • setPeriodic(long intervalMillis) Specify that this job should recur with the provided interval, not more than once per period.
  • setMinimumLatency(long minLatencyMillis) Specify that this job should be delayed by the provided amount of time.
  • setOverrideDeadline(long maxExecutionDelayMillis) Set deadline which is the maximum scheduling latency. The job will be run by this deadline even if other requirements are not met.
  • setBackoffCriteria(long initialBackoffMillis, int backoffPolicy)
  • setPersisted(boolean isPersisted) Set whether or not to persist this job across device reboots. This will only have an effect if your application holds the RECEIVE_BOOT_COMPLETED permission
final ComponentName name = new ComponentName(this, MyJobService.class);
// Our job info object
final JobInfo jobInfo = new JobInfo.Builder(123, name)
        .setPeriodic(TimeUnit.HOURS.toMillis(1))
        .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
        .setPersisted(true)
        .build();

Really straightforward. But before i move on i want to mention that there is a problem with this. the setPeriodic() method only really works on android Nougat and above. If we want to get it working on all versions we need to use the setMinimumLatency() method. Which forces us to do something like this

private JobInfo getJobInfo(final int id, final long hour, final ComponentName name) {
    final JobInfo jobInfo;
    final long interval = TimeUnit.HOURS.toMillis(hour);
    final boolean isPersistent = true;
    final int networkType = JobInfo.NETWORK_TYPE_ANY;

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        jobInfo = new JobInfo.Builder(id, name)
                .setMinimumLatency(interval)
                .setRequiredNetworkType(networkType)
                .setPersisted(isPersistent)
                .build();
    } else {
        jobInfo = new JobInfo.Builder(id, name)
                .setPeriodic(interval)
                .setRequiredNetworkType(networkType)
                .setPersisted(isPersistent)
                .build();
    }

    return jobInfo;
}

Interestingly enough, lets take a look at our class service now.

public class MyJobService extends JobService {

    @Override
    public boolean onStartJob(final JobParameters params) {
        // This is where we run all of our code/logic
        return true;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        return true;
    }
}

Entry point for the callback from the JobScheduler.

This is the base class that handles asynchronous requests that were previously scheduled. You are responsible for overriding onStartJob(JobParameters), which is where you will implement your job logic.

This service executes each incoming job on a Handler running on your application’s main thread. This means that you must offload your execution logic to another thread/handler/AsyncTask of your choosing. Not doing so will result in blocking any future callbacks from the JobManager – specifically onStopJob(android.app.job.JobParameters), which is meant to inform you that the scheduling requirements are no longer being met.

Note: A JobService extends Service, and just like a regular Service, we need to handle threading our selves. Read that block of text above again, you will see it says “This service executes each incoming job on a Handler running on your application’s main thread.

Alright, lets take a look at the two methods here, onStartJob(JobParameters) and the onStopJob(JobParameters). The onStartJob gets called when the job is started, and the onStopJob method gets called when the job is stopped. Pretty easy. The return types of these methods are very important, and they determine how the job is handled. By default, the onStartJob runs on the applications main thread, so we need to tell the JobService that we will be doing so, and to do this we return either true or false. If we say True, we are running some process on another thread, then we need call the job finished jobFinished() method.

class MyJobService extends JobService {

    @Override
    public boolean onStartJob(final JobParameters params) {

        HandlerThread handlerThread = new HandlerThread("SomeOtherThread");

        Handler handler = new Handler(handlerThread.getLooper());
        handler.post(new Runnable() {
            @Override
            public void run() {
                // do some stuff, or not
                jobFinished(params, true); // see this, we are saying we just finished the job
            }
        });

        return true;
    }

    @Override
    public boolean onStopJob(final JobParameters params) {
        return false;
    }
}

Lets take a closer look the jobFinished(params, true) method. First we need to under stand what the JobParameters are. JobParameters are used to configure/identify your job. You do not create this object yourself, instead it is handed in to your application by the System. The parameters that this object contains are provided by us in the setExtras() method in the JobInfo object. So when we call jobFinished(params, true) we just keep passing the parameters along incase they need to be used in the onStopJob method. The boolean that is passed in the jobFinished(params, true) method is what tells the “JobManager” that you’d like to reschedule this job based on the retry criteria provided at job creation-time. False to drop the job. Regardless of the value returned, your job must stop executing.

Now, the onStopJob(JobParameters receives the parameters passed in through the jobFinished(params, true) method from the onStartJob(JobParameters) method. The return type of this method lets the JobManager know if the job should be rescheduled according to the back-off criteria specified at schedule-time or not.

Last but not least, lets finally schedule this thing!

Heres The whole activity code. You can find the full code HERE

public class MainActivity extends AppCompatActivity {

    private static final String TAG = MainActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        final FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                scheduleJob();
            }
        });
    }

    private void scheduleJob() {
        final JobScheduler jobScheduler = (JobScheduler) getSystemService(
                Context.JOB_SCHEDULER_SERVICE);

        final ComponentName name = new ComponentName(this, MyJobService.class);

        final int result = jobScheduler.schedule(getJobInfo(123, 1, name));

        if (result == JobScheduler.RESULT_SUCCESS) {
            Log.d(TAG, "Scheduled job successfully!");
        }

    }

    private JobInfo getJobInfo(final int id, final long hour,
                               final ComponentName name) {
        final JobInfo jobInfo;
        final long interval = TimeUnit.HOURS.toMillis(hour);
        final boolean isPersistent = true;
        final int networkType = JobInfo.NETWORK_TYPE_ANY;

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            jobInfo = new JobInfo.Builder(id, name)
                    .setMinimumLatency(interval)
                    .setRequiredNetworkType(networkType)
                    .setPersisted(isPersistent)
                    .build();
        } else {
            jobInfo = new JobInfo.Builder(id, name)
                    .setPeriodic(interval)
                    .setRequiredNetworkType(networkType)
                    .setPersisted(isPersistent)
                    .build();
        }

        return jobInfo;
    }
}

Thats all for now, if you have any question or comments please don’t hesitate to contact me.

And now this