Notification in Android – Part 2

In our last blog, we covered the basics of Android Notification along with their different styles. If you have not read it yet, I will recommend you to read it first before deep diving into other notification styles.

In this blog post, we will be covering the following Notification:

  • Reply Notification
  • Media Notification

Reply Notification Aka Direct Reply Notification

With the introduction of Android N, Direct Reply Notification was rolled out to Android users in order to reduce the numbers of steps required to act on the notification once received. Next question which pops in my mind is how Reply Notification differ from Normal Notification? Let’s Look into it:

Reply Notification is created using two sets of features:

  • Action
  • Remote Input

Action, as we know, signifies a user action on the notification and Remote Input once added, make the Android OS understand that input is requested from the user.

Let’s have a look at how we can create a Remote Input in our notification:

 String replyLabel = "Reply"
 RemoteInput remoteInput = new RemoteInput.Builder(REPLY)
 .setLabel(replyLabel)
 .build();

If you look carefully in the above code, there are two things that might need your attention, first REPLY, and second replyLabel. KEY_REPLY will be used to fetch the user input later on and reply label is the action which will be shown on the notification.

Now, focus our attention towards creating Notification Action:

  NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(
                android.R.drawable.ic_menu_send, replyLabel, getReplyPendingIntent(notificationID,
                messageID))
                .addRemoteInput(remoteInput)
                .setAllowGeneratedReplies(true)
                .build();

If you look at the above code, we are mapping the action with RemoteInput alongside, we also need an icon and a replyLabel (which we create in the previous step)

Now comes the most important part of building this notification, deciding the Pending Intent. Generally, some of the developers forget to remember that Pending Intent must depend on the OS version of the user.

As we know now, Reply Notification was introduced in Android N, hence deciding the Pending Intent for Android N or above can either be a BroadcastReceiver or a Long-running Service. What about Android Versions prior to Android N?

So, for all those versions we can use our Android OS backbone an Activity and give our own User Interface!! Let’s have a look, how we will create it:

 private static PendingIntent getReplyPendingIntent(int notificationId, int messageId) {
        Intent intent;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            intent = new Intent(BaseApplication.getInstance(), NotificationReceiver.class);
            intent.setAction(REPLY_ACTION);
            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
            intent.putExtra(NOTIFICATION_ID, notificationId);
            intent.putExtra(MESSAGE_ID, messageId);
            return PendingIntent.getBroadcast(BaseApplication.getInstance(), 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
        } else {
            Intent mainIntent = new Intent(BaseApplication.getInstance(), MainActivity.class);
            TaskStackBuilder stackBuilder = TaskStackBuilder.create(BaseApplication.getInstance());
            stackBuilder.addParentStack(MainActivity.class);
            stackBuilder.addNextIntent(mainIntent);
            Intent replyIntent = new Intent(BaseApplication.getInstance(), ReplyActivity.class);
            replyIntent.setAction(REPLY_ACTION);
            replyIntent.putExtra(MESSAGE_ID, messageId);
            replyIntent.putExtra(NOTIFICATION_ID, notificationId);
            stackBuilder.addNextIntent(replyIntent);
            return stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
        }
    }

Looking at the above code, you might be thinking what is this TaskStackBuilder? Let’s deep dive a little into this as well:

To understand the concept well, let’s assume we have two Activities, Activity A and Activity B. Activity A consist of List of emails and Activity B displays any Email in Detail. Now when the application is not alive and you get a notification for an Email. As soon as user taps on this notification, Email description i.e Activity B is displayed to the user. Now when the user back press from Activity B, Activity A i.e Email ListActivity should be displayed to the user. In such special Scenario’s we make use of Task Stack Builder.

Now comes the question, how we will be reading the reply which the user sent from the notification. Let’s have a look at that piece of code:

if (action != null && action.equalsIgnoreCase(Utils.REPLY_ACTION)) {
            CharSequence message = Utils.getReplyMessage(intent);
            int messageId = intent.getIntExtra(Utils.MESSAGE_ID, 0);
            Toast.makeText(context, "Message ID: " + messageId + "\nMessage: " + message,
                    Toast.LENGTH_SHORT).show();
        }

So in our broadcast receiver, we have checked for the action whether it corresponds to the action send by our Pending Intent?

 public static CharSequence getReplyMessage(Intent intent) {
        Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
        if (remoteInput != null) {
            return remoteInput.getCharSequence(Utils.REPLY);
        }
        return null;
    }

In the above method, we fetch the Data from the bundle using the same KEY which we used while creating our RemoteInput object.

Media Notification

