8.3: Job Scheduler

Contents:

You've seen that you can trigger events based on the real-time clock, or the elapsed time since boot using the AlarmManager class. Most tasks, however, do not require an exact time, but should be scheduled based on a combination of system and user requirements. For example, a news app might like to update the news in the morning, but could wait until the device is charging and connected to wifi to update the news, to preserve the user's data and system resources.

The JobScheduler class is meant for this kind of scheduling; it allows you to set the conditions, or parameters of running your task. Given these conditions, the JobScheduler calculates the best time to schedule the execution of the job. Some examples of these parameters are: persistance of the job across reboots, the interval that the job should run at, whether or not the device is plugged in, or whether or not the device is idle.

The task to be run is implemented as a JobService subclass and executed according to the specified constraints.

JobScheduler is only available on devices running API 21+, and is currently not available in the support library. For backward compatibility, use the GcmNetworkManager (soon to be FirebaseJobDispatcher).

In this practical, you will create an app that schedules a notification to be posted when the parameters set by the user are fulfilled, and the system requirements are met.

What you should already KNOW

From the previous practicals, you should be able to:

  • Deliver a notification.
  • Get an integer value from a Spinner view.
  • Use Switch views for user input.
  • Create PendingIntents.

What you will LEARN

You will learn to:

  • Implement a JobService.
  • Construct a JobInfo object with specific constraints.
  • Schedule a JobService based on the JobInfo object.

What you will DO

In this practical, you will:

  • Implement a JobService that delivers a simple notification to let the user know the job is running.
  • Get user input to configure the constraints (such as waiting until the device is charging) on the JobService you are scheduling.
  • Schedule the job using JobScheduler.

App Overview

For this practical you will create an app called "Notification Scheduler". Your app will demonstrate the JobScheduler framework by allowing the user to select constraints and schedule a job. When that job is executed, it will post a notification (in this app, your notification is effectively your "job"). Preview for the Notification Scheduler

To use the JobScheduler, you need two additional parts: JobService and JobInfo. A JobInfo object contains the set of conditions that will trigger the job to run. A JobService is the implementation of the job that is to run under those conditions.

Task 1. Implement a JobService

To begin with, you must create a service that will be run at the time determined by the conditions. The JobService is automatically executed by the system, and the only parts you need to implement are:

onStartJob() callback

  • called when the system determines that your task should be run. You implement the job to be done in this method.
    Note: onStartJob() is executed on the main thread, and therefore any long-running tasks must be offloaded to a different thread. In this case, you are simply posting a notification, which can be done safely on the main thread.
  • returns a boolean indicating whether the job needs to continue on a separate thread. If true, the work is offloaded to a different thread, and your app must call jobFinished() explicitly in that thread to indicate that the job is complete. If the return value is false, the framework knows that the job is completed by the end of onStartJob() and it will automatically call jobFinished() on your behalf.

onStopJob() callback

  • called if the conditions are no longer met, meaning that the job must be stopped.
  • returns a boolean that determines what to do if the job is not finished. If the return value is true, the job will be rescheduled, otherwise, it will be dropped.

1.1 Create the Project and the NotificationJobService

Verify that the minimum SDK you are using is API 21. Prior to API 21, JobScheduler does not work, as it is missing some of the required APIs.

  1. Use the empty template , and create a new project called "Notification Scheduler".
  2. Create a new Java class called NotificationJobService that extends JobService.
  3. Add the required methods: onStartJob() and onStopJob().
  4. In your AndroidManfiest.xml file, register your JobService with the following permission inside the <application> tag:
    <service
       android:name=".NotificationJobService"
       android:permission="android.permission.BIND_JOB_SERVICE"/>
    

