How to get displayed notifications on Android

When doing mobile test automation, you might need to check if a certain notification is displayed or to get all displayed notifications. In this post I’m going to describe how this can be achieved on Android using adb and/or UiAutomator. Some of the approaches can also be used with Appium.

1. Use GUI automation  

In order to get displayed notifications using GUI, you will need open the notification drawer with UiDevice.openNotification() method, get the list UIObject by id(“com.android.systemui:id/notification_stack_scroll” or “com.android.systemui:id/scroll” in Lollipop and below) and then search for the desired notification by its text. You will also need to implement scrolling the list in order to get notifications in the bottom of it.

The downside of this approach, as with all GUI automation, is that it is slow and can be flaky.

2. Use NotificationManager.
NotificationManager notificationManager = (NotificationManager) InstrumentationRegistry.
getInstrumentation().getTargetContext().getSystemService(NOTIFICATION_SERVICE);
StatusBarNotification[] notifications = notificationManager.getActiveNotifications();

This approach is straightforward and easy to implement, but unfortunately, it allows to get only notifications posted by the calling app. That means that we not only won’t be able to obtain notifications posted by other apps, but also by the app under test itself, if our automation is located in a separate project from it. 

3. Use NotificationListenerService

Unlike NotificationManager this service can get all displayed notifications using getActiveNotifications() method. Unfortunately, although I was able to successfully implement this approach in a vanilla Android app, it didn’t work when used in tests.

Also, when using this approach you would also need to automate granting notification access permission to the listener.

4. Adb command

Android shell command “adb shell dumpsys notification — noredact” will return information about all notifications currently displayed on the device. By further processing its output with grep or sed we can get notifications’ contents.

$ adb shell dumpsys notification --noredact | grep ‘android.text\|android.title\|android.subtext’
          android.title=String (1 h 4 min of screen time used) 
          android.text=null 
          android.title=null 
          android.subText=null          
          android.text=null 
          android.title=String (Daily News Updates | The Japan Times)
          android.subText=String (satura001@yahoo.co.uk)
          android.text=SpannableString (Today's top news: Japan health ministry to monitor health of early COVID-19 vaccine recipients
          android.title=String (USB debugging connected) 
          android.text=String (Touch to disable USB debugging) 
          android.title=String (File transfer via USB) 
          android.text=String (Touch for more options) 
          android.title=String (Blue Light Filter - Night M… is redisplaying over other apps) 
          android.text=String (If you don’t want Blue Light Filter - Night M… to use this feature, tap to open settings and turn it off.)

Then, using the following code we can transform this output into a list of key-value pairs, with “title”, “subText” and “text” being the keys.

public static boolean hasNotificationItemViaAdb(String title, String subtitle, String text) {
    for (Map<String, String> notification : getNotificationListViaAdb()) {
        if ((title == null || (notification.get("title") != null &&
                Pattern.compile(title).matcher(notification.get("title")).matches())) &&
                (subtitle == null || (notification.get("subText") != null &&
                        Pattern.compile(subtitle).matcher(notification.get("subText")).matches())) &&
                (text == null || (notification.get("text") != null &&
                        Pattern.compile(text).matcher(notification.get("text")).matches()))) {
            return true;
        }
    }
    return false;
}

public static List<Map<String, String>> getNotificationListViaAdb() {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
        throw new RuntimeException("Notification list cannot be obtained via adb on Android < 6. Please utilize GUI methods instead");
    List<Map<String, String>> notifications = new ArrayList<>();
    String dumpsys = executeAdbShellCommand("dumpsys notification --noredact | sed -n /extras/,/stats/p");
    Pattern extrasPattern = Pattern.compile("extras=\\{\\s+.+?\\R\\s{6}\\}\\R", Pattern.DOTALL);
    Pattern contentsPattern = Pattern.compile("android\\.(title|text|subText)=(null|\\w+\\s\\((.+?)\\))\\R\\s{8}", Pattern.DOTALL);
    Matcher matcher = extrasPattern.matcher(dumpsys);
    while (matcher.find()) {
        String extras = matcher.group();
        Matcher contentsMatcher = contentsPattern.matcher(extras);
        HashMap<String, String> notification = new HashMap<>();
        while (contentsMatcher.find()) {
            String contentsKey = contentsMatcher.group(1);
            String contentsValue = contentsMatcher.group(2).equals("null") ? "" : contentsMatcher.group(3);
            notification.put(contentsKey, contentsValue);
        }
        notifications.add(notification);
    }
    return notifications;
}

The downside of this approach is that we need to get the output of the shell command somehow. In Appium this can be done with the command below (see the details here) or described here. Otherwise you will have to implement communication between the tests running on the device and the machine which devices are physically connected to. We did this using http server and client running on the device and a test runner PC correspondingly.

driver.executeScript(“mobile: shell”, args);

Another downside is that it will work only on Android M and above.

Summary

Unfortunately, there is no silver bullet and each of the approaches has is upsides and downsides.

In our case, we went with combination of GUI appoach for devices older than Android M, and adb shell command with the newer ones.