Notification Changes
Notifications in Android continue to evolve with the platform. New features get rolled in regularly and Android Lollipop (5.x) is no different. Lollipop introduced some great features with regards to notifications. Most of these are focused around user privacy, but there is also a usability factor to consider. But wait, today’s Android dashboard (last updated on Mar. 2, 2015) shows very low Lollipop usage:
So as an app author why should you be concerned about dealing with Lollipop now? There are several very good reasons: 1) it shows the app users that the app is still being maintained and supporting newer platforms, 2) it is better to get ahead now before the roll out numbers take off and put your team under pressure, and 3) there can be some unwanted side effects with the Lollipop changes. A great example is if a legacy app shows a persistent notification. Let’s say I have a legacy app which communicates with my continuous integration build server and tells me the state of the build. If the app isn’t updated with some simple changes, the notifications in Lollipop won’t show their details or up to date info on the lock screen. The good news is that adapting legacy code to handle notifications in Lollipop isn’t terribly difficult In this article I’ll briefly cover some of the new features which are available then address how you can update your legacy app to not just take advantage of these features but also how to make the app’s traditional notifications behave well on newer devices. Note that I’m not going to touch on everything new for notifications in Lollipop, such as Wearable support. Wearables will generally work out of the box with your notifications, unless you want to get a custom look/feel or even provide newer styling such as cards and stacking notifications. Since those are very wearable specific (and a much bigger topic) I’m going to exclude them from this discussion.
What’s New
For this article, I’m going to roll the new features of API 20 and 21 together and call them “Lollipop” since API 20 is technically the “L preview” version. While conceptually notifications seem like they should be quite simple to setup, the options have continued to expand with each API release which can sometimes make it daunting to navigate. In fact, the actual construction of a Notification
is cumbersome enough that in Honeycomb (API 11) the Notification.Builder
class was added to assist developers. This helper class is invaluable for creating Notification
objects. As usual, to ensure that no app author is left out, even those targeting Android versions down to API level 4 (Eclair) the compatibility library provides the NotificationCompat.Builder
class to provide the same functionality.
Additional Actions
Notification actions are not something brand new with Lollipop. Actions were first introduced in Jelly Bean (API 16) and allow your notifications to have an additional area when touched will cause a different PendingIntent
to be sent. Typically the actions are collapsed and can be expanded by the user with an extra swipe of a notification. Let’s look at a simple example.
public class MainActivity extends ActionBarActivity { ... private NotificationManager mMgr; @Override protected void onCreate(Bundle savedInstanceState) { Button curBtn; super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mMgr = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); curBtn = (Button)findViewById(R.id.btn_alt_action); curBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Context ctx = MainActivity.this; Notification.Builder bldr = createBuilder(R.string.alt_action_main_title, R.string.alt_action_main_content, android.R.drawable.ic_dialog_info); Intent altIntent = new Intent(ctx, SubActivity.class); altIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 1, altIntent, PendingIntent.FLAG_UPDATE_CURRENT); bldr.addAction(android.R.drawable.ic_dialog_alert, getString(R.string.alt_action_title), pendingIntent); postNotification(bldr, R.id.btn_alt_action); } }); ... } private Notification.Builder createBuilder(int title, int content, int icon) { Notification.Builder bldr = new Notification.Builder(this); bldr.setContentTitle(getString(title)); bldr.setContentText(getString(content)); bldr.setSmallIcon(icon); Intent intent = new Intent(this, MainActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT); intent.putExtra(EXTRA_PAYLOAD, getString(title)); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); bldr.setContentIntent(pendingIntent); bldr.setDefaults(Notification.DEFAULT_VIBRATE); return bldr; } }
The sample code above uses a private helper method to create a Notification.Builder
object for our app’s notifications. This is done to maximize code re-use within the app as it is going to use buttons to trigger notifications with different features. Within the createBuilder()
method the base notification options are setup: the title, content text, icon and the PendingIntent
used to trigger our app. For our purposes this is just going to bring our MainActivity
back to the foreground. It includes an extra with the title of the notification which triggered the app to be brought back up. The notification action is added on the button click. Note that it constructs a different PendingIntent
before adding it to our builder with the addAction()
method. When the user touches this action, a new sub-Activity is started which shows buttons for exercising privacy features, which I’ll discuss next. The screen caps below show the base notification and expanded action on a Nexus 7 running KitKat and a Nexus 5 running Lollipop.
Android’s action support was extended with some helper APIs in API 19 then again with API 20 with the Notification.Action
, Notification.Action.Builder
and Notification.Action.WearableExtender
helper classes. The first two helpers make it easier to creation actions and later attach them to a builder being used to construct the action. The third helper is used just for wearables which is still a bit of a changing landscape.
Privacy
One of the features which has been available in AOSP and OEM builds of Android for quite some time is notifications on the lock screen. This handy feature allows the user to quickly see events and decide whether to look at them in more detail. However, the drawback to previous approaches was a lack of privacy. Imagine receiving a 2-factor authentication text message while your phone is visible to others in an elevator or meeting. Obviously not a good thing as someone could see the details. I’m sure you can imagine some other situations where showing notification content is undesirable. Lollipop introduces the notion of “visibility” or privacy of a notification. This allows apps to make the decision of whether a notification contains sensitive information and should be displayed to the user or not. There are three possible settings:
VISIBILITY_PRIVATE
– the notification can be presented on all lockscreens, but content is hidden on secure lockscreensVISIBILITY_PUBLIC
– the notification and content can be presented on all lockscreensVISIBILITY_SECRET
– the notification content is not shown at all on a secure lockscreen
This puts the power of determining what is shown when the device is locked (mostly) up to the app. The user can still select to block all notifications on lock, so keep that in mind as well. Let’s look at how simple this is to add via the compatibility library:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ... Button btn = (Button)findViewById(R.id.btn_simple_legacy); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { NotificationCompat.Builder bldr = createBuilder(R.string.priv_title_legacy, R.string.priv_content_legacy, android.R.drawable.ic_dialog_alert); // Do NOT explicitly set the visibility, this is legacy mMgr.notify(R.id.btn_simple_legacy, bldr.build()); } }); btn = (Button)findViewById(R.id.btn_simple_public); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { NotificationCompat.Builder bldr = createBuilder(R.string.priv_title_public, R.string.priv_content_public, android.R.drawable.ic_dialog_info); bldr.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); mMgr.notify(R.id.btn_simple_public, bldr.build()); } }); btn = (Button)findViewById(R.id.btn_simple_secret); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { NotificationCompat.Builder bldr = createBuilder(R.string.priv_title_secret, R.string.priv_content_secret, android.R.drawable.ic_dialog_email); bldr.setVisibility(NotificationCompat.VISIBILITY_SECRET); mMgr.notify(R.id.btn_simple_secret, bldr.build()); } }); }
Note that this code utilizes the compatibility library so there’s no API protection code present to check for Lollipop specific features. I don’t recommend mixing the use of compatibility library classes and framework classes in a single project like this, but for this demo it’s ok.
This Activity
has three different buttons which set notifications. The first does not set a visibility at all, which will be treated as a VISIBILITY_PRIVATE
notification on Lollipop devices. The next notification is marked as VISIBILITY_PUBLIC
and the final one is marked as VISIBILITY_SECRET
. As the demo below shows, each of these behaves slightly different on Lollipop with respect to the lock screen. On older platforms, such as the Nexus 7 running KitKat shown below, there is no change in behavior.
Filtering and Heads-up Notifications
Setting the priority levels of notifications has been present since API 16, which allows the system to make decisions about how to order notifications. Lollipop extends this functionality by allowing apps to indicate a category for a notification. The combination of these two things allows users to customize their experience. Lollipop now gives users the ability to have notifications “interrupt” them based on a broad set of categories. This means the user will be notified via sounds/lights/vibration even when in-call. Similarly, notifications which have a priority set to high plus a vibration and/or sound set can cause a heads-up notification to be displayed. With this functionality, users can immediately choose to dismiss a notification or even take an action while the foreground Activity
is still shown, no swipe of the status bar required. Back to the demo’s MainActivity
, let’s add some notifications for a status and event. The status notification we’ll leave the default priority and the event we’ll make it a high priority.
curBtn = (Button)findViewById(R.id.btn_cat_status); curBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Context ctx = MainActivity.this; Notification.Builder bldr = createBuilder(R.string.cat_status_title, R.string.cat_status_content, android.R.drawable.ic_popup_disk_full); // Categories are only available on Android >= API 21 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { bldr.setCategory(Notification.CATEGORY_STATUS); } postNotification(bldr, R.id.btn_cat_status); } }); curBtn = (Button)findViewById(R.id.btn_cat_event); curBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Context ctx = MainActivity.this; Notification.Builder bldr = createBuilder(R.string.cat_event_title, R.string.cat_event_content, android.R.drawable.ic_popup_disk_full); // Categories are only available on Android >= API 21 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { bldr.setCategory(Notification.CATEGORY_EVENT); } bldr.setPriority(Notification.PRIORITY_HIGH); postNotification(bldr, R.id.btn_cat_event); } });
Since we’re back to using the framework’s Notification.Builder
, we need to ensure the right API level. This run through on a Nexus 5 running Lollipop shows the event notification pops up a heads-up display due to it priority level. The user could have selected to further filter this by category, if they did not want event type notifications to interrupt them then the heads-up display would not have been shown.
Leveraging Lollipop Notification Features
The features I have touched upon (along with others, such as for wearables) can be leveraged pretty easily using the support library’s NotificationCompat
suite of classes. The examples I’ve shown have used both the built-in APIs, protected with API level checks, and the compatibility library which requires no checks. The simplest way to go is to use the compatibility library as it handles the real heavy lifting for you. However, you should still be aware of limitations the system may impose. For example, if you are using the expandable notification actions then they will only be available on API 16 and above since that is the first platform version which enabled this functionality, even when using the compatibility library. Apps running on platforms older than this need to account for the fact that buttons in custom actions won’t be shown or available.
Preventing Lollipop Mishaps
The bigger issue to be concerned with (and the real purpose behind this article) is how to prevent mishaps on Lollipop within your legacy app. Most notification related changes in the platform are pretty transparent. But, there are still some things to deal with for a better user experience. By far the biggest pain point is the privacy treatment within legacy apps. This is because the framework takes a conservative approach with regards to notifications which do not specify privacy: it assumes VISIBILITY_PRIVATE
. This is a safe setting, allowing notifications to be presented on a locksceen, but not the content. For many apps this may be suitable, but for others it may cause problems for users. As an example, I regularly use an app which keeps a persistent notification showing my “home” weather, severe alerts, etc. It’s handy to have that information right on the lock screen so I can quickly check the temperature or new alerts when they arrive. But, the app’s notifications do not set visibility so I can see it has a persistent notification, just not the content. This makes the persistent notification useless (for me) on a Lollipop device.
It also makes a lot of sense to categorize and prioritize your app’s notifications. It’s easy to do and enables the user’s ability to filter and customize their experience, which is one of Android’s key traits. This subtle polish can also help to “wow” the user, brining an important notification right to their attention.
Wrap up
Android Lollipop’s notification functionality provides some great new functionality. This is great for the platform as it continues as it expands its rich feature set. But, it does come with some potential gotchas for new developers and existing legacy apps. In this article I walked through some of the newer features and outlined how they can be used and how to integrate them in a legacy app. There are much more to notifications than meets the eyes and the Google documentation provides some further details and great examples. What have been some of your experiences with notifications in legacy apps as well as Lollipop? Have you or your users encountered problems? Until next time, experiment and keep evolving your apps!
The sources used in this article can be found at the following GitHub repository:
https://github.com/hiq-larryschiefer/LolliNotify.git