1.2 Implement onStartJob()

  1. Add a notification icon for the "Job Running" notification.
  2. In onStartJob(), create a PendingIntent to launch the MainActivity of your app to be used as the content intent for your notification.
  3. In onStartJob(), construct and deliver a notification with the following attributes:

    Attribute

    Title

    Content Title

    "Job Service"

    Content Text

    "Your Job is running!"

    Content Intent

    contentPendingIntent

    Small Icon

    R.drawable.ic_job_running

    Priority

    NotificationCompat.PRIORITY_HIGH

    Defaults

    NotificationCompat.DEFAULT_ALL

    AutoCancel

    true

  4. Make sure onStartJob() returns false, because all of the work is completed in that callback.
  5. Make onStopJob() return true, so that the job is rescheduled if it fails.
@Override
public boolean onStartJob(JobParameters jobParameters) {
   //Set up the notification content intent to launch the app when clicked
   PendingIntent contentPendingIntent = PendingIntent.getActivity
           (this, 0, new Intent(this, MainActivity.class),
            PendingIntent.FLAG_UPDATE_CURRENT);

   NotificationManager manager =
       (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

   NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
           .setContentTitle(getString(R.string.job_service))
           .setContentText(getString(R.string.job_running))
           .setContentIntent(contentPendingIntent)
           .setSmallIcon(R.drawable.ic_job_running)
           .setPriority(NotificationCompat.PRIORITY_HIGH)
           .setDefaults(NotificationCompat.DEFAULT_ALL)
           .setAutoCancel(true);

   manager.notify(0, builder.build());

   return false;
}

Task 2. Implement the job conditions

Now that you have your JobService, it is time to identify the criteria for running the job. For this, use the JobInfo component. You will create a series of parameterized conditions for running a job using a variety of network connectivity types and device status.

To begin, you will create a group of radio buttons to determine the network type required for this job.

2.1 Implement the network constraint

One of the possible conditions for running a Job is the status of your device's network connectivity. You can limit the JobService to be executed only when certain network conditions are met. The options are:

  • NETWORK_TYPE_NONE: the job will run with or without a network connection. This is the default value.
  • NETWORK_TYPE_ANY: the job will run as long as a network (cellular, wifi) is available.
  • NETWORK_TYPE_UNMETERED: the job will run as long as the device is connected to wifi that does not use a HotSpot.

Create the layout for your app

Create the layout for your app to show the buttons for the user to choose the network criteria. UI for Network Condition Controls

  1. In your activity_main.xml file, change the rootview element to a vertical LinearLayout.
  2. Change the TextView to have the following attributes:

    Attribute

    Value

    android:layout_width

    "wrap_content"

    android:layout_height

    "wrap_content"

    android:text

    "Network Type Required: "

    android:textAppearance

    "@style/TextAppearance.AppCompat.Subhead"

    android:layout_margin

    "4dp"

  3. Add a RadioGroup container element below the TextView with the following attributes:

    Attribute

    Value

    android:layout_width

    "wrap_content"

    android:layout_height

    "wrap_content"

    android:orientation

    "horizontal"

    android:id

    "@+id/networkOptions"

    android:layout_margin

    "4dp"

    Note: Using a radio group ensures that only one of its children can be selected at a time. For more information on Radio Buttons see this guide.
  4. Add three RadioButtons as children to the RadioGroup with their layout height and width set to "wrap_content" and the following attributes:

    RadioButton 1

    android:text

    "None"

    android:id

    "@+id/noNetwork"

    android:checked

    true

    RadioButton 2

    android:text

    "Any"

    android:id

    "@+id/anyNetwork"

    RadioButton 3

    android:text

    "Wifi"

    android:id

    "@+id/wifiNetwork"

  5. Add two buttons below the radio button group with height and width set to "wrap content" with the following attributes:

    Button 1

    android:text

    "Schedule Job"

    android:onClick

    "scheduleJob"

    android:layout_gravity

    "center_horizontal"

    android:layout_margin

    "4dp"

    Button 2

    android:text

    "Cancel Jobs"

    android:onClick

    "cancelJobs"

    android:layout_gravity

    "center_horizontal"

    android:layout_margin

    "4dp"

  6. Add the method stubs for both of the onClick() methods in MainActivity.

Get the selected network option

  1. In scheduleJob(), find the RadioGroup by id and save it in an instance variable called networkOptions.
  2. Get the selected network id and save it in a integer variable:
    int selectedNetworkID = networkOptions.getCheckedRadioButtonId();
    
  3. Create a selected network option integer variable and set it equal to the default network option (no network required):
    int selectedNetworkOption = JobInfo.NETWORK_TYPE_NONE;
    
  4. Create a switch statement with the selected network id, and add a case for each of the possible id's:
    switch(selectedNetworkID){
        case R.id.noNetwork:
            break;   
        case R.id.anyNetwork:
            break;
        case R.id.wifiNetwork:
            break;
    }
    
  5. Assign the selected network option the appropriate JobInfo network constant, depending on the case:
    switch(selectedNetworkID){
       case R.id.noNetwork:
           selectedNetworkOption = JobInfo.NETWORK_TYPE_NONE;
           break;
       case R.id.anyNetwork:
           selectedNetworkOption = JobInfo.NETWORK_TYPE_ANY;
           break;
       case R.id.wifiNetwork:
           selectedNetworkOption = JobInfo.NETWORK_TYPE_UNMETERED;
           break;
    }
    

Create the JobScheduler and the JobInfo object

  1. In MainActivity, create a member variable for the JobScheduler, and initialize it in scheduleJob() using getSystemService():
    mScheduler = (JobScheduler) getSystemService(JOB_SCHEDULER_SERVICE);
    
  2. Create a member constant for the JOB_ID, and set it equal to 0.
  3. Create a JobInfo.Builder object in scheduleJob(). The constructor for the JobInfo.Builder class takes two parameters:
    • The JOB_ID.
    • The ComponentName for the JobService you created. A ComponentName is used to identify the JobService with the JobInfo object.
      ComponentName serviceName = new ComponentName(getPackageName(),
      NotificationJobService.class.getName());
      JobInfo.Builder builder = new JobInfo.Builder(JOB_ID, serviceName)
      
  4. Call setRequiredNetworkType() on the JobInfo.Builder object, passing in the selected network option:
    .setRequiredNetworkType(selectedNetworkOption);
    
  5. Call schedule() on the JobScheduler object, passing in the JobInfo object with the build() method:
    JobInfo myJobInfo = builder.build();
    mScheduler.schedule(myJobInfo);
    
  6. Show a Toast message, letting the user know the job was scheduled.
  7. In the cancelJobs() method, check if the JobScheduler object is null, and if not, call cancelAll() on it to remove all pending jobs, reset the JobScheduler to be null, and show a Toast message to let the user know the job was canceled:
    if (mScheduler!=null){
       mScheduler.cancelAll();
       mScheduler = null;
       Toast.makeText(this, "Jobs Canceled", Toast.LENGTH_SHORT).show();
    }
    
  8. Run the app. You can now set tasks that have network restrictions and see how long it takes for them to be executed. In this case, the task is to deliver a notification. To dismiss the notification, either swipe it away or tap on it to open the notification.

You may notice that if you do not change the network constraint to either "Any" or "Wifi", the app will crash with the following exception:

java.lang.IllegalArgumentException:
   You're trying to build a job with no constraints, this is not allowed.

This is because the "No Network Required" condition is the default and does not actually count as a constraint. The JobScheduler needs at least one constraint to properly schedule the JobService. In the following section you will create a conditional that is true when at least one constraint is set, and false otherwise. You will then schedule the task if it's true, and show a Toast to tell the user to set a constraint if it isn't.

2.2 Check for constraints

JobScheduler requires at least one constraint to be set. In this task you will create a boolean that will track if this requirement has been met, so that you can notify the user to set at least one constraint if they haven't already. As you create additional options in the further steps, you will need to modify this boolean so it is always true if at least one constraint is set, and false otherwise.

  1. Create a boolean variable called constraintSet that is true if selected network option is not the default JobInfo.NETWORK_TYPE_NONE:
    boolean constraintSet = selectedNetworkOption != JobInfo.NETWORK_TYPE_NONE;
    
  2. Create an if/else block using the constraintSet boolean.
  3. Move the code that schedules the task and shows the Toast message into the if block.
  4. If constraintSet is false, show a Toast message to the user to set at least one constraint. Don't forget to extract your string resources:
    if(constraintSet) {
       //Schedule the job and notify the user
       JobInfo myJobInfo = builder.build();
       mScheduler.schedule(myJobInfo);
       Toast.makeText(this, R.string.job_scheduled, Toast.LENGTH_SHORT).show();
    } else {
       Toast.makeText(this, R.string.no_constraint_toast, Toast.LENGTH_SHORT).show();
    }
    

2.3 Implement the Device Idle and Device Charging constraints

JobScheduler includes the ability to wait until the device is charging, or in an idle state (the screen is off, and the CPU has gone to sleep) to execute your JobService. You will now add switches to your app to toggle these constraints on your JobService.

Add the UI elements for the new constraints UI Controls for Device Idle and Device Charging Options

  1. In your activity_main.xml file, copy the network type label TextView and paste it below the RadioGroup.
  2. Change the android:text attribute to "Requires:".
  3. Below this textview, insert a horizontal LinearLayout with a 4dp margin.
  4. Create two Switch views as children to the horizontal LinearLayout with height and width set to "wrap_content" and the following attributes:

    Switch 1

    android:text

    "Device Idle"

    android:id

    "@+id/idleSwitch"

    Switch 2

    android:text

    "Device Charging"

    android:id

    "@+id/chargingSwitch"

Add the code for the new constraints

  1. In MainActivity, create member variables, mDeviceIdle and mDeviceCharging, for the switches and initialize them in onCreate().
  2. In the scheduleJob() method, add the following calls to set the constraints on the JobScheduler based on the user selection in the switches:
    builder.setRequiresDeviceIdle(mDeviceIdle.isChecked());
    builder.setRequiresCharging(mDeviceCharging.isChecked());
    
  3. Update the code that sets constraintSet to consider these new constraints:
    boolean constraintSet = (selectedNetworkOption != JobInfo.NETWORK_TYPE_NONE)
       || mDeviceChargingSwitch.isChecked() || mDeviceIdleSwitch.isChecked();
    
  4. Run your app, now with the additional constraints. Try the difference combinations of switches to see when the notification gets sent (that indicates that the job ran). You can test the charging state constraint in an emulator by opening the menu (the ellipses icon next to the emulated device), go to the Battery pane and toggle the Battery Status dropdown. There is no way to manually put the emulator in Idle mode as of the writing of this practical.

Waiting until the device is idle and plugged in is a common pattern for battery intensive tasks such as downloading or uploading large files.

2.4 Implement the Override Deadline constraint

Up to this point, there is no way to know precisely when the framework will execute your task. The system takes into account effective resource management which may delay your task depending on the state of the device, and does not guarantee that your task will run on time. For example, a news app may want to download the latest news only when wifi is available and the device is plugged in and charging; but a user may inadvertently forget to enable their wifi or charge their device. If you don't add a time parameter to your scheduled Job, that user will be disappointed when they wake up to yesterday's news. For this reason, the JobScheduler API includes the ability to set a hard deadline that will override the previous constraints.

Add the new UI for setting the deadline to run the task UI Controls for the Override Deadline Condition

In this step you will use a new UI component, a Seekbar, to allow the user to set a deadline between 0 and 100 seconds to execute your task.

The user sets the value by dragging the SeekBar.

  1. Create a horizontal LinearLayout below the existing LinearLayout with the switches, which will contain the labels for the SeekBar.
  2. The SeekBar will have two labels: a static one just like the label for the RadioGroup of buttons, and a dynamic one that will be updated with the value from the SeekBar. Add two TextViews to the LinearLayout with the following attributes:

    TextView 1

    android:layout_width

    "wrap_content"

    android:layout_height

    "wrap_content"

    android:text

    "Override Deadline: "

    android:id

    "@+id/seekBarLabel"

    android:textAppearance

    "@style/TextAppearance.AppCompat.Subhead"

    TextView 2

    android:layout_width

    "wrap_content"

    android:layout_height

    "wrap_content"

    android:text

    "Not Set"

    android:id

    "@+id/seekBarProgress"

    android:textAppearance

    "@style/TextAppearance.AppCompat.Subhead"

  3. Add a SeekBar view below the LinearLayout with the following attributes:

    Attribute

    Value

    android:layout_width

    "match_parent"

    android:layout_height

    "wrap_content"

    android:id

    "@+id/seekBar"

    android:layout_margin

    "4dp"

Write the code for adding the deadline

  1. In MainActivity, create a member variable for the SeekBar and initialize it in onCreate():
    mSeekBar = (SeekBar) findViewById(R.id.seekBar);
    
  2. Create final variables for both TextViews (they will be accessed from an inner class) and initialize them in onCreate():
    final TextView label = (TextView) findViewById(R.id.seekBarLabel);
    final TextView seekBarProgress = (TextView) findViewById(R.id.seekBarProgress);
    
  3. In onCreate(), call setOnSeekBarChangeListener() on the SeekBar, passing in a new OnSeekBarChangeListener (Android Studio should generate the required methods):

    mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
       @Override
       public void onProgressChanged(SeekBar seekBar, int i, boolean b) {}
    
       @Override
       public void onStartTrackingTouch(SeekBar seekBar) {}
    
       @Override
       public void onStopTrackingTouch(SeekBar seekBar) {}
    });
    
  4. The second argument of onProgressChanged() is the current value of the SeekBar. In the onProgressChanged() callback, check if the integer value is greater than 0 (meaning a value has been set by the user), and if it is, set the SeekBar progress label to the integer value, followed by "s" to indicate seconds:
    if (i > 0){
        mSeekBarProgress.setText(String.valueOf(i) + " s");
    }
    
  5. Otherwise, set the TextView to read "Not Set":
    else {
        mSeekBarProgress.setText("Not Set");
    }
    
  6. The override deadline should only be set if the integer value of the SeekBar is greater than 0. In the scheduleJob() method, create an integer to store the SeekBar progress and a boolean variable that is true if the SeekBar has an integer value greater than 0:
    int seekBarInteger = mSeekBar.getProgress();
    boolean seekBarSet = seekBarInteger > 0;
    
  7. If this boolean is true, call setOverrideDeadline() on the JobInfo.Builder, passing in the integer value from the SeekBar multiplied by 1000 (the parameter is in milliseconds, you want the user to set the deadline in seconds):
    if (seekBarSet) {
          builder.setOverrideDeadline(seekBarInteger * 1000);
    }
    
  8. Modify the constraintSet boolean to include the value of seekBarSet as a possible constraint:
    boolean constraintSet = selectedNetworkOption != JobInfo.NETWORK_TYPE_NONE
       || mDeviceChargingSwitch.isChecked() || mDeviceIdleSwitch.isChecked()
       || seekBarSet;
    
  9. Run the app. The user can now set a hard deadline in seconds by which time the JobService must be run!

