Keep Moving Forward | X-Team Magazine

The Beginner's Guide to Notifications in Android N

Written by Adam Jodłowski | Jul 6, 2016 4:00:00 AM

Android N (API 24) has introduced a lot of new features, and one of them is enhanced notification support for richer and more interactive system notifications.

The Notification API is backwards-compatible, but not every new feature is available across all platforms — we'll see how to deal with that using Android's API. In this tutorial, we'll create basic notifications and showcase just introduced advanced features in order to make sure your apps are ready for Android N.

A new notification look

What's most obvious when you look at the new notifications is the design change — they are now better structured, contain more information and provide more functionalities than ever before:

Source: Android documentation

Prerequisites

For the purpose of this tutorial, you don't need a physical device, just the software:

  • Android Studio 2.1 or higher
  • Android N SDK
  • Java 8 JDK (because of Java 8 features in N)
  • Emulator running Android N

Provided you already have Java 8 and an updated Android Studio, make sure you have Android N SDK and the emulator installed.

Go to your SDK Manager and from the SDK Platforms install the Android 6.X (N), API 24 SDK. In the SDK Tools tab, you'll find Android SDK Build-Tools and Android Support Repository — make sure they are up-to-date.

Go to Tools > Android > AVD Manager in order to setup Android emulator with Android N on-board. Click on + Create Virtual Device button and follow the usual steps — choose desired Device Definition, such as Nexus 5X. On the next page, you'll need to download the Android N system image, if you haven't done that already. Pick it as a system image and proceed with the AVD creation as usual.

This tutorial and screenshots were made using Android Studio 2.2 Preview 3, along with Android API 24 revision 1 and Gradle plugin 2.2.0-alpha3.

Project setup

Now, create the project from the File > New > New Project menu. You can also clone the git repository from the initial commit.

Open the file /app/build.gradle and make sure the that the most important values are not less than these (some dependencies were omitted for brevity):

android {
    compileSdkVersion 24
    buildToolsVersion "23.0.3"
    defaultConfig {
        minSdkVersion 16
        targetSdkVersion 24
    }
}

dependencies {
    compile 'com.android.support:appcompat-v7:24.0.0'
}

One exception could be the minSdkVersion, that depends on the decision, what platforms you want to support in your real app. We'll leave it as it is for the purpose of showing how to handle platform versions incompatibilities.

Display basic notification

Let's start by displaying a basic system notification. Add Button to the main layout and set a click listener on it in your MainActivity (don't forget to add Button widget with ID button to activity_main.xml layout!):

public class MainActivity extends AppCompatActivity {

    private Button button;
    private int notificationId = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = (Button) findViewById(R.id.button);
        button.setOnClickListener(buttonClickListener);
    }

    private View.OnClickListener buttonClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View view) {

            // NotificationCompat Builder takes care of backwards compatibility and
            // provides clean API to create rich notifications
            NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(MainActivity.this)
                    .setSmallIcon(android.R.drawable.ic_dialog_info)
                    .setContentTitle("Notification title")
                    .setContentText("Content text");

            // Obtain NotificationManager system service in order to show the notification
            NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
            notificationManager.notify(notificationId, mBuilder.build());

        }
    };
}

Run the app, click the button and you should see the notification (GitHub commit):

Add basic action

Some notifications are displayed to prompt the user to take an action — let's demonstrate that by displaying a dummy Details Activity for the item ID passed to the notification. Add DetailsActivity.java to the project using Studio's creator:

You can choose Empty Activity and go with the defaults, only changing the class name:

This should declare the new activity in AndroidManifest.xml automatically. If you created this file by hand, add the declaration to the manifest, inside the <application> XML node:

<activity android:name=".DetailsActivity"></activity>

DetailsActivity.java should print out on Logcat whatever has been passed as int number by EXTRA_DETAILS_ID inside intent extra.

public class DetailsActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_details);

        Log.d("PLAYGROUND", "Details ID: " + getIntent().getIntExtra("EXTRA_DETAILS_ID", -1));
    }
}

Go back to MainActivity.java, create PendingIntent, set it's intent extra value, set the notification's ContentIntent to it, and make sure it will auto cancel itself upon clicking (GitHub commit):

