This practical codelab is part of Unit 3: Working in the background in the Android Developer Fundamentals (Version 2) course. You will get the most value out of this course if you work through the codelabs in sequence:

Introduction

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

The JobScheduler class allows you to set the conditions, or parameters, for when to run your task. Given these conditions, JobScheduler calculates the best time to schedule the execution of the job. For example, job parameters can include the persistence of the job across reboots, whether the device is plugged in, or whether the device is idle.

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

JobScheduler is only available on devices running API 21 and higher, and is currently not available in the support library. For backward compatibility, use WorkManager. The WorkManager API lets you schedule background tasks that need guaranteed completion, whether or not the app process is around. For devices running API 14 and higher, including devices without Google Play services, WorkManager provides capabilities that are like those provided by JobScheduler.

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

What you should already know

You should be able to:

What you'll learn

What you'll do

For this practical you 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, the app posts a notification. (In this app, the notification is effectively the "job.")

To use JobScheduler, you need to use JobService and JobInfo:

To begin with, create a service that will run at a time determined by the conditions. The system automatically executes the JobService. The only parts you need to implement are the onStartJob() callback and the onStopJob() callback.

About the onStartJob() callback:

About the onStopJob() callback:

1.1 Create the project and the NotificationJobService class

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

  1. Create a new Java project called "Notification Scheduler". Use API 21 as the target SDK, and use the Empty Activity template.
  2. Inside the com.android.example.notificationscheduler package, create a new Java class that extends JobService. Call the new class NotificationJobService.
  3. Add the required methods, which are onStartJob() and onStopJob(). Click the red light bulb next to the class declaration and select Implement methods, then select OK.
  4. In your AndroidManfest.xml file, inside the <application> tag, register your JobService with the following permission:
<service
   android:name=".NotificationJobService"
   android:permission="android.permission.BIND_JOB_SERVICE"/>

1.2 Implement onStartJob()

Implement the following steps in NotificationJobService.java:

  1. Add an image asset to use as a notification icon for the "Job" notification. Name the image ic_job_running.
  2. Declare a member variable for the notification manager and a constant for the notification channel ID.
NotificationManager mNotifyManager;

// Notification channel ID.
private static final String PRIMARY_CHANNEL_ID =
       "primary_notification_channel";
  1. Inside the onStartJob() method, define a method to create a notification channel.
