Maintaining the responsiveness of foreground apps has been our primary focus throughout this book, and we've explored numerous ways to shift work away from the main thread to run in the background.
In all of our discussions so far, we wanted to get the work done as soon as possible, so although we moved it off to a background thread, we still performed the work concurrently with ongoing main thread operations, such as updating the user interface and responding to user interaction.
In this chapter we'll learn to schedule work to run at some distant time in the future, launching our application without user intervention if it isn't already running, and even waking the device from sleep if necessary.
In this chapter we will cover:
In Chapter 3, Distributing Work with Handler and HandlerThread, we learned to schedule work on a HandlerThread
using postDelayed
, postAtTime
, sendMessageDelayed
, and sendMessageAtTime
. These mechanisms are fine for short-term scheduling of work that should happen soon—while our application is running in the foreground.
However, if we want to schedule an operation to run at some point in the distant future, we'll run into problems. First, our application may be terminated before that time arrives, removing any chance of the Handler
running those scheduled operations. Second, the device may be asleep, and with its CPU powered down, it cannot run our scheduled tasks.
The solution to this is to use an alternative scheduling approach—one that is designed to overcome these problems: AlarmManager
.
AlarmManager
is a system service that provides scheduling capabilities far beyond those of Handler
. Being a system service, AlarmManager
cannot be terminated and has the capacity to wake the device from sleep to deliver scheduled alarms.
We can access AlarmManager
via a Context
instance, so from any lifecycle callback in an Activity
, we can get the AlarmManager
by using the following code:
AlarmManager am = (AlarmManager)getSystemService(ALARM_SERVICE);
Once we have a reference to the AlarmManager
, we can schedule an alarm to deliver a PendingIntent
object at a time of our choosing. The simplest way to do that is using the set
method.
void set(int type, long triggerAtMillis, PendingIntent operation)
When we set an alarm we must also specify a type
flag—the first parameter to the set
method. The type
flag sets the conditions under which the alarm should fire and which clock
to use for our schedule.
There are two conditions and two clocks, resulting in four possible type
settings.
The conditions specify whether or not the device will be woken up if it is sleeping at the time of the scheduled alarm—whether the alarm is a "wakeup" alarm or not.
The clocks provide a reference time against which we set our schedules, defining exactly what we mean when we set a value to triggerAtMillis
.
The elapsed-time system clock—android.os.SystemClock
—measures time as the number of milliseconds that have passed since the device booted, including any time spent in deep sleep. The current time according to the system clock can be found using:
SystemClock.elapsedRealtime()
The real-time clock measures time in milliseconds since the Unix epoch. The current time according to the real-time clock can be found with:
System.currentTimeMillis()
In Java, System.currentTimeMillis()
returns the number of milliseconds since midnight on January 1, 1970, Coordinated Universal Time (UTC)—a point in time known as the Unix epoch.
UTC is the internationally recognized successor to Greenwich Mean Time (GMT) and forms the basis for expressing international time zones, which are typically defined as positive or negative offsets from UTC.
Given these two conditions and two clocks, the four possible type
values we can use when setting alarms are:
AlarmManager.ELAPSED_REALTIME
: This schedules the alarm relative to the system clock. If the device is asleep at the scheduled time it will not be delivered immediately, instead the alarm will be delivered the next time the device wakes.AlarmManager.ELAPSED_REALTIME_WAKEUP
: This schedules the alarm relative to the system clock. If the device is asleep, it will be woken to deliver the alarm at the scheduled time.AlarmManager.RTC
: This schedules the alarm in UTC relative to the Unix epoch. If the device is asleep at the scheduled time, the alarm will be delivered when the device is next woken.AlarmManager.RTC_WAKEUP
: This schedules the alarm relative to the Unix epoch. If the device is asleep it will be awoken, and the alarm is delivered at the scheduled time.Let's consider a few examples. We'll use the TimeUnit
class from the java.lang.concurrent
package to calculate times in milliseconds. To set an alarm to go off 48 hours after the initial boot, we need to work with the system clock, as shown in the following code:
long delay = TimeUnit.HOURS.toMillis(48L); long time = System.currentTimeMillis() + delay; am.set(AlarmManager.ELAPSED_REALTIME, time, pending);
We can set an alarm to go off in 2 hours from now using a clock, by adding two hours to the current time. Using the system clock it looks like this:
long delay = TimeUnit.HOURS.toMillis(2L); long time = SystemClock.elapsedRealtime() + delay; am.set(AlarmManager.ELAPSED_REALTIME, time, pending);
To set an alarm for 2 hours from now using the real-time clock is similar to the previous code:
long delay = TimeUnit.HOURS.toMillis(2L); long time = System.currentTimeMillis() + delay; am.set(AlarmManager.RTC, time, pending);
To set an alarm for 9 p.m. today (or tomorrow, if it's already past 9 p.m. today):
Calendar calendar = Calendar.getInstance(); if (calendar.get(Calendar.HOUR_OF_DAY) >= 21) { calendar.add(Calendar.DATE, 1); } calendar.set(Calendar.HOUR_OF_DAY, 21); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); am.set(AlarmManager.RTC, calendar.getTimeInMillis(), pending);
None of the examples so far will wake the device if it is sleeping at the time of the alarm. To do that we need to use one of the WAKEUP
alarm conditions, for example:
am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, time, pending); am.set(AlarmManager.RTC_WAKEUP, time, pending);
If our application targets an API level below 19 (KitKat), alarms scheduled with set
will run at exactly the alarm time. For applications targeting KitKat or greater, the schedule is considered inexact and the system may re-order or group alarms to minimize wake-ups and save battery.
If we need precision scheduling and are targeting KitKat or greater, we can use the new setExact
method introduced at API level 19, but we'll need to check that the method exists before we try to call it; otherwise, our app will crash when run under earlier API levels:
if (Build.VERSION.SDK_INT >= 19) { am.setExact(AlarmManager.RTC_WAKEUP, time pending); } else { am.set(AlarmManager.RTC_WAKEUP, time, pending); }
This will deliver our alarm at exactly the specified time on all platforms. We should only use exact scheduling when we really need it—for example, to deliver alerts to the user at a specific time. For most other cases, allowing the system to adjust our schedule a little to preserve battery life is usually acceptable.
One more addition in KitKat is setWindow
, which introduces a compromise between exact and inexact alarms by allowing us to specify the time window within which the alarm must be delivered. This still allows the system some freedom to play with the schedules for efficiency, but lets us choose just how much freedom to allow.
Here's how we would use setWindow
to schedule an alarm to be delivered within a 3 minute window—at the earliest 10 minutes from now and at the latest 13 minutes from now—using the real-time clock:
long delay = TimeUnit.MINUTES.toMillis(10); long window = TimeUnit.MINUTES.toMillis(3); long earliest = System.currentTimeMillis() + delay; am.setWindow( AlarmManager.RTC_WAKEUP, earliest, window, pending);
Once set, an alarm can be canceled very easily—we just need to invoke the AlarmManager's cancel
method with an Intent
matching that of the alarm we want to cancel.
The process of matching uses the filterEquals
method of Intent
, which compares the action, data, type, class, and categories of both Intents to test for equivalence. Any extras we may have set in the Intent
are not taken into account. We can set and cancel an alarm using different Intent
instances like this:
public void setThenCancel() { AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE); long at = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(5L); am.set(AlarmManager.RTC, at, createPendingIntent()); am.cancel(createPendingIntent()); } private PendingIntent createPendingIntent() { Intent intent = new Intent("my_action"); // extras don't affect matching intent.putExtra("random", Math.random()); return PendingIntent.getBroadcast( this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); }
It is important to realize that whenever we set an alarm, we implicitly cancel any existing alarm with a matching Intent
, replacing it with the new schedule.
As well as setting a one-off alarm, we have the option to schedule repeating alarms using setRepeating
and setInexactRepeating
. Both methods take an additional parameter that defines the interval in milliseconds at which to repeat the alarm. Generally it is advisable to avoid setRepeating
and always use setInexactRepeating
, allowing the system to optimize wake-ups and giving more consistent behavior on devices running different Android versions:
void setRepeating( int type, long triggerAtMillis, long intervalMillis, PendingIntent operation); void setInexactRepeating( int type, long triggerAtMillis, long intervalMillis, PendingIntent operation)
AlarmManager
provides some handy constants for typical repeat intervals:
AlarmManager.INTERVAL_FIFTEEN_MINUTES AlarmManager.INTERVAL_HALF_HOUR AlarmManager.INTERVAL_HOUR AlarmManager.INTERVAL_HALF_DAY AlarmManager.INTERVAL_DAY
We can schedule a repeating alarm to be delivered approximately 2 hours from now, then repeating every 15 minutes or so thereafter like this:
Intent intent = new Intent("my_action"); PendingIntent broadcast = PendingIntent.getBroadcast( this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); long start = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(2L); AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE); am.setRepeating( AlarmManager.RTC_WAKEUP, start, AlarmManager.INTERVAL_FIFTEEN_MINUTES, broadcast);
18.226.172.130