private View.OnClickListener buttonClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View view) {

            // Create PendingIntent to take us to DetailsActivity
            // as a result of notification action
            Intent detailsIntent = new Intent(MainActivity.this, DetailsActivity.class);
            detailsIntent.putExtra("EXTRA_DETAILS_ID", 42);
            PendingIntent detailsPendingIntent = PendingIntent.getActivity(
                    MainActivity.this,
                    0,
                    detailsIntent,
                    PendingIntent.FLAG_UPDATE_CURRENT
            );

            // NotificationCompat Builder takes care of backwards compatibility and
            // provides clean API to create rich notifications
            NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(MainActivity.this)
                    .setSmallIcon(android.R.drawable.ic_dialog_info)
                    .setContentTitle("Something important happened")
                    .setContentText("See the details")
                    .setContentIntent(detailsPendingIntent)
                    .setAutoCancel(true);

            ...

        }
    };

Now when you click on the notification, it should be dismissed automatically, and you should be taken to the DetailsActivity with EXTRA_DETAILS_ID extra set to the value that we passed to the notification — look at the Logcat:

06-19 18:32:42.386 6482-6482/io.github.adamjodlowski.notifications D/PLAYGROUND: Details ID: 42

Set custom actions

In case you'd like to take more than one default action for given notification, you can use custom actions and associate different behaviors with them. Modify the NotificationCompat Builder section in the MainActivity, adding two method invocations at the end:

NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(MainActivity.this)
                    .setSmallIcon(android.R.drawable.ic_dialog_info)
                    .setContentTitle("Something important happened")
                    .setContentText("See the details")
                    .setAutoCancel(true)
                    .setContentIntent(detailsPendingIntent)
                    .addAction(android.R.drawable.ic_menu_compass, "Details", detailsPendingIntent)
                    .addAction(android.R.drawable.ic_menu_directions, "Show Map", detailsPendingIntent);

This will add two actions to the notification — notice that the notification is not being dismissed when you click on them — you could remedy that by manually canceling the notification when you receive the PendingIntent — this is where the notification ID comes in handy.

Cancel notification

Clicking on custom actions doesn't cancel the notification. To do that, change the private int notificationId = 1; in MainActivity.java to public static int NOTIFICATION_ID = 1;, so you can invoke the following code at the end of onCreate() method in DetailsActivity.java (GitHub commit):

NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        notificationManager.cancel(MainActivity.NOTIFICATION_ID);

Update notification

Our notification content is being automatically updated, instead of issuing new ones every time we click a button. This is possible because we've been supplying the same NOTIFICATION ID, so the NotificationManager knows that it's been the same notification instance, but with different contents. You can maintain different notifications in your app this way, as long as you distinguish them with their IDs. Try it out with some dynamic data, for example, printing

"" + System.currentTimeMillis()

as a ContentText.

Test on older platform versions

Run the app on an emulator with the platform version lower than 24. You'll see that the notification is indeed displayed in a different way. Notice that the action icons appear on API 23 (in the example screenshot) — they were not visible on API 24. This is one of the changes in visuals that was introduced by Android N (in order to accommodate more space for action buttons). NotificationCompat Builder makes sure everything works across platforms, but you need to be aware of this kind of visual differences.

Direct Reply

The newest addition to Notification features is Direct Reply — a special action that allows the user to input text right into the notification interface, and our application to receive it, without it being brought to the foreground. Let's see how it works.

Add another constant value to the MainActivity so we can have a shared key, to send and query for the reply text:

public static final String KEY_NOTIFICATION_REPLY = "KEY_NOTIFICATION_REPLY";

Create and register BroadcastReceiver

BroadcastReceiver is an Android component that works in the background (but on the UI Thread!) and lets us process messages without bringing the app interface to the front. We need to create it to handle whatever users input to the DirectReply action. Just like we've added a new Activity — click on your package name in the Project pane or choose from Android Studio's menu: File > New > Other > Broadcast Receiver:

Uncheck the Exported option (we don't need that) and proceed. Make sure the BroadcastReceiver has been declared in AndroidManifest.xml:

<receiver android:name=".ReplyReceiver" android:enabled="true" android:exported="false" />

In the ReplyReceiver.java file, fill the handler method body:

public class ReplyReceiver extends BroadcastReceiver {
    public ReplyReceiver() {
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
        if (remoteInput != null) {

            CharSequence id = remoteInput.getCharSequence(MainActivity.KEY_NOTIFICATION_REPLY);

            NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context)
                    .setSmallIcon(android.R.drawable.ic_dialog_info)
                    .setContentTitle("Request received for ID: " + id);

            NotificationManager notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
            notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
        }
    }
}