/**
* Creates a Notification channel, for OREO and higher.
*/
public void createNotificationChannel() {

   // Define notification manager object.
   mNotifyManager =
           (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

   // Notification channels are only available in OREO and higher.
   // So, add a check on SDK version.
   if (android.os.Build.VERSION.SDK_INT >=
           android.os.Build.VERSION_CODES.O) {

       // Create the NotificationChannel with all the parameters.
       NotificationChannel notificationChannel = new NotificationChannel
               (PRIMARY_CHANNEL_ID,
                       "Job Service notification",
                       NotificationManager.IMPORTANCE_HIGH);

       notificationChannel.enableLights(true);
       notificationChannel.setLightColor(Color.RED);
       notificationChannel.enableVibration(true);
       notificationChannel.setDescription
               ("Notifications from Job Service");

       mNotifyManager.createNotificationChannel(notificationChannel);
   }
} 
  1. Inside onStartJob(), call the method to create the notification channel. Create a PendingIntent that launches your app's MainActivity. This intent is the content intent for your notification.
//Create the notification channel
createNotificationChannel();

//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);
  1. 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

  1. Extract your strings.
  2. Make sure that onStartJob() returns false, because for this app, all of the work is completed in the onStartJob() callback.

Here is the complete code for the onStartJob() method:

@Override
public boolean onStartJob(JobParameters jobParameters) {

   //Create the notification channel
   createNotificationChannel();

   //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);

   NotificationCompat.Builder builder = new NotificationCompat.Builder
           (this, PRIMARY_CHANNEL_ID)
           .setContentTitle("Job Service")
           .setContentText("Your Job ran to completion!")
           .setContentIntent(contentPendingIntent)
           .setSmallIcon(R.drawable.ic_job_running)
           .setPriority(NotificationCompat.PRIORITY_HIGH)
           .setDefaults(NotificationCompat.DEFAULT_ALL)
           .setAutoCancel(true);

   mNotifyManager.notify(0, builder.build());
   return false;
}
  1. Make sure that onStopJob() returns true, because if the job fails, you want the job to be rescheduled instead of dropped.

Now that you have your JobService, it's 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 statuses.

To begin, you create a group of radio buttons to determine the network type that this job requires.

2.1 Implement the network constraint

One possible condition for running a job is the status of the device's network connection. You can limit the JobService so that it executes only when certain network conditions are met. There are three options:

Create the layout for your app

Your app layout includes radio buttons with which the user chooses network criteria.

Implement the following steps in the activity_main.xml file. Make sure to extract all the dimensions and string resources.

  1. Change the root view element to a vertical LinearLayout and give the layout a padding of 16dp. You might get a few errors, which you fix later.
<LinearLayout 

xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="vertical"
   android:padding="16dp">

....

</LinearLayout>
  1. 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"

  1. Below the TextView, add a RadioGroup container element 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"

  1. Add three RadioButton views as child elements inside the RadioGroup. For each of the radio buttons, set the layout height and width to "wrap_content", and set 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"

  1. Add two Button views below the RadioGroup. For each of the buttons, set the height and width to "wrap content", and set 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"

  1. In MainActivity, add a method stub for an onClick() method for each of the two buttons.

Get the selected network option

Implement the following steps in MainActivity.java. Extract your string resources when required.

  1. In the scheduleJob() method, find the RadioGroup by ID and save it in an instance variable called networkOptions.
RadioGroup networkOptions = findViewById(R.id.networkOptions);
  1. In the scheduleJob() method, get the selected network ID and save it in an integer variable.
int selectedNetworkID = networkOptions.getCheckedRadioButtonId();
  1. In the scheduleJob() method, create an integer variable for the selected network option. Set the variable to the default network option, which is NETWORK_TYPE_NONE.
int selectedNetworkOption = JobInfo.NETWORK_TYPE_NONE;
  1. In the scheduleJob() method, create a switch statement with the selected network ID. Add a case for each of the possible IDs.
  2. In the scheduleJob() method, 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. Create a member variable for the JobScheduler.
private JobScheduler mScheduler;
  1. Inside the scheduleJob() method, use getSystemService() to initialize mScheduler.
mScheduler = (JobScheduler) getSystemService(JOB_SCHEDULER_SERVICE);
  1. Create a member constant for the JOB_ID, and set it to 0.
private static final int JOB_ID = 0;
  1. Inside the scheduleJob() method, after the Switch block, create a JobInfo.Builder object. The first parameter is the JOB_ID. The second parameter is the ComponentName for the JobService you created. The ComponentName is used to associate the JobService with the JobInfo object.
ComponentName serviceName = new ComponentName(getPackageName(),
       NotificationJobService.class.getName());
JobInfo.Builder builder = new JobInfo.Builder(JOB_ID, serviceName);
  1. Call setRequiredNetworkType() on the JobInfo.Builder object. Pass in the selected network option.
.setRequiredNetworkType(selectedNetworkOption);
  1. Call schedule() on the JobScheduler object. Use the build() method to pass in the JobInfo object.
JobInfo myJobInfo = builder.build();
mScheduler.schedule(myJobInfo);
  1. Show a Toast message, letting the user know the job was scheduled.
Toast.makeText(this, "Job Scheduled, job will run when " +
       "the constraints are met.", Toast.LENGTH_SHORT).show();
  1. In the cancelJobs() method, check whether the JobScheduler object is null. If not, call cancelAll() on the object to remove all pending jobs. Also reset the JobScheduler to null and show a toast message to tell the user that the job was canceled.
if (mScheduler!=null){
   mScheduler.cancelAll();
   mScheduler = null;
   Toast.makeText(this, "Jobs cancelled", Toast.LENGTH_SHORT).show();
}
  1. Run the app. You can now set tasks that have network restrictions and see how long it takes for the tasks to be executed. In this case, the task is to deliver a notification. To dismiss the notification, the user either swipes the notification away or taps it to open the app.

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

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

The crash happens because the "No Network Required" condition is the default, and this condition does not count as a constraint. To properly schedule the JobService, the JobScheduler needs at least one constraint.

In the following section you create a conditional variable that's true when at least one constraint is set, and false otherwise. If the conditional is true, your app schedules the task. If the conditional is false, your app shows a toast message that tells the user to set a constraint.

2.2 Check for constraints

JobScheduler requires at least one constraint to be set. In this task you create a boolean that tracks whether this requirement is 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.

Implement the following steps in MainActivity.java, inside scheduleJob():

  1. After the JobInfo.Builder definition, above the myJobInfo definition, create a boolean variable called constraintSet. The variable is true if the selected network option is not the default. (The default is JobInfo.NETWORK_TYPE_NONE.)
boolean constraintSet = selectedNetworkOption != JobInfo.NETWORK_TYPE_NONE;
  1. After the constraintSet definition, create an if/else block using the constraintSet variable.
  2. Move the code that schedules the job and shows the toast message into the if block.
  3. If constraintSet is false, show a toast message to the user telling them 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, "Job Scheduled, job will run when " +
           "the constraints are met.", Toast.LENGTH_SHORT).show();
}else {
   Toast.makeText(this, "Please set at least one constraint",
           Toast.LENGTH_SHORT).show();
}

