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
.
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:
HomeActivity
are finished and removed, because we set the Intent.FLAG_ACTIVITY_CLEAR_TOP
flag.HomeActivity
is (re)started.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.
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.
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.
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.
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.
3.17.176.167