2.5 Implement the Periodic constraint

JobScheduler also allows you to schedule a repeated task, much like AlarmManager. This option has a few caveats:

  • The task is not guaranteed to run in a given period (the other conditions may not be met, or there might not be enough system resources).
  • Using this constraint prevents you from also setting an override deadline or a minimum latency (), since these options do not make sense for repetitive tasks. See JobInfo.Builder) documentation for more information.

Add the Periodic Switch to the layout

You will add a Switch to allow the user to switch between having the job run once or repeatedly at periodic intervals.

  1. In activity_main.xml, add a Switch view between the two horizontal LinearLayouts. Use the following attributes:

    Attribute

    Value

    android:layout_width

    "wrap_content"

    android:layout_height

    "wrap_content"

    android:text

    "Periodic"

    android:id

    "@+id/periodicSwitch"

    android:layout_margin

    "4dp"

  2. Create a member variable for the switch and initialize it in onCreate():
    mPeriodicSwitch = (Switch) findViewById(R.id.periodicSwitch);
    

Write the code to use the Periodic Switch

The override deadline and periodic constraints are mutually exclusive. You will use the switch to toggle the functionality and label of the SeekBar to represent either the override deadline, or the periodic interval.

  1. Call setOnCheckedChangeListener() on the periodic switch, passing in a new OnCheckedChangeListener.
  2. If checked, set the label to "Periodic Interval: ", otherwise to "Override Deadline: ":
    mPeriodicSwitch.setOnCheckedChangeListener(
       new CompoundButton.OnCheckedChangeListener() {
       @Override
       public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
           if (isChecked){
               label.setText(R.string.periodic_interval);
           } else {
               label.setText(R.string.override_deadline);
           }
       }
    });
    

