NotificationListenerService and a Whatsapp extension for Dashclock

Android 4.3 (API 18) introduced NotificationListenerService.

In this post I'll talk how to use this API, and I'll show how an app can observe the stream of notifications with the user's permission.

It is very easy to use a NotificationListenerService.
  • First of all you have to extend NotificationListenerService and implement onNotificationPosted() and onNotificationRemoved() methods
    public class WhtsNotificationListener extends NotificationListenerService {
    
            @Override
            public void onNotificationPosted(StatusBarNotification sbn) {
                  //..............
            }
    
            @Override
            public void onNotificationRemoved(StatusBarNotification sbn) {
                  //.............. 
            }
    }
    
  • Then you must declare the service in your manifest file with the BIND_NOTIFICATION_LISTENER_SERVICE permission and include an intent filter with the SERVICE_INTERFACE action
     <service
      android:name="it.gmariotti.android.examples.dashclock.extensions.
                           wsa2.WhtsNotificationListener"
      android:label="@string/service_name"
      android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" >
      <intent-filter>
         <action android:name="android.service.notification.NotificationListenerService" >
         </action>
      </intent-filter>
    </service>
  • Finally user must enable your service. Without this authorization it doesn't work!
    You can find it in "Settings" -> "Security" -> "Notification access".

    It could be a good idea to use a Intent in PreferenceScreen to help user to find this menu to enable/disable this permission.
    You can use android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS
    
    <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
    
        <PreferenceCategory android:title="@string/pref_config" >
            <Preference
                android:summary="@string/pref_config_setting_summary"
                android:title="@string/pref_config_setting" >
                <intent android:action="android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS">
                </intent>
            </Preference>
        </PreferenceCategory>
    
    </PreferenceScreen>
    A little note about it.In Settings there is a String ACTION_NOTIFICATION_LISTENER_SETTINGS that references this action but it is @hide (little bug in 4.3).

It is enough to use a NotificationListenerService.

Now we can use this service to observe the stream of notifications, and I'll show how to use it in a Dashclock Extension.

About 6 months ago I wrote how to use the Accessibility Service to listen the notifications and to use it with a Whatsapp extension for Dashclock.
Now only for Android 4.3 (!) we can improve this extension and switch from Accessibility Service to the new service as Android Team suggests.

First of all, we have to complete our NotificationListenerService.
public class WhtsNotificationListener extends NotificationListenerService {

        private static final String TAG = LogUtils.makeLogTag(WhtsNotificationListener.class);
  
        @Override
        public void onCreate() {
            super.onCreate();
            LOGD(TAG,"Notification Listener created!");
            MessageManager mManager=MessageManager.getInstance(this);
        }

        public void addNotification(StatusBarNotification sbn,boolean updateDash)  {
             if (sbn==null) return;
  
             if (sbn!=null && sbn.getPackageName().equalsIgnoreCase("com.whatsapp")){
                   MessageManager mManager=MessageManager.getInstance(this);
                   if (mManager!=null){
                       mManager.addNotification(sbn.getNotification(),
                                   sbn.getPostTime(),updateDash);
                   }
             }
        }

        @Override
        public void onNotificationPosted(StatusBarNotification sbn) {

             LOGD(TAG,"Notification Posted:\n");
             LOGD(TAG,"StatusBarNotification="+ sbn.toString());
             if (sbn.getNotification()!=null)
                 LOGD(TAG,"Notification="+sbn.getNotification().toString());
             addNotification(sbn,true); 
        }

        @Override
        public void onNotificationRemoved(StatusBarNotification sbn) {
             LOGD(TAG,"Notification Removed:\n");
             if (sbn!=null && sbn.getPackageName().equalsIgnoreCase("com.whatsapp")){
                  MessageManager mManager=MessageManager.getInstance(this);
                  if (mManager!=null){
                      mManager.clearAll();
                  }
             }
        }
}
We use the method onNotificationPosted(StatusBarNotification sbn) to add a message in dashclock and the method onNotificationRemoved(StatusBarNotification sbn) to remove the message in dashclock.

In both cases we have to test the package name of the StatusBarNotification
We can use sbn.getPackageName().equalsIgnoreCase("com.whatsapp")).

The method onNotificationRemoved is very useful.With Accessibility Service we don't know when the notification is read by user. Now we can know when it is removed (either because the user dismissed it or the originating app withdrew it).

We can find the notification's text with sbn.getNotification().tickerText;
Here you can find the logcat of a new whatsapp notification:
08-08 23:12:27.242: D/dashclock_WhtsNotifica(1041): Notification Posted:
08-08 23:12:27.242: D/dashclock_WhtsNotifica(1041): StatusBarNotification=
         StatusBarNotification(pkg=com.whatsapp user=UserHandle{0} id=1 tag=null score=0: 
         Notification(pri=0 contentView=com.whatsapp/0x1090082 vibrate=null 
                      sound=null defaults=0x0 flags=0x0 kind=[null]))
