New INotificationListener interface.

Use with INotificationManager.registerListener(). Limited to
system only right now.

Change-Id: I65b6a8778267022cdc5e58eb75ae607a54b1cc52
diff --git a/Android.mk b/Android.mk
index c8576a0..b9cd7bf 100644
--- a/Android.mk
+++ b/Android.mk
@@ -69,6 +69,7 @@
 	core/java/android/app/IAlarmManager.aidl \
 	core/java/android/app/IBackupAgent.aidl \
 	core/java/android/app/IInstrumentationWatcher.aidl \
+	core/java/android/app/INotificationListener.aidl \
 	core/java/android/app/INotificationManager.aidl \
 	core/java/android/app/IProcessObserver.aidl \
 	core/java/android/app/ISearchManager.aidl \
diff --git a/core/java/android/app/INotificationListener.aidl b/core/java/android/app/INotificationListener.aidl
new file mode 100644
index 0000000..f010a2a
--- /dev/null
+++ b/core/java/android/app/INotificationListener.aidl
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2013, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import com.android.internal.statusbar.StatusBarNotification;
+
+/** @hide */
+oneway interface INotificationListener
+{
+    void onNotificationPosted(in StatusBarNotification notification);
+    void onNotificationRemoved(in StatusBarNotification notification);
+}
\ No newline at end of file
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index 1f4c81d..14bcc0d 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -17,6 +17,7 @@
 
 package android.app;
 
+import android.app.INotificationListener;
 import android.app.ITransientNotification;
 import android.app.Notification;
 import android.content.Intent;
@@ -39,5 +40,8 @@
 
     StatusBarNotification[] getActiveNotifications(String callingPkg);
     StatusBarNotification[] getHistoricalNotifications(String callingPkg, int count);
+
+    void registerListener(in INotificationListener listener, int userid);
+    void unregisterListener(in INotificationListener listener, int userid);
 }
 
diff --git a/core/java/com/android/internal/statusbar/INotificationListener.java b/core/java/com/android/internal/statusbar/INotificationListener.java
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/core/java/com/android/internal/statusbar/INotificationListener.java
diff --git a/services/java/com/android/server/NotificationManagerService.java b/services/java/com/android/server/NotificationManagerService.java
index 13bf39f..9f2685b 100644
--- a/services/java/com/android/server/NotificationManagerService.java
+++ b/services/java/com/android/server/NotificationManagerService.java
@@ -26,6 +26,7 @@
 import android.app.AppOpsManager;
 import android.app.IActivityManager;
 import android.app.INotificationManager;
+import android.app.INotificationListener;
 import android.app.ITransientNotification;
 import android.app.Notification;
 import android.app.PendingIntent;
@@ -151,6 +152,7 @@
     private boolean mInCall = false;
     private boolean mNotificationPulseEnabled;
 
+    // used as a mutex for access to all active notifications & listeners
     private final ArrayList<NotificationRecord> mNotificationList =
             new ArrayList<NotificationRecord>();
 
@@ -161,6 +163,8 @@
 
     private final AppOpsManager mAppOps;
 
+    private ArrayList<NotificationListenerInfo> mListeners = new ArrayList<NotificationListenerInfo>();
+
     // Notification control database. For now just contains disabled packages.
     private AtomicFile mPolicyFile;
     private HashSet<String> mBlockedPackages = new HashSet<String>();
@@ -174,6 +178,38 @@
     private static final String TAG_PACKAGE = "package";
     private static final String ATTR_NAME = "name";
 
+    private class NotificationListenerInfo implements DeathRecipient {
+        INotificationListener listener;
+        int userid;
+        public NotificationListenerInfo(INotificationListener listener, int userid) {
+            this.listener = listener;
+            this.userid = userid;
+        }
+
+        public void notifyPostedIfUserMatch(StatusBarNotification sbn) {
+            if (this.userid != sbn.getUserId()) return;
+            try {
+                listener.onNotificationPosted(sbn);
+            } catch (RemoteException ex) {
+                // not there?
+            }
+        }
+
+        public void notifyRemovedIfUserMatch(StatusBarNotification sbn) {
+            if (this.userid != sbn.getUserId()) return;
+            try {
+                listener.onNotificationRemoved(sbn);
+            } catch (RemoteException ex) {
+                // not there?
+            }
+        }
+
+        @Override
+        public void binderDied() {
+            unregisterListener(this.listener, this.userid);
+        }
+    }
+
     private static class Archive {
         static final int BUFFER_SIZE = 1000;
         ArrayDeque<StatusBarNotification> mBuffer = new ArrayDeque<StatusBarNotification>(BUFFER_SIZE);
@@ -406,6 +442,56 @@
         return tmp;
     }
 
+    @Override
+    public void registerListener(final INotificationListener listener, final int userid) {
+        checkCallerIsSystem();
+        synchronized (mNotificationList) {
+            try {
+                NotificationListenerInfo info = new NotificationListenerInfo(listener, userid);
+                listener.asBinder().linkToDeath(info, 0);
+                mListeners.add(info);
+            } catch (RemoteException e) {
+                // already dead
+            }
+        }
+    }
+
+    @Override
+    public void unregisterListener(INotificationListener listener, int userid) {
+        checkCallerIsSystem();
+        synchronized (mNotificationList) {
+            final int N = mListeners.size();
+            for (int i=N-1; i>=0; i--) {
+                final NotificationListenerInfo info = mListeners.get(i);
+                if (info.listener == listener && info.userid == userid) {
+                    mListeners.remove(listener);
+                }
+            }
+        }
+    }
+
+    private void notifyPostedLocked(NotificationRecord n) {
+        final StatusBarNotification sbn = n.sbn;
+        for (final NotificationListenerInfo info : mListeners) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    info.notifyPostedIfUserMatch(sbn);
+                }});
+        }
+    }
+
+    private void notifyRemovedLocked(NotificationRecord n) {
+        final StatusBarNotification sbn = n.sbn;
+        for (final NotificationListenerInfo info : mListeners) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    info.notifyRemovedIfUserMatch(sbn);
+                }});
+        }
+    }
+
     public static final class NotificationRecord
     {
         final StatusBarNotification sbn;
@@ -1165,6 +1251,8 @@
 
                 // finally, keep some of this information around for later use
                 mArchive.record(n);
+
+                notifyPostedLocked(r);
             } else {
                 Slog.e(TAG, "Ignoring notification with icon==0: " + notification);
                 if (old != null && old.statusBarKey != null) {
@@ -1175,6 +1263,8 @@
                     finally {
                         Binder.restoreCallingIdentity(identity);
                     }
+
+                    notifyRemovedLocked(r);
                 }
                 return; // do not play sounds, show lights, etc. for invalid notifications
             }
@@ -1341,6 +1431,7 @@
                 Binder.restoreCallingIdentity(identity);
             }
             r.statusBarKey = null;
+            notifyRemovedLocked(r);
         }
 
         // sound