Friday, February 11, 2011

Android Notifications, C2DM, and The Way to Succeed and the Way to Suck Eggs

When it comes to Android Cloud-to-Device Messaging, I've found many new and exciting ways to fail and fail hard. So y'know, learn from my failure and stuff:

State: To Have or Not To Have, That is The Question

There are a lot of cool things you can do with C2DM, but of course notifications are one of the most common and most important things. Now remember that unlike on other inferior platforms, you get total control of what happens when a message is pushed to your application via C2DM. So you don't have to show a notification at all. In case you do though, the first thing you need to decide is if your notifications will be stateful or stateless.

What's the difference? Glad you asked. Let's take a simple scenario. A message is pushed to your app via C2DM, and you display a notification to the user. Now Android notifications do not force the user to either open or dismiss the notification. In my opinion that is a very good thing, and I think most folks agree. You might really be interested in a particular notification, but sometimes you've just gotta finish that game of Meteor Blitz or finish posting a crazy picture to picplz. That's not a problem on Android. The sound will go off, the ticker text will scroll across the status bar, and then a simple icon will appear up there and the user can deal with the notification at their convenience. However, let's say that a few minutes later that another message is pushed to your app via C2DM and once again you need to notify the user of this. If you choose a stateless system, then you will simply replace the current notification about message #1 with a new one about message #2. Alternatively you could post a new notification about message #2 that would sit along side the notification about message #1. However if you do this, then you are a schmuck. You've just created an application that can potentially flood the status bar with your notifications. Your app would have be to all kinds of awesome for me to keep it on my phone, especially if I can't turn off your notifications. So don't do that. So really there's only one option if you want to go stateless: most recent notification always wins and anything older goes to the trash.

Don't like that approach? Me neither. That first notification might have nothing to do with the second, and it might be more important. So that means we need to go stateful. With a stateful system, we can remember that we received message #1, and then replace it with a notification that indicates that we have received message #1 and message #2. How you do that will be very specific to your application of course, but you can figure something out. The point is that you persisted the state, that you had received message #1 and it has not been viewed yet, and you use this knowledge to provide a better experience for the user when message #2 rolls in. Of course now the real fun begins.

Watch out for the Clear Button

You need to not only keep track of the messages that you have received via C2DM, but also their state. Has the user seen them or not? What constitutes "seen" is again specific to your application, but it just means that at various places in your application you will need "mark as seen" for certain messages. There is gotcha here and that is the "Clear" button on the status bar. That should have the effect of marking all messages as seen. However, how do you know when the user taps this button? I thought that maybe there was an Intent fired and that I could receive this with a BroadcastReceiver with the proper action/Intent filter. I searched the docs and found no such action, but I still thought that this was probably the case and it was just not documented (there are a lot of such Intent/actions!) I was so stumped that I posted a question on StackOverflow and offered a 50 reputation point bounty. Turns out there is a very simple answer: the deleteIntent field on the Notification object. Talk about face-palm... Anyways you can set a PendingIntent here, and that PendingIntent can start a Service that does the "mark all as read" work for you.

Badging

Android provides a really easy way to badge your notification icons in the status bar. If you just set the number field on the Notification object, you get badging for free. However, badging looks a little ugly, and if you only have one message then you probably don't want it. In that case just don't set the number field and you are good to go. Then the next time a new message comes in, set the number field to 2. Try this out and rut it on your phone. Unless you have a Nexus S, you will notice a problem. The badging will not show up when you set the number 2 (or 3 or 4 or whatever.) This appears to be a bug with Android that was fixed in 2.3. That's why it works correctly on a Nexus S, but not on anything else (the NS is the only phone with 2.3 as of the time I wrote this.) So what do you do? Badge with a 1? Ugly. Don't badge at all? Less informative. The correct answer is workaround.

Actually you might get lucky and do it the "right" way to begin with. What I did was create a Notification with the same ID as the existing Notification, and then simply call notify on the NotificationManager service. That should replace the existing Notification with the new one, and you should get the right badging. This is the case on my Nexus S, so I'm guessing it's the case on Android 2.3+. This is not the case on previous versions of Android. Everything about the old Notification is replaced except for the badging. The work around is to remove the old Notification (using the cancel method on NotificationManager) and then create the new one using notify. That will work on all versions of Android.

Collapse Key

If you read the C2DM documentation, one of the seemingly innocuous things that it mentions is the collapse key. It says this is "An arbitrary string that is used to collapse a group of like messages when the device is offline, so that only the last message gets sent to the client. This is intended to avoid sending too many messages to the phone when it comes back online." Doesn't this sound like this is for the C2DM servers, so they can throttle the messages being sent to your app? Maybe so, but I found that it come in handy on the app as well, as C2DM passes it on to the app. The reason is simple. You may get the same message from C2DM twice. Well I suppose it might even come more than twice, but what's the difference? Regardless you should expect that dupes might be sent, and so you should de-dupe. The collapse key is perfect for this. A good way to keep track of these is to use a static HashMap (depending on how you implement the Service that processes the C2DM messages, you may not be guaranteed that there will only be one instance of that Service, hence a static HashMap) with soft keys, so that if it grows too large the GC will toss old collapse keys that you don't need anymore.

No comments: