1. Welcome
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:
- For the complete list of codelabs in the course, see Codelabs for Android Developer Fundamentals (V2).
- For details about the course, including links to all the concept chapters, apps, and slides, see Android Developer Fundamentals (Version 2).
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:
- Create an app that delivers a notification.
- Get an integer value from a
Spinner
view. - Use
Switch
views for user input. - Create
PendingIntents
.
What you'll learn
- How to implement a
JobService
. - How to construct a
JobInfo
object with specific constraints. - How to schedule a
JobService
based on theJobInfo
object.
What you'll do
- Implement a
JobService
that delivers a simple notification to let the user know the job is running. - Get user input to configure constraints (such as waiting until the device is charging) on the
JobService
. - Schedule the job using
JobScheduler
.
2. App overview
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
:
- A
JobInfo
object contains the set of conditions that trigger a job to run. - A
JobService
is the implementation of the job that runs under the conditions set in theJobInfo
object.
3. Task 1: Implement a JobService
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:
- Called when the system determines that your task should be run. In this method, you implement the job to be done.
- Returns a
boolean
indicating whether the job needs to continue on a separate thread. Iftrue
, the work is offloaded to a different thread, and your app must calljobFinished()
explicitly in that thread to indicate that the job is complete. Iffalse
, the system knows that the job is completed by the end ofonStartJob()
, and the system callsjobFinished()
on your behalf.
About the onStopJob()
callback:
- If the conditions described in the
JobInfo
are no longer met, the job must be stopped, and the system callsonStopJob()
. - The
onStopJob()
callback returns aboolean
that determines what to do if the job is not finished. If the return value istrue
, the job is rescheduled; otherwise, the job is dropped.
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.
- Create a new Java project called "Notification Scheduler". Use API 21 as the target SDK, and use the Empty Activity template.
- Inside the
com.android.example.notificationscheduler
package, create a new Java class that extendsJobService
. Call the new classNotificationJobService
. - Add the required methods, which are
onStartJob()
andonStopJob()
. Click the red light bulb next to the class declaration and select Implement methods, then select OK. - In your
AndroidManfest.xml
file, inside the<application>
tag, register yourJobService
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
:
- Add an image asset to use as a notification icon
for the "Job" notification. Name the image
ic_job_running
. - 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";
- Inside the
NotificationJobService
class, 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 the 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);
}
}
- Inside
onStartJob()
, call the method to create the notification channel. Create aPendingIntent
that launches your app'sMainActivity
. 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);
- In
onStartJob()
, construct and deliver a notification with the following attributes:
Attribute | Title |
Content Title |
|
Content Text |
|
Content Intent |
|
Small Icon |
|
Priority |
|
Defaults |
|
AutoCancel |
|
- Extract your strings.
- Make sure that
onStartJob()
returnsfalse
, because for this app, all of the work is completed in theonStartJob()
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;
}
- Make sure that
onStopJob()
returnstrue
, because if the job fails, you want the job to be rescheduled instead of dropped.
4. Task 2: Implement the job conditions (JobInfo)
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:
NETWORK_TYPE_NONE
means that the job will run with or without a network connection. This is the default value.NETWORK_TYPE_ANY
means that the job will run as long as a network (cellular, Wi-Fi) is available.NETWORK_TYPE_UNMETERED
means that the job will run as long as the device is connected to Wi-Fi that does not use a HotSpot.
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.
- Change the root view element to a vertical
LinearLayout
and give the layout a padding of16dp
. 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>
- Change the
TextView
to have the following attributes:
Attribute | Value |
|
|
|
|
|
|
|
|
|
|
- Below the
TextView
, add aRadioGroup
container element with the following attributes:
Attribute | Value |
|
|
|
|
|
|
|
|
|
|
- Add three
RadioButton
views as child elements inside theRadioGroup
. For each of the radio buttons, set the layout height and width to"wrap_content"
, and set the following attributes:
RadioButton 1 | ||
|
| |
|
| |
|
| |
RadioButton 2 | ||
|
| |
|
| |
RadioButton 3 | ||
|
| |
|
|
- Add two
Button
views below theRadioGroup
. For each of the buttons, set the height and width to "wrap content"
, and set the following attributes:
Button 1 | ||
|
| |
|
| |
|
| |
|
| |
Button 2 | ||
|
| |
|
| |
|
| |
|
|
- In
MainActivity
, add a method stub for anonClick()
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.
- In the
scheduleJob()
method, find theRadioGroup
by ID and save it in an instance variable callednetworkOptions
.
RadioGroup networkOptions = findViewById(R.id.networkOptions);
- In the
scheduleJob()
method, get the selected network ID and save it in an integer variable.
int selectedNetworkID = networkOptions.getCheckedRadioButtonId();
- In the
scheduleJob()
method, create an integer variable for the selected network option. Set the variable to the default network option, which isNETWORK_TYPE_NONE
.
int selectedNetworkOption = JobInfo.NETWORK_TYPE_NONE;
- In the
scheduleJob()
method, create a switch statement with the selected network ID. Add a case for each of the possible IDs. - In the
scheduleJob()
method, assign the selected network option the appropriateJobInfo
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
- Create a member variable for the
JobScheduler
.
private JobScheduler mScheduler;
- Inside the
scheduleJob()
method, usegetSystemService()
to initializemScheduler
.
mScheduler = (JobScheduler) getSystemService(JOB_SCHEDULER_SERVICE);
- Create a member constant for the
JOB_ID
, and set it to0
.
private static final int JOB_ID = 0;
- Inside the
scheduleJob()
method, after theSwitch
block, create aJobInfo.Builder
object. The first parameter is theJOB_ID
. The second parameter is theComponentName
for theJobService
you created. TheComponentName
is used to associate theJobService
with theJobInfo
object.
ComponentName serviceName = new ComponentName(getPackageName(),
NotificationJobService.class.getName());
JobInfo.Builder builder = new JobInfo.Builder(JOB_ID, serviceName);
- Call
setRequiredNetworkType()
on theJobInfo.Builder
object. Pass in the selected network option.
.setRequiredNetworkType(selectedNetworkOption);
- Call
schedule()
on theJobScheduler
object. Use thebuild()
method to pass in theJobInfo
object.
JobInfo myJobInfo = builder.build();
mScheduler.schedule(myJobInfo);
- 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();
- In the
cancelJobs()
method, check whether theJobScheduler
object isnull
. If not, callcancelAll()
on the object to remove all pending jobs. Also reset theJobScheduler
tonull
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();
}
- 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()
:
- After the
JobInfo.Builder
definition, above themyJobInfo
definition, create aboolean
variable calledconstraintSet
. The variable istrue
if the selected network option is not the default. (The default isJobInfo.NETWORK_TYPE_NONE
.)
boolean constraintSet = selectedNetworkOption != JobInfo.NETWORK_TYPE_NONE;
- After the
constraintSet
definition, create anif/else
block using theconstraintSet
variable. - Move the code that schedules the job and shows the toast message into the
if
block. - If
constraintSet
isfalse
, 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:
- Copy the
TextView
that you used for the network-type label and paste it below theRadioGroup
. - Change the
android:text
attribute to "Requires:
". - Below this, add a horizontal
LinearLayout
with a4dp
margin.
Attribute | Value |
|
|
|
|
|
|
|
|
- Create two
Switch
views as children to the horizontalLinearLayout
. Set the height and width to "wrap_content"
, and use the following attributes:
Switch 1 | ||
|
| |
|
| |
Switch 2 | ||
|
| |
|
|
Add the code for the new constraints
Implement the following steps in MainActivity.java
:
- Create member variables called
mDeviceIdle
andmDeviceCharging
, for the switches. Initialize the variables inonCreate()
.
// Switches for setting job options.
private Switch mDeviceIdleSwitch;
private Switch mDeviceChargingSwitch;
onCreate():
mDeviceIdleSwitch = findViewById(R.id.idleSwitch);
mDeviceChargingSwitch = findViewById(R.id.chargingSwitch);
- In the
scheduleJob()
method, add the following calls. The calls set constraints on theJobInfo.Builder
based on the user selection in theSwitch
views, during the creation of thebuilder
object.
.setRequiresDeviceIdle(mDeviceIdleSwitch.isChecked())
.setRequiresCharging(mDeviceChargingSwitch.isChecked());
- Update the code that sets
constraintSet
to consider the new constraints:
boolean constraintSet = (selectedNetworkOption != JobInfo.NETWORK_TYPE_NONE)
|| mDeviceChargingSwitch.isChecked() || mDeviceIdleSwitch.isChecked();
- 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:
- Open the More menu (the ellipses icon next to the emulated device).
- Go to the Battery pane.
- 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:
- Below the
LinearLayout
that has theSwitch
views, create a horizontalLinearLayout
. The newLinearLayout
is for theSeekBar
labels.
Attribute | Value |
|
|
|
|
|
|
|
|
- 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 theLinearLayout
with the following attributes:
TextView 1 | ||
|
| |
|
| |
|
| |
|
| |
|
| |
TextView 2 | ||
|
| |
|
| |
|
| |
|
| |
|
|
- Add a
SeekBar
view below theLinearLayout
. Use the following attributes:
Attribute | Value |
|
|
|
|
|
|
|
|
Write the code for adding the deadline
Implement the following steps in MainActivity.java
. Don't forget to extract your string resources.
- Create a member variable for the
SeekBar
and initialize it inonCreate()
.
// Override deadline seekbar.
private SeekBar mSeekBar;
onCreate():
mSeekBar = findViewById(R.id.seekBar);
- In
onCreate()
, create and initialize a final variable for the seek bar's progressTextView
. (The variable will be accessed from an inner class.)
final TextView seekBarProgress = findViewById(R.id.seekBarProgress);
- In
onCreate()
, callsetOnSeekBarChangeListener()
on the seek bar, passing in a newOnSeekBarChangeListener
. (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) {}
});
- The second argument of
onProgressChanged()
is the current value of the seek bar. In theonProgressChanged()
callback, check whether the integer value is greater than0
(meaning a value has been set by the user). If the value is greater than0
, set the seek bar's progress label to the integer value, followed bys
to indicate seconds. Otherwise, set theTextView
to read"Not Set"
.
if (i > 0) {
seekBarProgress.setText(i + " s");
} else {
seekBarProgress.setText("Not Set");
}
- The override deadline should only be set if the integer value of the
SeekBar
is greater than0
. In thescheduleJob()
method, create anint
to store the seek bar's progress. Also create aboolean
variable that'strue
if the seek bar has an integer value greater than0
.
int seekBarInteger = mSeekBar.getProgress();
boolean seekBarSet = seekBarInteger > 0;
- In the
scheduleJob()
method after thebuilder
definition, ifseekBarSet
istrue
, callsetOverrideDeadline()
on theJobInfo.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);
}
- Modify the
constraintSet
to include the value ofseekBarSet
as a possible constraint:
boolean constraintSet = selectedNetworkOption != JobInfo.NETWORK_TYPE_NONE
|| mDeviceChargingSwitch.isChecked() || mDeviceIdleSwitch.isChecked()
|| seekBarSet;
- Run the app. The user can now set a hard deadline, in seconds, by which time the
JobService
must run!
5. Solution code
Android Studio project: NotificationScheduler
6. Coding challenge
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:
- Implement a
JobService
that starts anExecutor
task when the given constraints are met. - The
Executor
task should sleep for 5 seconds. - If the constraints stop being met while the thread is sleeping, reschedule the job and show a
Toast
message saying that the job failed.
7. Summary
JobScheduler
provides a flexible framework to intelligently accomplish background services.JobScheduler
is only available on devices running API 21 and higher.- To use the
JobScheduler
, you need two parts:JobService
andJobInfo
. JobInfo
is a set of conditions that trigger the job to run.JobService
implements the job to run under the conditions specified byJobInfo
.- You only have to implement the
onStartJob()
andonStopJob()
callback methods, which you do in yourJobService
. - The implementation of your job occurs, or is started, in
onStartJob()
. - The
onStartJob()
method returns aboolean
value that indicates whether the service needs to process the work in a separate thread. - If
onStartJob()
returnstrue
, you must explicitly calljobFinished()
. IfonStartJob()
returnsfalse
, the runtime callsjobFinished()
on your behalf. JobService
is processed on the main thread, so you should avoid lengthy calculations or I/O.JobScheduler
is the manager class responsible for scheduling the task.JobScheduler
batches tasks to maximize the efficiency of system resources, which means that you do not have exact control of when tasks are executed.
8. Related concept
The related concept documentation is in 8.3: Efficient data transfer.
9. Learn more
Android developer documentation:
10. Homework
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:
- Assign homework if required.
- Communicate to students how to submit homework assignments.
- Grade the homework assignments.
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:
- Instead of performing an actual download, the app delivers a notification.
- When the user taps the Download Now button, it triggers a "downloading" notification.
- The "download" is performed once a day, when the phone is idle but connected to power and to Wi-Fi, or when the user taps the button.
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?
JobSchedulerCompat
workManager
AlarmManager
Submit your app for grading
Guidance for graders
Check that the app has the following features:
- The
JobInfo
object has 4 criteria set:setRequiresCharging()
,setPeriodic()
,setRequiresDeviceIdle()
, andsetRequiredNetworkType()
.
11. Next codelab
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).