Lyubomir Ganev
by Lyubomir Ganev
8 min read

Categories

  • post

Tags

  • android
  • alarm
  • pending intent
  • software development

The Android SDK offers an API for scheduling one time or recurring events called alarms. This is for example how your alarm clock applications work, or how a reminder for a calendar event is triggered. It’s pretty simple to use but is has some specific behavior, which you need to take in account. In this post I will describe some of the problems I have encountered when not completely understanding how this API works.

How do you schedule a simple one time alarm

Well basically it should look something like this.

Calendar c = Calendar.getInstance();
c.add(Calendar.SECOND, 10);
final long afterTenSeconds = c.getTimeInMillis();
final int requestCode = 1;

final Intent myIntent = new Intent("MyIntentAction");
    notificationIntent.addCategory("android.intent.category.DEFAULT");
    notificationIntent.putExtra("message", "Hello world!");
    
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
PendingIntent broadcast = PendingIntent.getBroadcast(context, requestCode,
                myIntent, PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.set(AlarmManager.RTC_WAKEUP, afterTenSeconds, broadcast);

So what you typically do is to wrap an Intent into a PendingIntent of a particular type and then call the AlarmManager to execute this PendingIntent at some point in time in the future. The last part is, you need to have a component such as a BroadcastReceiver, Activity or a Service to receive the wrapped Intent and do something useful with it.

What is a PendingIntent

PendingIntents are a very powerful API in the Android SDK. They allow you to wrap a particular Intent specific to your application in order to give it to another application. The nice thing is, they provide the other application with the required security permissions to actually invoke the Intent you have wrapped. Since Intents are can be broadcasted or used to start an Activity or a Service, there are different methods for building a PendingIntent. In the example above, we have used the getBroadcast() method, but there are also methods getActivity() and getService().

What is important to understand is that PendingIntents are not exclusively used only for scheduling alarms through the AlarmManager. They are also used for example by the NotificationManager when showing statusbar notifications. Therefore, PendingIntents have a couple of specific properties which you have to understand, before you start using them to schedule alarms.

The PendingIntent request code

As you have noticed, when building a PendingIntent, you are required to provide an integer called requestCode. The official documentation states that this is a “private request code for the sender”. However, request codes are also a fundamental way to distinguish between PendingIntent instances. You see, the getBroadcast() method is much more than a just a static factory for creating a PendingIntent instance. It internally registers this particular PendingIntent into a kind of global system registry with all PendingIntents from all applications. Remember, the PendingIntents are designed so that other applications could invoke a particular Intent of your application. Therefore a system wide registry makes a lot of sense.

You might wonder, how the single integer for a request code is enough to guarantee that no PendingIntent will be accidentally overwritten by another application? Thankfully, the request code is not solely responsible for determining the uniqueness of a PendingIntent.

PendingIntent disctinction

How do you know if two PendintIntents are the same? It is important to understand that a PendingIntent is just a container with extra functionality, which carries your wrapped Intent inside of it. If you look at the equals() method implementation inside the PendingIntent class, you will find this description:

Comparison operator on two PendingIntent objects, such that true is returned then they both represent the same operation from the same package. This allows you to use {@link #getActivity}, {@link #getBroadcast}, or {@link #getService} multiple times (even across a process being killed), resulting in different PendingIntent objects but whose equals() method identifies them as being the same operation.

So what exactly does “represent the same operation” mean? I turns out, this is a combination of the request code and the wrapped Intent. So now we need to know, how two Intents can be compared. To understand this, you need to look at the documentation of the Intent.filterEquals() method. Basically two Intents are considered equal if their action, data, type, class, and categories are the same. However, nothing is mentioned about their extras Bundle. My tests have shown that these extras are completely ignored when comparing Intents.

To sum up, you can be quite sure you refer to the same PendingIntent if you know its request code and the action, data, type, class, and categories of the wrapped Intent inside of it.

PendingIntent cancellation

As I have mentioned above, the PendingIntent is a whole API on its own, completely independent of the AlarmManager or NotificationManager. So if there is global registry with all PendingIntents, it makes sense to have some way of clearing them, right? This functionality is provided by the cancel() method of a PendingIntent.

This method can be called only by the application, which originally created the PendingIntent. The result of this is that even if another application, such as the AlarmManager, already has a scheduled PendintIntent, nothing will happen when it tries to execute it, because it has been cancelled. This is a really powerful functionality to use and I believe that not that many people are actually aware of it.

PendingIntent flags

You might have also noticed the PendingIntent.FLAG_UPDATE_CURRENT parameter used when calling the getBroadcast() method. The last parameter of the method is an integer containing a bitmask of PendingIntent flags. These flags are also really crucial to understand when you use the PendingIntent API. There are just a few flags you can use:

  • FLAG_ONE_SHOT
  • FLAG_NO_CREATE
  • FLAG_CANCEL_CURRENT
  • FLAG_UPDATE_CURRENT
  • FLAG_IMMUTABLE

I will not go into much detail about all different flags, since their documentation is really self-explanatory. I will focus only on one of them, since it really nicely gives an insight into what acutally happens when you call the getBroadcast(), getService() or getActivity() method of the PendingIntent class

The flag FLAG_NO_CREATE.

Its documentation states:

Flag indicating that if the described PendingIntent does not already exist, then simply return null instead of creating it. For use with {@link #getActivity}, {@link #getBroadcast}, and {@link #getService}.

So when you look at all other flags and this one, you realize that every time you call getBroadcast(), getService() or getActivity(), you put or update a specific PendingIntent into the global registry. FLAG_NO_CREATE is the only way to call any of these methods, without actually registering anything new. So the question is, when would you want to use this? This flag is really useful if you just want to check if a particular PendingIntent exists and is active. The following code snippet illustrates such sample usage.

final int requestCode = 1;

final Intent myIntent = new Intent("MyIntentAction");
    notificationIntent.addCategory("android.intent.category.DEFAULT");
    notificationIntent.putExtra("message", "Hello world!");

PendingIntent broadcast = PendingIntent.getBroadcast(context, requestCode,
                myIntent, PendingIntent.FLAG_NO_CREATE);
if (broadcast != null) {
    broadcast.cancel();
}

Cancelling alarms

Now that we understand how PendingIntents work, we can actually look at the AlarmManager API again. It provides a method for cancelling scheduled alarms, which expects a PendingIntent instance as a parameter. The documentation of this method is unfortunately not complete. It states the following:

Remove any alarms with a matching {@link Intent}. Any alarm, of any type, whose Intent matches this one (as defined by {@link Intent#filterEquals}), will be canceled.

One can assume from it that as long as the wrapped Intent matches, you have cancelled the alarm, right? Unfortunately this is a wrong assumption!

As we know from the PendingIntent matching described above, only if both the request code and the wrapped Intent match, then we can be sure that we are referring to the same PendingIntent. Thus we will cancel exactly the alarm related to the input PendingIntent parameter only if both its request code and wrapped Intent match. The following snippet illustrates this behavior:

Calendar c = Calendar.getInstance();
c.add(Calendar.SECOND, 10);
final long afterTenSeconds = c.getTimeInMillis();
final int requestCode = 42;
final int anotherRequestCode = 1337;
final AlarmManager alarmManager = (AlarmManager)
    context.getSystemService(Context.ALARM_SERVICE);
final Intent sameIntent = new Intent("MyIntentAction");
    notificationIntent.addCategory("android.intent.category.DEFAULT");
    notificationIntent.putExtra("message", "Hello world!");

final PendingIntent broadcast = PendingIntent.getBroadcast(context, 
    requestCode, sameIntent, PendingIntent.FLAG_UPDATE_CURRENT);
                
alarmManager.set(AlarmManager.RTC_WAKEUP, afterTenSeconds, broadcast);

// Check PendintIntent exists
assertTrue(PendingIntent.getBroadcast(context, requestCode,
                sameIntent, PendingIntent.FLAG_NO_CREATE) != null));
    
alarmManager.cancel(PendingIntent.getBroadcast(context, anotherRequestCode, 
    sameIntent, PendingIntent.FLAG_UPDATE_CURRENT));
    
// Check PendintIntent exists
if (PendingIntent.getBroadcast(context, requestCode,
                sameIntent, PendingIntent.FLAG_NO_CREATE) != null)) {
    // We just cancelled the same Intent, WTF?!
}

// Try cancel with the same request code
alarmManager.cancel(PendingIntent.getBroadcast(context, requestCode, 
    sameIntent, PendingIntent.FLAG_UPDATE_CURRENT));
    
// Check PendintIntent exists
if (PendingIntent.getBroadcast(context, requestCode,
                sameIntent, PendingIntent.FLAG_NO_CREATE) == null)) {
    // OK, we have successfully cancelled the alarm
}

Another important thing to note is that the AlarmManager.cancel() method does not cancel the PendingIntent itself. So even after calling this method, the initial PendingIntent is still active in the registry. However, this should be expected, since as already mentioned, the PendingIntents are not used only by the AlarmManager, but are independent API in the SDK.

AlarmManager and PendingIntent lifecycle

A very important thing to keep in mind is that alarms and PendingIntents are not always kept by the framework. I have performed some tests to try to find out when exactly are alarms being disabled and when exactly are PendingIntents being cancelled, depending on external influence by the framework or the user. The following results have been consistent on three different devices running three different Android versions.

  • Motorola Nexus 6 running Android 6.0
  • LG G3S running Android 4.4
  • Sony Xperia Z1 Compact running Android 5.1

PendingIntent & AlarmManager lifecycle test results

Performed action    Cancel PendingIntent    Clear alarm
Force close from app settings   no   yes
Device restart   yes   yes
Runtime exception   no   no
ANR Force close   no   no
Swipe away from recent apps   no   no
Clear app data from settings   yes/no   yes

As you can see, Alarms are not 100% reliable and there are a lot of situations, where you need to take care and reschedule them. The only situation you cannot do anything about it, is when the user Force closes your application from the app settings screen. If this occurs, the Android framework will NOT invoke automatically any Service or BroadcastReceiver of your application unless the user explicitly starts your application again. This might look a little harsh for us developers, but makes perfect sense from a user point of view. Imagine some Service, which crashes immediately every time it gets started. If it is a sticky service, the framework will try to restart it over and over again. Constantly popping an application not responding dialog in front of the user will prevent normal device usage, which is really bad. So this fail-safe functionality is actually a good thing for the whole system and for other apps.

Time to play - try out the different options in the companion app

I have included a demo in the blog’s companion app where you can test some PendingIntents and alarm scheduling. You can find the source code on GitHub