08-08 23:12:27.242: D/MessageManager(1041): tickerText=Message from Mr.Rossi

I used a MessageManager to connect accessibility service with DashClockExtension.
public class MessageManager {

   private final static String TAG = "MessageManager";

   private WhtsExtension mDashExtension;
   private WhtsNotificationListener mNotificationListener;
   private static MessageManager sInstance;
 
   //----------------------------------------------------------------------------
 
   public static MessageManager getInstance(WhtsExtension context) {
       if (sInstance == null) 
           sInstance = new MessageManager();
       if (sInstance.mDashExtension==null)
           sInstance.mDashExtension=context;   
       return sInstance;
   }
 
   public static MessageManager getInstance(WhtsNotificationListener listener) {
       if (sInstance == null) 
           sInstance = new MessageManager();
       if (sInstance.mNotificationListener==null)
           sInstance.mNotificationListener=listener;
       return sInstance;
   }
 
   private MessageManager() {
       mCount = 0;
       mMsgs = new ArrayList();
   }
 
   public void setListener(WhtsNotificationListener notificationListener) {
       this.mNotificationListener=notificationListener;
   }

   //----------------------------------------------------------------------------
 
   private ArrayList mMsgs;
   private int mCount;
 
   public void addNotification(Notification notification, long postTime,boolean updateDash) {
  
      if (notification==null) return;
  
      String rawText=""+notification.tickerText;
      LOGD(TAG,"rawrext="+rawText);
  
      MessageWht msg=new MessageWht();
      msg.setText(rawText);
      mCount++;
      mMsgs.add(msg);
  
      if (updateDash && mDashExtension!=null)
         mDashExtension.onUpdateData(WhtsExtension.UPDATE_REASON_CONTENT_CHANGED);
  
   } 
 
   public void clearAll() {
      mCount=0;
      mMsgs=new ArrayList();
      if (mDashExtension!=null)
         mDashExtension.onUpdateData(WhtsExtension.UPDATE_REASON_CONTENT_CHANGED);
   }   
}
This class stores information about messages (count and text).
Below our dashclock extension:
public class WhtsExtension extends DashClockExtension {

      private static final String TAG = LogUtils.makeLogTag(WhtsExtension.class);

      private MessageManager mManager;

      private String dashTitle;
      private String dashSubtitle;
      private String dashStatus;
      private int dashIcon;
      private boolean dashVisible = true;

      // ----------------------------------------------------

      @Override
      protected void onInitialize(boolean isReconnect) {
           super.onInitialize(isReconnect);
           LOGD(TAG, "onInitialize " + isReconnect);
           setUpdateWhenScreenOn(true);
 
           if (!isReconnect) {
              mManager = MessageManager.getInstance(this);
           }
      }

      @Override
      protected void onUpdateData(int reason) {
           LOGD(TAG, "onUpdate " + reason);
  
           readData();
  
           // publish
           publishUpdateExtensionData();

      }
 
      // ------------------------------------------------------------
 
      private void publishUpdateExtensionData() {

           // Publish the extension data update.
           ExtensionData extData = new ExtensionData();
           if (dashVisible) {
               extData.visible(true).icon(dashIcon).status(dashStatus)
                      .expandedTitle(dashTitle).expandedBody(dashSubtitle);
   
               extData.clickIntent(null);
            } else {
               extData.visible(false);
            }

            publishUpdate(extData);
       } 
      
      //--------------------------------------------------------------

       private void readData() {

            if (mManager == null) return;
  
            dashIcon=R.drawable.ic_extension_whts;
            int mCount = mManager.getmCount();
            LOGD(TAG,"count="+mCount);

            if (mCount > 0) {
               dashVisible = true;
               dashStatus = "" + mCount;
  
               Resources res = getResources();
               String book = res.getQuantityString(R.plurals.notifications,
                           mCount, mCount);
               dashTitle = book;
  
               ArrayList msgs = mManager.getmMsgs();
               if (msgs != null) {
                   StringBuilder sb = new StringBuilder();
                   String and = "";
    
                   for (MessageWht msg:msgs) {
                       sb.append(and);
                       sb.append(msg.getText());
                       and="\n";
                   }
    
                   dashSubtitle=sb.toString();
               }
            }  else {
               dashVisible = false;
            }
       }
}
That's all.... it works.


You can get code from GitHub:

Popular posts from this blog

Expand and collapse animation

How to centralize the support libraries dependencies in gradle

Android-5: Card and images with rounded corners in Android 4