All that remains now is to implement the logic in the scheduleJob() method to properly set the constraints on the JobInfo object.

If the periodic option is on:

  • If the SeekBar has a non-zero value, set the constraint by calling setPeriodic() on the JobInfo.Builder object.
  • If the SeekBar has a value of 0, show a Toast message asking the user to set a periodic interval with the SeekBar.

If the periodic option is off:

  • If the SeekBar has a non-zero value, the user has set an override deadline. Apply the override deadline using the setOverrideDeadline() option.
  • If the SeekBar has a value of 0, the user has simply not specified an override deadline or a periodic task, so add nothing to the JobInfo.Builder object.
  • Replace the code that sets the override deadline to the JobInfo.Builder in scheduleJob() with the following code to implement this logic:
    if (mPeriodicSwitch.isChecked()){
       if (seekBarSet){
           builder.setPeriodic(seekBarInteger * 1000);
       } else {
           Toast.makeText(MainActivity.this,
               "Please set a periodic interval", Toast.LENGTH_SHORT).show();
       }
    } else {
       if (seekBarSet){
           builder.setOverrideDeadline(seekBarInteger * 1000);
       }
    }
    

Solution code

Android Studio project: NotificationScheduler

Coding challenge

Note: All coding challenges are optional and are not prerequisites for later lessons.