2.3 Implement the Device Idle and Device Charging constraints

Using JobScheduler, you can have your app wait to execute your JobService until the device is charging, or until the device is in an idle state (screen off and CPU asleep).

In this section, you add switches to your app to toggle these constraints on your JobService.

Add the UI elements for the new constraints

Implement the following steps in your activity_main.xml file:

  1. Copy the TextView that you used for the network-type label and paste it below the RadioGroup.
  2. Change the android:text attribute to "Requires:".
  3. Below this, add a horizontal LinearLayout with a 4dp margin.

Attribute

Value

android:layout_width

"match_parent"

android:layout_height

"wrap_content"

android:orientation

"horizontal"

android:layout_margin

"4dp"

  1. Create two Switch views as children to the horizontal LinearLayout. Set the height and width to "wrap_content", and use 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

Implement the following steps in MainActivity.java:

  1. Create member variables called mDeviceIdle and mDeviceCharging, for the switches. Initialize the variables in onCreate().
//Switches for setting job options
private Switch mDeviceIdleSwitch;
private Switch mDeviceChargingSwitch;

onCreate():

mDeviceIdleSwitch = findViewById(R.id.idleSwitch);
mDeviceChargingSwitch = findViewById(R.id.chargingSwitch);
  1. In the scheduleJob() method, add the following calls. The calls set constraints on the JobInfo.Builder based on the user selection in the Switch views, during the creation of the builder object.
.setRequiresDeviceIdle(mDeviceIdleSwitch.isChecked())
.setRequiresCharging(mDeviceChargingSwitch.isChecked());
  1. Update the code that sets constraintSet to consider the new constraints:
boolean constraintSet = (selectedNetworkOption != JobInfo.NETWORK_TYPE_NONE)
       || mDeviceChargingSwitch.isChecked() || mDeviceIdleSwitch.isChecked();
  1. Run your app, now with the additional constraints. Try different combinations of switches to see when the notification that indicates that the job ran is sent.

To test the charging-state constraint in an emulator:

  1. Open the More menu (the ellipses icon next to the emulated device).
  2. Go to the Battery pane.
  3. Toggle the Battery Status drop-down menu. There is currently no way to manually put the emulator in idle mode.

For battery-intensive tasks such as downloading or uploading large files, it's a common pattern to wait until the device is idle and connected to a power supply.

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 might delay your task depending on the state of the device, and does not guarantee that your task will run on time.

The JobScheduler API includes the ability to set a hard deadline that overrides all previous constraints.

Add the new UI for setting the deadline to run the task

In this step you use 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 seek bar left or right.

Implement the following steps in your activity_main.xml file:

  1. Below the LinearLayout that has the Switch views, create a horizontal LinearLayout. The new LinearLayout is for the SeekBar labels.