As you can see, we try to get the RemoteInput from the intent that is passed to the onReceive() method. If it's not there, it means that the current Android platform doesn't support this feature and we can gracefully return from the method. If it is there, we can retrieve the message from the user and act on it — in the example, we just echo it back.

Very important: we need to update or cancel the Notification that triggered this RemoveInput broadcast — this lets Android know that we've successfully processed the input.

With BroadcastReceiver in place, let's declare the RemoteInput, so the Broadcast gets eventually triggered by the user.

Declare RemoteInput

When the user enters a message into the Direct Reply action, it will be sent in the Intent we specified for the action. Let's modify the onCreate() method body of MainActivity like so:

// Create PendingIntent to take us to DetailsActivity
// as a result of notification action
...

// Define PendingIntent for Reply action
PendingIntent replyPendingIntent = null;
// Call Activity on platforms that don't support DirectReply natively
if (Build.VERSION.SDK_INT < 24) {
    replyPendingIntent = detailsPendingIntent;
} else { // Call BroadcastReceiver on platforms supporting DirectReply
    replyPendingIntent = PendingIntent.getBroadcast(
            MainActivity.this,
            0,
            new Intent(MainActivity.this, ReplyReceiver.class),
            PendingIntent.FLAG_UPDATE_CURRENT
    );
}

// Create RemoteInput and attach it to Notification Action
RemoteInput remoteInput = new RemoteInput.Builder(KEY_NOTIFICATION_REPLY)
        .setLabel("Reply")
        .build();
NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(
        android.R.drawable.ic_menu_save, "Provide ID", replyPendingIntent)
        .addRemoteInput(remoteInput)
        .build();

// NotificationCompat Builder takes care of backwards compatibility and
// provides clean API to create rich notifications
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(MainActivity.this)
        .setSmallIcon(android.R.drawable.ic_dialog_info)
        .setContentTitle("Something important happened")
        .setContentText("See the details")
        .setAutoCancel(true)
        .setContentIntent(detailsPendingIntent)
        .addAction(replyAction)
        .addAction(android.R.drawable.ic_menu_compass, "Details", detailsPendingIntent)
        .addAction(android.R.drawable.ic_menu_directions, "Show Map", detailsPendingIntent);

// Obtain NotificationManager system service in order to show the notification
...

Let's go through the code piece-by-piece:

  • PendingIntent for DetailsActivity stays as it was
  • We define another PendingIntent for Reply Action — it's supported natively from Platform API 24, so on older platforms we just show the Activity as usual, but on 24+ we create PendingIntent that triggers Broadcast (not Activity) — this Broadcast will be fired instead of showing Activity, and our BroadcastReceiver will get the Intent
  • We create a RemoteInput and attach it to the NotificationAction — Android will take care of displaying the input widget when the user clicks the Provide ID action
  • We add our action to the notification invoking .addAction(replyAction)

Notice that we're branching our code according to the android.os.Build.VERSION.SDK_INT value — that allows us to provide new features for platforms supporting them and still compile correctly for old platforms.

Test it

Run the app on an emulator supporting API 24+ (GitHub commit). Click on the Provide ID action and input text.

Of course, this is just a simple example of communication between the Notification and our app — in this case, the BroadcastReceiver, but it can also be a Service — it depends on our needs.

Bonus: MessagingStyle

Android N introduced Notification's Messaging Style that mimics a chat conversation. To use it, just set the style on the Notification Builder

NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context)
        .setSmallIcon(android.R.drawable.ic_dialog_info)
        .setStyle(new NotificationCompat.MessagingStyle("Me")
                .setConversationTitle("Example Playground Chat")
                .addMessage("Lorem", 1, null) // null means device's user
                .addMessage("ipsum", 2, "Bot")
                .addMessage("dolor", 3, "Bot")
                .addMessage("sit amet", 4, null));

and display the notification as usual.

Summary

Android N APIs are now final so it's high time for you to get your apps ready for the release. In this tutorial, we've learned how to work with basic Notifications in Android and showcased newest additions to API 24. The most notable changes are the visual overhaul and Direct Reply features, enabling our apps to receive textual input from the user without bringing the application to the front. We've also seen Notification differences between platforms and how to gracefully handle them across Android platform versions.

Source code for this tutorial can be found on GitHub.

by Adam Jodłowski