Delayed Execution with AlarmManager

To actually use your service in the background, you will need some way to make things happen when none of your activities are running. Say, by making a timer that goes off every five minutes or so.

You could do this with a Handler by calling Handler.sendMessageDelayed(…) or Handler.postDelayed(…). But this solution will probably fail if the user navigates away from all your activities. The process will shut down, and your Handler messages will go kaput with it.

So instead of Handler, you will use AlarmManager, a system service that can send Intents for you.

How do you tell AlarmManager what intents to send? You use a PendingIntent. You can use PendingIntent to package up a wish: I want to start PollService. You can then send that wish to other components on the system, like AlarmManager.

Write a new method called setServiceAlarm(Context, boolean) inside PollService that turns an alarm on and off for you. You will write it as a static method. That keeps your alarm code with the other code in PollService that it is related to while allowing other components to invoke it. You will usually want to turn it on and off from front-end code in a fragment or other controller.

Listing 28.8  Adding alarm method (PollService.java)

public class PollService extends IntentService {
    private static final String TAG = "PollService";

    // Set interval to 1 minute
    private static final long POLL_INTERVAL_MS = TimeUnit.MINUTES.toMillis(1);

    public static Intent newIntent(Context context) {
        return new Intent(context, PollService.class);
    }

    public static void setServiceAlarm(Context context, boolean isOn) {
        Intent i = PollService.newIntent(context);
        PendingIntent pi = PendingIntent.getService(context, 0, i, 0);

        AlarmManager alarmManager = (AlarmManager)
                context.getSystemService(Context.ALARM_SERVICE);

        if (isOn) {
            alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME,
                    SystemClock.elapsedRealtime(), POLL_INTERVAL_MS, pi);
        } else {
            alarmManager.cancel(pi);
            pi.cancel();
        }
    }
    ...
}

The first thing you do in your method is construct your PendingIntent that starts PollService. You do this by calling PendingIntent.getService(…), which packages up an invocation of Context.startService(Intent). It takes in four parameters: a Context with which to send the intent, a request code that you can use to distinguish this PendingIntent from others, the Intent object to send, and finally a set of flags that you can use to tweak how the PendingIntent is created. (You will use one of these in a moment.)

After that, you need to either set the alarm or cancel it.

To set the alarm, you call AlarmManager.setRepeating(…). This method also takes four parameters: a constant to describe the time basis for the alarm (more on that in a moment), the time at which to start the alarm, the time interval at which to repeat the alarm, and finally a PendingIntent to fire when the alarm goes off.

Because you used AlarmManager.ELAPSED_REALTIME as the time basis value, you specified the start time in terms of elapsed realtime: SystemClock.elapsedRealtime(). This triggers the alarm to go off when the specified amount of time has passed. If you had used AlarmManager.RTC, you would instead base the start time on “clock time” (e.g., System.currentTimeMillis()). This would trigger the alarm to go off at a fixed point in time.

Canceling the alarm is done by calling AlarmManager.cancel(PendingIntent). You will also usually want to cancel the PendingIntent. In a moment, you will see how canceling the PendingIntent also helps you track the status of the alarm.

Add some quick test code to run your alarm from within PhotoGalleryFragment.

Listing 28.9  Adding alarm startup code (PhotoGalleryFragment.java)

public class PhotoGalleryFragment extends Fragment {
    private static final String TAG = "PhotoGalleryFragment";
    ...
    @Override
    public void onCreate(Bundle savedInstanceState) {
        ...
        updateItems();

        Intent i = PollService.newIntent(getActivity());
        getActivity().startService(i);
        PollService.setServiceAlarm(getActivity(), true);

        Handler responseHandler = new Handler();
        mThumbnailDownloader = new ThumbnailDownloader<>(responseHandler);
        ...
    }
    ...
}

Finish typing in this code and run PhotoGallery. Then immediately hit the Back button and exit out of the app.

Notice anything in Logcat? PollService is faithfully chugging along, running again every 60 seconds. This is what AlarmManager is designed to do. Even if your process gets shut down, AlarmManager will keep on firing intents to start PollService again and again. (This behavior is, of course, extremely annoying. You may want to uninstall the app until we get it straightened out.)

Being a good citizen: using alarms the right way

How exact do you need your repeating to be? Repeatedly executing work from your background service has the potential to eat up the user’s battery power and data service allotment. Furthermore, waking the device from sleep (spinning up the CPU when the screen was off to do work on your behalf) is a costly operation. Luckily, you can configure your alarm to have a lighter usage footprint in terms of interval timing and wake requirements.

Repeating alarms: not so exact

The setRepeating(…) method sets a repeating alarm, but that repetition is not exact. In other words, Android reserves the right to move it around a little bit. As a result, 60 seconds is the lowest possible interval time you can set for stock Android. (Other devices may elect to make this value higher.)

