Notification / Notification.Builder / Nougat

Nougat – Bundled Notifications

Nougat has now been released in to the wild and there are a number of new features which developers should be considering. In this loosely-related series of articles we’re going to be looking at various aspects of these new features to see how we can best make use of them. In this article we’re going to look at one of the new Nougat Notification types.

nougat-smallNotifications have been extended in Nougat and we now have some additional ways of presenting notifications. Being a (pseudo!) messaging app which is generating a number of distinct notifications over a period of time, it may be good to group these notifications together and ‘Bundled Notifications’ enable us to do precisely that. Anyone who has developed notifications for Android Wear may already be familiar with the concept of stacking notifications. Bundled notifications work in exactly the same way, and the API is pretty much identical. But for those unfamiliar with stacking notifications we’ll give a ful explanation of how to implement bundled notifications.

Firstly it is important to understand the behaviour that the user will see. On a Nougat or later device bundled notifications will appear as a list of the individual notifications with the newest at the top under a common header:

bundled-closed-cropped

There is an indication that there are further notification – the +2 in the screen capture. If the user taps on the header, then the notification list will expand to show details of all of the notifications:

bundled-open-cropped

The benefits to the user should be pretty obvious – if we are producing a large number of distinct notifications, then by bundling them in this way we allow the system to present them to the user without overwhelming him/her with a large volume because the collapsed view will be shown. Also the volume of our notifications will not hide notifications from other apps which may be important to the user.

So what happens on older devices? Bundled notifications on a Marshmallow device look something like this:

bundled-legacy-cropped

Essentially the benefits are the same – a large number of notifications from our app will not overwhelm the user, but there is much less detail – and the user will not see any individual notifications. We can control what goes in to this single notification and also add an action which will enable the user to open the app directly to the list of unread messages, if necessary. The important thing is to consider both Nougat+ users and legacy users when designing the notifications.

So to implement this we’ll create a NotificationBuilder class:

final class NotificationBuilder {

    private static final String GROUP_KEY = "Messenger";
    private static final String NOTIFICATION_ID = "com.stylingandroid.nougat.NOTIFICATION_ID";
    private static final int SUMMARY_ID = 0;

    private final Context context;
    private final NotificationManagerCompat notificationManager;
    private final SharedPreferences sharedPreferences;

    static NotificationBuilder newInstance(Context context) {
        Context appContext = context.getApplicationContext();
        Context safeContext = ContextCompat.createDeviceProtectedStorageContext(appContext);
        if (safeContext == null) {
            safeContext = appContext;
        }
        NotificationManagerCompat notificationManager = NotificationManagerCompat.from(safeContext);
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(safeContext);
        return new NotificationBuilder(safeContext, notificationManager, sharedPreferences);
    }

    private NotificationBuilder(Context context,
                                NotificationManagerCompat notificationManager,
                                SharedPreferences sharedPreferences) {
        this.context = context.getApplicationContext();
        this.notificationManager = notificationManager;
        this.sharedPreferences = sharedPreferences;
    }
    .
    .
    .
}

We may need to generate messages before the user has unlocked the device following a reboot so it is important to use device storage for our SharedPreferences, if it’s available.

Let’s look at how we actually generate the notifications:

final class NotificationBuilder {
    .
    .
    .
    void sendBundledNotification(Message message) {
        Notification notification = buildNotification(message, GROUP_KEY);
        notificationManager.notify(getNotificationId(), notification);
        Notification summary = buildSummary(message, GROUP_KEY);
        notificationManager.notify(SUMMARY_ID, summary);
    }

    private Notification buildNotification(Message message, String groupKey) {
        return new NotificationCompat.Builder(context)
                .setContentTitle(message.sender())
                .setContentText(message.message())
                .setWhen(message.timestamp())
                .setSmallIcon(R.drawable.ic_message)
                .setShowWhen(true)
                .setGroup(groupKey)
                .build();
    }

    private Notification buildSummary(Message message, String groupKey) {
        return new NotificationCompat.Builder(context)
                .setContentTitle("Nougat Messenger")
                .setContentText("You have unread messages")
                .setWhen(message.timestamp())
                .setSmallIcon(R.drawable.ic_message)
                .setShowWhen(true)
                .setGroup(groupKey)
                .setGroupSummary(true)
                .build();
    }

    private int getNotificationId() {
        int id = sharedPreferences.getInt(NOTIFICATION_ID, SUMMARY_ID) + 1;
        while (id == SUMMARY_ID) {
            id++;
        }
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putInt(NOTIFICATION_ID, id);
        editor.apply();
        return id;
    }
}

The sendBundledNotification() method is what we call to add a new notification. This builds two separate notifications – the message notification and the summary notification. In the earlier examples each individual message which appeared on Nougat+ devices was the message notification; and the single notification which appeared on pre-Nougat devices was the summary notification. What binds these together is using an identical group key for both notifications – which gets applied to the notification by calling setGroup() on the notification Builder.

Another important point is that the message notification gets posted using a uniquely generated notification ID (we store an ID field in SharedPreferences and increment it for each new notification); whereas the summary message uses a fixed ID. This allows us to add new notifications for each message while only providing a single summary notification.

The summary message needs to be flagged as a summary message by calling setGroupSummary(true) on the notification Builder. The official documentation that I’ve seen strongly recommend the user of a summary notification. However, in my testing, my experience was that messages will simply show as individual notifications (and won’t get bundled) if you don’t provide a summary notification. So it’s really more a requirement than recommendation.

Finally, it is worth pointing out that I’m using NotificationCompat throughout which is always good practice. Furthermore setGroup() and setGroupSummary() are only available in API 20 and later so you’ll have to use this if you’re targeting APIs below 20.

All that remains is to create notifications from our MessengerService:

public class MessengerService extends GcmTaskService {
    private static final String TAG = MessengerService.class.getCanonicalName();

    private Messenger messenger;
    private ServiceScheduler serviceScheduler;
    private NotificationBuilder notificationBuilder;

    public MessengerService() {
    }

    @Override
    public void onCreate() {
        super.onCreate();
        messenger = Messenger.newInstance(this);
        serviceScheduler = ServiceScheduler.newInstance(this);
        notificationBuilder = NotificationBuilder.newInstance(this);
    }

    @Override
    public int onRunTask(TaskParams taskParams) {
        Message message = messenger.generateNewMessage();
        Log.d(TAG, message.toString());
        notificationBuilder.sendBundledNotification(message);
        serviceScheduler.scheduleService();
        return GcmNetworkManager.RESULT_SUCCESS;
    }

    @Override
    public void onDestroy() {
        messenger = null;
        serviceScheduler = null;
        super.onDestroy();
    }
}

So that’s it for bundled notifications, however there is another way of presenting notifications which is particularly well suited to messaging apps and we’ll take a look at that in the next article.

The source code for this article is available here.

© 2016, Mark Allison. All rights reserved.

Copyright © 2016 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.