Chapter 7. Scheduling Work with AlarmManager

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:

  • Scheduling alarms with AlarmManager
  • Canceling alarms
  • Scheduling repeating alarms
  • Handling alarms
  • Staying awake with WakeLocks
  • Applications of AlarmManager

Scheduling alarms with AlarmManager

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()

Note

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

Canceling alarms

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.

Scheduling repeating alarms

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

Tip

From API level 19, all repeating alarms are inexact—that is, if our application targets KitKat or above, our repeat alarms will be inexact even if we use setRepeating.

If we really need exact repeat alarms, we can use setExact instead, and schedule the next alarm while handling the current one.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.49.252