Handling alarms

Now that we know how to schedule alarms, let's take a look at what we can schedule with them.

Essentially, we can schedule anything that can be started with a PendingIntent, which means we can use alarms to start Activities, Services, and BroadcastReceivers. To specify the target of our alarm, we need to use the static factory methods of PendingIntent:

PendingIntent.getActivities(…)
PendingIntent.getActivity(…)
PendingIntent.getService(…)
PendingIntent.getBroadcast(…)

In the following sections, we'll see how each type of PendingIntent can be used with AlarmManager.

Handling alarms with Activities

Starting an Activity from an alarm is as simple as registering the alarm with a PendingIntent created by invoking the static getActivity method of PendingIntent.

When the alarm is delivered, the Activity will be started and brought to the foreground, displacing any app that was currently in use. Keep in mind that this is likely to surprise and perhaps annoy users!

When starting Activities with alarms, we will probably want to set Intent.FLAG_ACTIVITY_CLEAR_TOP; so that if the application is already running, our target Activity assumes its normal place in the navigation order within the app.

Intent intent = new Intent(context, HomeActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pending = PendingIntent.getActivity(
    Context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

Not all Activities are suited to being started with getActivity. We might need to start an Activity that normally appears deep within the app, where pressing "back" does not exit to the home screen, but returns to the next Activity on the back-stack.

This is where getActivities comes in. With getActivities, we can push more than one Activity onto the back-stack of the application, allowing us to populate the back-stack to create the desired navigation flow when the user presses "back". To do this, we create our PendingIntent by sending an array of Intents to getActivities:

Intent home = new Intent(context, HomeActivity.class);
Intent target = new Intent(context, TargetActivity.class);
home.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pending = PendingIntent.getActivities(
    context, 0, new Intent[]{ home, target },    
    PendingIntent.FLAG_UPDATE_CURRENT);

The array of Intents specifies the Activities to launch, in order. The logical sequence of events when this alarm is delivered is:

  1. If the application is already running, any Activities on the back-stack above HomeActivity are finished and removed, because we set the Intent.FLAG_ACTIVITY_CLEAR_TOP flag.
  2. HomeActivity is (re)started.
  3. TargetActivity is started and placed on the back-stack above HomeActivity. The TargetActivity becomes the foreground Activity

Handling alarms with Activities is good to know about, but is not a technique we will use often, since it is so intrusive. We are much more likely to want to handle alarms in the background, which we'll look at next.

Handling alarms with BroadcastReceiver

We met BroadcastReceiver already in Chapter 6, Long-running Tasks with Service, where we used it in an Activity to receive broadcasts from an IntentService. In this section, we'll use BroadcastReceiver to handle alarms.

BroadcastReceivers can be registered and unregistered dynamically at runtime—like we did in Chapter 6, Long-running Tasks with Service, or statically in the Android manifest file with a <receiver> element, and can receive alarms regardless of how they are registered.

It is more common to use a statically registered receiver for alarms, because these are known to the system and can be invoked by alarms to start an application if it is not currently running. A receiver registration in the manifest looks like this:

<receiver android:name=".AlarmReceiver">
    <intent-filter>
        <action android:name="reminder"/>
    </intent-filter>
</receiver>

The <intent-filter> element gives us the opportunity to say which Intents we want to receive, by specifying the action, data, and categories that should match. Here we're just matching Intents with the action reminder, so we can set an alarm for this receiver with:

Intent intent = new Intent("reminder");
intent.putExtra(AlarmReceiver.MSG, "try the alarm examples!");
PendingIntent broadcast = PendingIntent.getBroadcast(
    context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE);
long at = calculateNextMidnightRTC();
am.set(AlarmManager.RTC_WAKEUP, at, broadcast);

When this alarm comes due, AlarmManager will wake the device—if it isn't already awake—and deliver the Intent to the BroadcastReceiver's onReceive method.

public class AlarmReceiver extends BroadcastReceiver {
    public static final String MSG = "message";
    @Override
    public void onReceive(Context ctx, Intent intent) {
        // do some work while the device is awake  
    }
}

The AlarmManager guarantees that the device will remain awake at least until onReceive completes, which means we can be sure of getting some work done before the device will be allowed to return to sleep.

Doing work with BroadcastReceiver

When the system delivers an alarm to our BroadcastReceiver it does so on the main thread, so the usual main thread limitations apply: we cannot perform networking and we should not perform heavy processing or use blocking operations.

In addition, a statically registered BroadcastReceiver has a very limited lifecycle. It cannot create user-interface elements other than toasts or notifications posted via NotificationManager; the onReceive method must complete within 10 seconds or its process may be killed; and once onReceive completes, the receiver's life is over.

If the work that we need to do in response to the alarm is not intensive, we can simply complete it during onReceive. A good use for BroadcastReceivers that receive alarms is posting notifications to the notification drawer as follows:

public class AlarmReceiver extends BroadcastReceiver {
    public static final String MSG = "message";
    @Override
    public void onReceive(Context ctx, Intent intent) {
        NotificationCompat.Builder builder =
            new NotificationCompat.Builder(context)
                .setSmallIcon(android.R.drawable.stat_notify_chat)
                .setContentTitle("Reminder!")
                .setContentText(intent.getStringExtra(MSG));
        NotificationManager nm = (NotificationManager)
            context.getSystemService(
                Context.NOTIFICATION_SERVICE);
        nm.notify(intent.hashCode(), builder.build());    
    }
}

We can make this more useful by including a PendingIntent object in the notification, so that the user can tap the notification to open an Activity in our app:

@Override
    public void onReceive(Context ctx, Intent intent) {
      Intent activity = new Intent(
            ctx, HomeActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        PendingIntent pending = PendingIntent.getActivity(
            ctx, 0, activity, 
            PendingIntent.FLAG_UPDATE_CURRENT);
        NotificationCompat.Builder builder =
            new NotificationCompat.Builder(context)
                .setSmallIcon(android.R.drawable.stat_notify_chat)
                .setContentTitle("Reminder!")
                .setContentText(intent.getStringExtra(MSG))
                .setContentIntent(pending)
                .setAutoCancel(true);
        NotificationManager nm = (NotificationManager)
            context.getSystemService(
                Context.NOTIFICATION_SERVICE);
        nm.notify(intent.hashCode(), builder.build());    
    }

Although we can spend up to 10 seconds doing work in our BroadcastReceiver, we really shouldn't—if the app is in use when the alarm is triggered the user will suffer noticeable lag if onReceive takes more than a hundred milliseconds to complete. Exceeding the 10 second budget will cause the system to kill the application and report a background ANR.

A second option is available to us only if we're targeting API level 11 and above, and allows onReceive to delegate work to a background thread for up to 10 seconds—we'll discuss this in the next section.

Doing background work with goAsync

If our application targets a minimum API level of 11, we can use a feature of BroadcastReceiver that it introduced: goAsync.

public final PendingResult goAsync()

With goAsync we can extend the lifetime of a BroadcastReceiver instance beyond the completion of its onReceive method, provided the whole operation still completes within the 10 second budget.

If we invoke goAsync, the system will not consider the BroadcastReceiver to have finished when onReceive completes. Instead, the BroadcastReceiver lives on until we call finish on the PendingResult returned to us by goAsync. We must ensure that finish is called within the 10 second budget, otherwise the system will kill the process with a background ANR.

Using goAsync we can offload work to background threads using any appropriate concurrency construct—for example, an AsyncTask—and the device is guaranteed to remain awake until we call finish on the PendingResult.

public void onReceive(final Context context, final Intent intent) {
    final PendingResult result = goAsync();
    new AsyncTask<Void,Void,Void>(){
        @Override
        protected Void doInBackground(Void... params) {
            try {
                // … do some work here, for up to 10 seconds
            } finally {
                result.finish();
            }
        }
    }.execute();    
}

This is nice; though its utility is limited by the 10 second budget and the effects of fragmentation (it is only available to API level 11 or above). In the next section, we'll look at scheduling long-running operations with services.

Handling alarms with Services

Just like starting Activities, starting a Service from an alarm involves scheduling an appropriate PendingIntent instance, this time using the static getService method.

We almost certainly want our Service to do its work off the main thread, so sending work to an IntentService this way seems ideal, and an IntentService will also stop itself when the work is finished.

This works beautifully if the device is awake.

However, if the device is asleep we have a potential problem. AlarmManager's documentation tells us that the only guarantee we have about the wakefulness of the device is that it will remain awake until a BroadcastReceiver's onReceive method completes.

Since directly starting a Service does not involve a BroadcastReceiver, and in any case is an asynchronous operation, there is no guarantee that the Service will have started up before the device returns to sleep, so the work may not get done until the device is next awakened.

This is almost certainly not the behavior we want. We want to ensure that the Service starts up and completes its work, regardless of whether the device was awake when the alarm was delivered. To do that, we'll need a BroadcastReceiver and a little explicit power management, as we'll see next.

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

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