Challenge: Up until now, your tasks scheduled by the JobService focused on delivering a notification. Most of the time, however, JobScheduler is used for more robust background tasks such as updating the weather or syncing with a database. Since background tasks can be more complex in nature, both from a programmatic and from a functionality standpoint, the job of notifying the framework when the task is complete falls on the developer. Fortunately, the developer can do this by calling jobFinished().

  1. Implement a JobService that starts an AsyncTask when the given constraints are met. The AsyncTask should sleep for 5 seconds. This will require you to call jobFinished() once the task is complete. If the constraints are no longer met while the thread is sleeping, show a Toast message saying that the job failed and also reschedule the job.

Summary

  • JobScheduler provides a flexible framework to intelligently accomplish background services.
  • JobScheduler is only available on devices running API 21+
  • To use the JobScheduler, you need two parts: JobService and JobInfo.
  • JobInfo is a set of conditions that will trigger the job to run.
  • JobService implements the job to run under the conditions specified by JobInfo.
  • You only have to implement the onStartJob() and onStopJob() callback methods in your JobService.
  • The implementation of your job occurs (or is started) in onStartJob().
  • onStartJob() returns a boolean that indicates whether the service needs to process the work in a separate thread.
  • If onStartJob() returns true, you must explicitly call jobFinished(). If onStartJob() returns false, the runtime will call jobFinished() on your behalf.
  • JobService is processed on the main thread, so avoid lengthy calculations or I/O.
  • JobScheduler is the manager class responsible for scheduling the task.JobScheduler batches tasks together to maximize the efficiency of system resources, which means you do not have exact control of when it will be executed.

The related concept documentation is in Android Developer Fundamentals: Concepts.

Learn more

Android Developer Documentation

Reference

results matching ""

    No results matching ""