Attribute

Value

android:layout_width

"match_parent"

android:layout_height

"wrap_content"

android:orientation

"horizontal"

android:layout_margin

"4dp"

  1. Give the seek bar two labels: a static label like the label for the group of radio buttons, and a dynamic label that's updated with the value from the seek bar. Add two TextView views 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"

  1. Add a SeekBar view below the LinearLayout. Use 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

Implement the following steps in MainActivity.java. Don't forget to extract your string resources.

  1. Create a member variable for the SeekBar and initialize it in onCreate().
//Override deadline seekbar
private SeekBar mSeekBar;

onCreate():

mSeekBar = findViewById(R.id.seekBar);
  1. In onCreate(), create and initialize a final variable for the seek bar's progress TextView. (The variable will be accessed from an inner class.)
final TextView seekBarProgress = findViewById(R.id.seekBarProgress);
  1. In onCreate(), call setOnSeekBarChangeListener() on the seek bar, 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) {}
});
  1. The second argument of onProgressChanged() is the current value of the seek bar. In the onProgressChanged() callback, check whether the integer value is greater than 0 (meaning a value has been set by the user). If the value is greater than 0, set the seek bar's progress label to the integer value, followed by s to indicate seconds. Otherwise, set the TextView to read "Not Set".
if (i > 0){
   seekBarProgress.setText(i + " s");
}else {
   seekBarProgress.setText("Not Set");
}
  1. The override deadline should only be set if the integer value of the SeekBar is greater than 0. In the scheduleJob() method, create an int to store the seek bar's progress. Also create a boolean variable that's true if the seek bar has an integer value greater than 0.
int seekBarInteger = mSeekBar.getProgress();
boolean seekBarSet = seekBarInteger > 0;
  1. In the scheduleJob() method after the builder definition, if seekBarSet is true, call setOverrideDeadline() on the JobInfo.Builder. Pass in the seek bar's integer value multiplied by 1000. (The parameter is in milliseconds, and you want the user to set the deadline in seconds.)
if (seekBarSet) {
      builder.setOverrideDeadline(seekBarInteger * 1000);
}
  1. Modify the constraintSet to include the value of seekBarSet as a possible constraint:
boolean constraintSet = selectedNetworkOption != JobInfo.NETWORK_TYPE_NONE
       || mDeviceChargingSwitch.isChecked() || mDeviceIdleSwitch.isChecked()
       || seekBarSet;
  1. Run the app. The user can now set a hard deadline, in seconds, by which time the JobService must run!

Android Studio project: NotificationScheduler

Challenge: Up until now, your JobService tasks have simply delivered a notification, but JobScheduler is usually used for more robust background tasks, such as updating the weather or syncing with a database. Because background tasks can be more complex, programmatically and functionally, the job of notifying the framework when the task is complete falls on the developer. Fortunately, the developer can do this by calling jobFinished().

This challenge requires you to call jobFinished() after the task is complete:

The related concept documentation is in 8.3: Efficient data transfer.

Android developer documentation:

This section lists possible homework assignments for students who are working through this codelab as part of a course led by an instructor. It's up to the instructor to do the following:

Instructors can use these suggestions as little or as much as they want, and should feel free to assign any other homework they feel is appropriate.

If you're working through this codelab on your own, feel free to use these homework assignments to test your knowledge.

Build and run an app

Create an app that simulates a large download scheduled with battery and data consumption in mind. The app contains a Download Now button and has the following features:

Hint :Define the JobService class as an inner class. That way, the Download Now button and the JobService can call the same method to deliver the notification.

Answer these questions

Question 1

What class do you use if you want features like the ones provided by JobScheduler, but you want the features to work for devices running API level 20 and lower?

Submit your app for grading

Guidance for graders

Check that the app has the following features:

To find the next practical codelab in the Android Developer Fundamentals (V2) course, see Codelabs for Android Developer Fundamentals (V2).

For an overview of the course, including links to the concept chapters, apps, and slides, see Android Developer Fundamentals (Version 2).