Friday, October 29, 2010

Android: implementing a notification service the right way

One of the key features of Android is the ability for apps to provide notifications that pop up in the status bar. I'll try to explain in this post how you should implement a Service component that polls a web service regularly to check for updates.

The first thing you need is a preferences activity, where the user can set what notifications they want and how often they want your app to check for them. This is easily achieved using a PreferenceActivity, and is not the subject of this post. If you want to learn more on how to create such an activity, click on the aforementioned link.

Once you have your preferences activity, it's time to write your Service. Here are a few key points that you should always respect. I will go into more detail on each one shortly.

  • Respect the global background data setting (I don't think a lot of apps do this).
  • Do your work in a separate thread. It can happen that your Service will run on the UI thread as one of your activities.
  • Obtain a partial wake lock when starting, and release it when you're done. If you don't do this, the system might (and will) kill your service while your background thread is running.
  • Call stopSelf() when you're done, in order to free resources.
  • Tell the system to not start your Service again if it's killed for more resources.

To explain all of these, I'll provide a skeleton Service that does everything mentioned above. All you have to do is fill in the work specific to your project:

public class NotificationService extends Service { private WakeLock mWakeLock; /** * Simply return null, since our Service will not be communicating with * any other components. It just does its work silently. */ @Override public IBinder onBind(Intent intent) { return null; } /** * This is where we initialize. We call this when onStart/onStartCommand is * called by the system. We won't do anything with the intent here, and you * probably won't, either. */ private void handleIntent(Intent intent) { // obtain the wake lock PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Const.TAG); mWakeLock.acquire(); // check the global background data setting ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); if (!cm.getBackgroundDataSetting()) { stopSelf(); return; } // do the actual work, in a separate thread new PollTask().execute(); } private class PollTask extends AsyncTask<Void, Void, Void> { /** * This is where YOU do YOUR work. There's nothing for me to write here * you have to fill this in. Make your HTTP request(s) or whatever it is * you have to do to get your updates in here, because this is run in a * separate thread */ @Override protected Void doInBackground(Void... params) { // do stuff! return null; } /** * In here you should interpret whatever you fetched in doInBackground * and push any notifications you need to the status bar, using the * NotificationManager. I will not cover this here, go check the docs on * NotificationManager. * * What you HAVE to do is call stopSelf() after you've pushed your * notification(s). This will: * 1) Kill the service so it doesn't waste precious resources * 2) Call onDestroy() which will release the wake lock, so the device * can go to sleep again and save precious battery. */ @Override protected void onPostExecute(Void result) { // handle your data stopSelf(); } } /** * This is deprecated, but you have to implement it if you're planning on * supporting devices with an API level lower than 5 (Android 2.0). */ @Override public void onStart(Intent intent, int startId) { handleIntent(intent); } /** * This is called on 2.0+ (API level 5 or higher). Returning * START_NOT_STICKY tells the system to not restart the service if it is * killed because of poor resource (memory/cpu) conditions. */ @Override public int onStartCommand(Intent intent, int flags, int startId) { handleIntent(intent); return START_NOT_STICKY; } /** * In onDestroy() we release our wake lock. This ensures that whenever the * Service stops (killed for resources, stopSelf() called, etc.), the wake * lock will be released. */ public void onDestroy() { super.onDestroy(); mWakeLock.release(); } }

You now have a Service that works as it should, awesome. But how do you start it? You use the AlarmManager, of course (I suggest you read it's documentation if you haven't already). But how and when do you schedule it? How do you handle app updates? How do you handle device reboots? Well, here are some guidelines that I hope will make answering these questions much easier:

  • Use repeating alarms so that you don't have to always re-schedule yourself in your Service.
  • Use inexact repeating alarms so that the system can group together multiple alarms in order to preserve battery life.
  • Always cancel an alarm before resetting it.
  • When receiving the BOOT_COMPLETED signal, don't immediately start your Service. This would slow down the device too much (it already has a lot of work to do when it boots). Instead, set an alarm.

What I personally do is set an alarm whenever onResume() is called on my main Activity (the one that is launched when the user clicks on the app icon in their launcher), and in a BroadcastReceiver that handles the BOOT_COMPLETED event. Here's some code to help you understand:

MainActivity.java

public void onResume() { super.onResume(); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); int minutes = prefs.getInt("interval"); AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE); Intent i = new Intent(this, NotificationService.class); PendingIntent pi = PendingIntent.getService(this, 0, i, 0); am.cancel(pi); // by my own convention, minutes <= 0 means notifications are disabled if (minutes > 0) { am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + minutes*60*1000, minutes*60*1000, pi); } }

AndroidManifest.xml

<receiver android:name=".BootReceiver"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> </intent-filter> </receiver> </application> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

BootReceiver.java

public void onReceive(Context context, Intent intent) { // in our case intent will always be BOOT_COMPLETED, so we can just set // the alarm // Note that a BroadcastReceiver is *NOT* a Context. Thus, we can't use // "this" whenever we need to pass a reference to the current context. // Thankfully, Android will supply a valid Context as the first parameter SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); int minutes = prefs.getInt("interval"); AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); Intent i = new Intent(context, NotificationService.class); PendingIntent pi = PendingIntent.getService(context, 0, i, 0); am.cancel(pi); // by my own convention, minutes <= 0 means notifications are disabled if (minutes > 0) { am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + minutes*60*1000, minutes*60*1000, pi); } }

Finally, you've got yourself a fully functional notifications service that stops and starts whenever it should, that behaves the way the user wants it to behave and that plays nicely with the system. This took some researching, so if I saved a bit of your time, leave a comment :)

55 comments:

  1. Spectacular write up. Will help me in creating a new component that I'm going to be releasing to open source that is intended to help devs push notifications to users of their apps.

    ReplyDelete
  2. @The Dadical: Like C2DM? Don't reinvent the wheel :)

    ReplyDelete
  3. Like Xtify? Don't reinvent the wheel :)

    ReplyDelete
  4. Excellent. I have been studying Service and AlarmManager and your document helped me a lot.

    ReplyDelete
  5. Great write-up! Thanks for this - I learned the hard way why you shouldn't use a Service solely to handle notifications. Hope others can learn from your tips as well.

    ReplyDelete
  6. Just had to respond to the C2DM & XTIFY -- Don't overlook Urban Airship/Airmail. Very impressed with them so far and they seem much more reliable than C2DM and much cheaper and more responsive than XTIFY.

    ReplyDelete
  7. Very well explained ....

    ReplyDelete
  8. Great write up. It would be easier to read if the code was formatted, but that could just be my browser/computer

    ReplyDelete
  9. Code formatting is back up. Sorry 'bout that.

    ReplyDelete
  10. Great write-up, thank you for taking the time to share with the community!

    ReplyDelete
  11. Thanks for the very helpful HOWTO.

    I am wondering why you cancel the previous alarm? My best guess so far, is that is how you deal with the using changing his prefs. Is that the reason?

    ReplyDelete
  12. @Frank I cancel it for two reasons. One, as you said, to deal with a preference change and second because this way I am 100% sure there will never be two alarms at the same time. I'd rather have no alarm than two at the same time :)

    ReplyDelete
  13. Really nice writeup! Do the general guidelines still apply or have there been any changes on what is considered best practices during these 1.5 years since the publish date?

    In addition to the question on canceling the alarm: Wouldn't using the PendingIntent.FLAG_CANCEL_CURRENT on the PendingIntent.getService() call give the same result?

    ReplyDelete
  14. Nice Tutorial it really hepls :)

    ReplyDelete
  15. Amazing tutorial, this i was looking for.. and best thing i learnt today is, don't start your service at boot_completed, as already lot of things going on with android device on start-up of device

    ReplyDelete
  16. Thank's, Very nice tutorial, but i have an problem in my app that after app force stop, alarm and notifications are not invoked. You have any solution for that?

    ReplyDelete
  17. One quick comment - you don't mention declaring the new service in the Android Manifest. Is this not needed for some reason or are you assuming that the reader will know this? If the latter if may be useful to mention it anyway for completeness.

    ReplyDelete
    Replies
    1. +1, it is needed to work.
      Also don't forget to add

      to your manifest for the wake lock.

      Delete
    2. Thanks for these comments guys, I wondered why mine wasnt registering!

      Delete
    3. How do I have to declare the service in the Android Manifest?
      When the NotificationService is called, my app crashes...

      Delete
  18. Very nice tutorial .. exactly what i was looking for .. Thank's man ..

    ReplyDelete
  19. nice work and +1 for mentioning alarmmanager instead of timer

    ReplyDelete
  20. Excellent Tutorial ... Helped me a lot.

    ReplyDelete
  21. Excellent tutorial... Thanks a lot :)

    ReplyDelete
  22. Excellent tutorial. Thanks. I've spent ton o time researching, and then I found your example. Which explains the pitfalls with Alarms that lead me to your tutorial.

    ReplyDelete
  23. Should i use:

    startService(new Intent(MainActivity.this, NotificationService.class));

    in onResume() of the MainActivity?

    because without calling startService(), onStartCommand() is not called. I couldn't find startService() anywhere in the code.

    Thank You

    ReplyDelete
    Replies
    1. You don't call startService() anywhere, that is the point. You use the AlarmManager to have Android start your service for you.

      Delete
  24. My Sincere thanks to you for this tutorial. Most of the tutorials shows how to send Notification but "when" is the most imp part which you covered awesomely well...

    Would you mind adding "Also don't forget to add
    to your manifest for the wake lock." as mentioned in above posts, this will save even more time.. :)

    ReplyDelete
  25. Thanks for this tutorial - it sums up the concepts nicely in one convenient place.

    ReplyDelete
  26. Thank you, one of the most detailed tutorial on this topic. Wish Android documentation folks wrote their docs more like this.

    ReplyDelete
  27. hello friend ,i am creating application , which has one list and list getting data through json, i want notification that user getting new data in that list ,how can i do this

    ReplyDelete
  28. How can I set again the alarm after the user forced closed the application? Without restarting the device

    ReplyDelete
  29. Many thanks for this fine article, Felix. This has helped my development massively.

    ReplyDelete
  30. Great stuff: helped me a lot.
    Thanks man.

    ReplyDelete
  31. I am trying to understand the need for Bootreceiver. As you explained above, if you set a repeating alarm on onResume() it should keep launching your service everytime alarm is fired. What happens when phone is rebooted? Does your above repeating alarm get cancelled. If it does get cancelled, it makes sense to do the bootreceiver logic. Otherwise why do you need it? What other reason do you think for the service to not get launched? I am seeing an issue wherein notifications are not coming after 1-2 days

    ReplyDelete
  32. Thanks, easy & comprehensive. Another useful tool in the Android toolbox :-)

    ReplyDelete
  33. Thanks for sharing! Do you have a github repos of this?

    ReplyDelete
  34. Excelent!! Thank you so much!

    ReplyDelete
  35. This is so helpful. Thanks for sharing. I appreciate your job.

    emergency mass notification software

    ReplyDelete
  36. What is difference between Toast and Notification??

    ReplyDelete
    Replies
    1. Toast show msg on activity
      and Notifications are messages that are pushed to users telling them that something has happened. E.g. When a user receives new email, the notification system is used to notify the user.

      Delete
  37. Nice write-up, thank you for taking the time to do it!

    ReplyDelete
  38. Thanks man for the good summary and the details. This explains why my service starts and stops as it wants :D

    ReplyDelete
  39. This can't work because you need to set permission of WAKE_LOCK in manifest.... ALso it simply doesn't work. The Service never gets called

    ReplyDelete
  40. Ooooh and you have to add the Service to the Manifest !!!

    ReplyDelete
    Replies
    1. If you do that, it will be called

      Delete
  41. Wow. This was exactly what I needed presented exactly the way I needed it. Thanks so much!

    I ended up having to add 3 permissions to get it to work, but it's worked flawlessly:

    INTERNET
    WAKE_LOCK
    ACCESS_NETWORK_STATE

    ReplyDelete
  42. Thanks, you saved me a lot of time.

    ReplyDelete
  43. Its not working for me, I have followed all steps , the service never gets started, I have declared the service more over used the wakelock permission too, can any one help ?

    ReplyDelete