Media Notification was introduced with Android Lollipop. In case you are developing a Music App and the target Android version is Lollipop and above, you have just struck gold:). Android has given the developers a Life by bringing Media Notification through which you can control Media Sessions.

In Order to make use of Media Session inside Media Notification you need to get the permission from the User:

<permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />

So to demonstrate the Idea, we have created a background Service, which when started will trigger a notification and notify the user that a song is playing. In this blog, we will focus on media Notification rather than building the complete Media Application. Although, for basic understanding let’s go through some of the functions:

  public int onStartCommand(Intent intent, int flags, int startId) {
        if (mManager == null) {
            initMediaSessions();
        }
        handleIntent(intent);
        return super.onStartCommand(intent, flags, startId);
    }

As soon as we start the service, we will get the Intent object in this method where we are initializing the MediaSession and then Handling the Intent Object.

private void handleIntent(Intent intent) {
        if (intent == null || intent.getAction() == null)
            return;

        int action = Integer.parseInt(intent.getAction());
        Bundle bundle = intent.getExtras();
        if (bundle != null) {
            mNotificationID = bundle.getInt(Utils.NOTIFICATION_ID, 0);
            strNotificationChannel = bundle.getString(Utils.NOTIFICATION_CHANNEL);
        }
        handleIntentAction(action);
    }

In the above method, we are retrieving the values passed from the Activity and triggering the corresponding Intent Action.

private void handleIntentAction(int action) {
        switch (action) {
            case Utils.PLAY:
                mController.getTransportControls().play();
                break;
            case Utils.PAUSE:
                mController.getTransportControls().pause();
                break;
            case Utils.FAST_FORWARD:
                mController.getTransportControls().fastForward();
                break;
            case Utils.REWIND:
                mController.getTransportControls().rewind();
                break;
            case Utils.PREVIOUS:
                mController.getTransportControls().skipToPrevious();
                break;
            case Utils.NEXT:
                mController.getTransportControls().skipToNext();
                break;
            case Utils.STOP:
                mController.getTransportControls().stop();
                break;
            default:
                break;
        }
    }

In the above method, we extract the action and call the corresponding transport control method.

Now Let’s look into how we create the Media Notification:

 public static Notification createMediaNotification(NotificationCompat.Action action,
                                                       MediaSessionCompat mediaSession,
                                                       String channelID, int notificationID) {
        
        Intent intent = new Intent(BaseApplication.getInstance(), MusicPlayerService.class);
        intent.setAction(String.valueOf(STOP));

        PendingIntent pendingIntent = PendingIntent.getService(BaseApplication.getInstance().getApplicationContext(),
                1, intent, PendingIntent.FLAG_UPDATE_CURRENT);

        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(BaseApplication.getInstance(), channelID)
                .setSmallIcon(R.mipmap.ic_launcher)
                .setContentTitle(BaseApplication.getInstance().getString(R.string.artist_name))
                .setContentText(BaseApplication.getInstance().getString(R.string.song_title))
                .setDeleteIntent(pendingIntent)
                .setStyle(new android.support.v4.media.app.NotificationCompat.MediaStyle().
                        setShowActionsInCompactView(1, 2, 3).
                        setMediaSession(mediaSession.getSessionToken()));

        notificationBuilder.addAction(getMediaAction(android.R.drawable.ic_media_previous,
                BaseApplication.getInstance().getString(R.string.previous), PREVIOUS, channelID, notificationID));
        notificationBuilder.addAction(getMediaAction(android.R.drawable.ic_media_rew,
                BaseApplication.getInstance().getString(R.string.rewind), REWIND, channelID, notificationID));
        notificationBuilder.addAction(action);
        notificationBuilder.addAction(getMediaAction(android.R.drawable.ic_media_ff,
                BaseApplication.getInstance().getString(R.string.fast_forward), FAST_FORWARD, channelID, notificationID));
        notificationBuilder.addAction(getMediaAction(android.R.drawable.ic_media_next,
                BaseApplication.getInstance().getString(R.string.next), NEXT, channelID, notificationID));
        return notificationBuilder.build();
    }

If you look carefully at the above code, Firstly, we have used NotificationCompat.MediaStyle() to display media notification. Secondly, we have added Actions to the Notification, one can add up to 5 Actions when the notification is in an expanded state. Lastly, setShowActionsInCompactView(1, 2, 3) method tells the OS that when the notification is in collapse mode, add these indices to the view so that only those actions are visible to the user.

You can find a complete working project demonstration the Reply and Media Notification from here.

Feel free to reach out to me for any queries or comments. Cheers!