This is because alarms can really blow a hole in your phone’s battery management. Every time an alarm fires, the device has to wake up and spin up an application. Many apps need to turn on the phone’s radio to use the internet like PhotoGallery does, which results in even more battery usage.

If it were only your app, the exactness of the alarm would not matter. After all, if your alarm wakes up every 15 minutes, and your app is the only 15-minute alarm running, then the phone will wake up and turn on its radio four times an hour no matter how precise the alarm is.

If it were your app and nine other apps with exact 15-minute alarms, though, things change. Because every alarm is exact, the device needs to wake up for each one. That means turning on the radio 40 times an hour instead of four times.

Inexactness means that Android is allowed to take the liberty of moving those alarms around, so that they do not run exactly every 15 minutes. The result is that every 15 minutes, your device can wake up and run all 10 of those 15-minute alarms at the same time. That would pull you back down to only four wake-ups instead of 40, saving all kinds of battery in the process.

Some apps really do need exact alarms, of course. If that is your app, then you must use either AlarmManager.setWindow(…) or AlarmManager.setExact(…), which allow you to set an exact alarm to occur only once. The repeating part you have to handle yourself.

Time basis options

Another important decision is which time basis value to specify. There are two main options: AlarmManager.ELAPSED_REALTIME and AlarmManager.RTC.

AlarmManager.ELAPSED_REALTIME uses the amount of time that has passed since the last boot of the device (including sleep time) as the basis for interval calculations. ELAPSED_REALTIME is the best choice for your alarm in PhotoGallery because it is based on the relative passage of time and thus does not depend on clock time. (Also, the documentation recommends you use ELAPSED_REALTIME instead of RTC if at all possible.)

AlarmManager.RTC uses clock time in terms of UTC. UTC should only be used for clock-basis alarms. However, UTC does not respect locale, whereas the user’s idea of clock time includes locale. Clock-basis alarms should respect locale somehow. This means you must implement your own locale handling in conjunction with using the RTC time basis if you want to set a clock-time alarm. Otherwise, use ELAPSED_REALTIME as the time basis.

If you use one of the time basis options outlined above, your alarm will not fire if the device is in sleep mode (the screen is turned off), even if the prescribed interval has passed. If you need your alarm to occur on a more precise interval or time, you can force the alarm to wake up the device by using one of the following time basis constants: AlarmManager.ELAPSED_REALTIME_WAKEUP and AlarmManager.RTC_WAKEUP. However, you should avoid using the wake-up options unless your alarm absolutely must occur at a specific time.

PendingIntent

Let’s talk a little bit more about PendingIntent. A PendingIntent is a token object. When you get one here by calling PendingIntent.getService(…), you say to the OS, Please remember that I want to send this intent with startService(Intent). Later on you can call send() on your PendingIntent token, and the OS will send the intent you originally wrapped up in exactly the way you asked.

The really nice thing about this is that when you give that PendingIntent token to someone else and they use it, it sends that token as your application. Also, because the PendingIntent itself lives in the OS, not in the token, you maintain control of it. If you wanted to be cruel, you could give someone else a PendingIntent object and then immediately cancel it, so that send() does nothing.

If you request a PendingIntent twice with the same intent, you will get the same PendingIntent. You can use this to test whether a PendingIntent already exists or to cancel a previously issued PendingIntent.

Managing alarms with PendingIntent

You can only register one alarm for each PendingIntent. That is how setServiceAlarm(Context, boolean) works when isOn is false: It calls AlarmManager.cancel(PendingIntent) to cancel the alarm for your PendingIntent and then cancels your PendingIntent.

Because the PendingIntent is also cleaned up when the alarm is canceled, you can check whether that PendingIntent exists to see whether the alarm is active. This is done by passing in the PendingIntent.FLAG_NO_CREATE flag to PendingIntent.getService(…). This flag says that if the PendingIntent does not already exist, return null instead of creating it.

Write a new method called isServiceAlarmOn(Context) that uses PendingIntent.FLAG_NO_CREATE to tell whether the alarm is on.

Listing 28.10  Adding isServiceAlarmOn() method (PollService.java)

public class PollService extends IntentService {
    ...
    public static void setServiceAlarm(Context context, boolean isOn) {
        ...
    }

    public static boolean isServiceAlarmOn(Context context) {
        Intent i = PollService.newIntent(context);
        PendingIntent pi = PendingIntent
                .getService(context, 0, i, PendingIntent.FLAG_NO_CREATE);
        return pi != null;
    }
    ...
}

Because this PendingIntent is only used for setting your alarm, a null PendingIntent here means that your alarm is not set.

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

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