Merge "SysUI: Actual MSIM status bar support" into lmp-mr1-dev
diff --git a/services/core/java/com/android/server/EventLogTags.logtags b/services/core/java/com/android/server/EventLogTags.logtags
index 8417ccc..f04487e 100644
--- a/services/core/java/com/android/server/EventLogTags.logtags
+++ b/services/core/java/com/android/server/EventLogTags.logtags
@@ -51,8 +51,8 @@
 # ---------------------------
 # NotificationManagerService.java
 # ---------------------------
-# when a NotificationManager.notify is called
-2750 notification_enqueue (uid|1|5),(pid|1|5),(pkg|3),(id|1|5),(tag|3),(userid|1|5),(notification|3),(update|1)
+# when a NotificationManager.notify is called. status: 0=post, 1=update, 2=ignored
+2750 notification_enqueue (uid|1|5),(pid|1|5),(pkg|3),(id|1|5),(tag|3),(userid|1|5),(notification|3),(status|1)
 # when someone tries to cancel a notification, the notification manager sometimes
 # calls this with flags too
 2751 notification_cancel (uid|1|5),(pid|1|5),(pkg|3),(id|1|5),(tag|3),(userid|1|5),(required_flags|1),(forbidden_flags|1),(reason|1|5),(listener|3)
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 70d0e6a..c851868 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -116,6 +116,7 @@
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.Map.Entry;
 import java.util.NoSuchElementException;
 import java.util.Objects;
 
@@ -139,6 +140,7 @@
     static final int SHORT_DELAY = 2000; // 2 seconds
 
     static final long[] DEFAULT_VIBRATE_PATTERN = {0, 250, 250, 250};
+
     static final int VIBRATE_PATTERN_MAXLEN = 8 * 2 + 1; // up to eight bumps
 
     static final int DEFAULT_STREAM_TYPE = AudioManager.STREAM_NOTIFICATION;
@@ -166,6 +168,15 @@
     static final float MATCHES_CALL_FILTER_TIMEOUT_AFFINITY =
             ValidateNotificationPeople.STARRED_CONTACT;
 
+    /** notification_enqueue status value for a newly enqueued notification. */
+    private static final int EVENTLOG_ENQUEUE_STATUS_NEW = 0;
+
+    /** notification_enqueue status value for an existing notification. */
+    private static final int EVENTLOG_ENQUEUE_STATUS_UPDATE = 1;
+
+    /** notification_enqueue status value for an ignored notification. */
+    private static final int EVENTLOG_ENQUEUE_STATUS_IGNORED = 2;
+
     private IActivityManager mAm;
     AudioManager mAudioManager;
     StatusBarManagerInternal mStatusBar;
@@ -209,6 +220,7 @@
     final ArrayMap<String, NotificationRecord> mNotificationsByKey =
             new ArrayMap<String, NotificationRecord>();
     final ArrayList<ToastRecord> mToastQueue = new ArrayList<ToastRecord>();
+    final ArrayMap<String, NotificationRecord> mSummaryByGroupKey = new ArrayMap<>();
 
     ArrayList<String> mLights = new ArrayList<String>();
     NotificationRecord mLedNotification;
@@ -251,6 +263,7 @@
     private static final int REASON_LISTENER_CANCEL = 10;
     private static final int REASON_LISTENER_CANCEL_ALL = 11;
     private static final int REASON_GROUP_SUMMARY_CANCELED = 12;
+    private static final int REASON_GROUP_OPTIMIZATION = 13;
 
     private static class Archive {
         final int mBufferSize;
@@ -1658,6 +1671,16 @@
 
             pw.println("\n  Condition providers:");
             mConditionProviders.dump(pw, filter);
+
+            pw.println("\n  Group summaries:");
+            for (Entry<String, NotificationRecord> entry : mSummaryByGroupKey.entrySet()) {
+                NotificationRecord r = entry.getValue();
+                pw.println("    " + entry.getKey() + " -> " + r.getKey());
+                if (mNotificationsByKey.get(r.getKey()) != r) {
+                    pw.println("!!!!!!LEAK: Record not found in mNotificationsByKey.");
+                    r.dump(pw, "      ", getContext());
+                }
+            }
         }
     }
 
@@ -1779,16 +1802,34 @@
                         // Retain ranking information from previous record
                         r.copyRankingInformation(old);
                     }
-                    mRankingHelper.extractSignals(r);
+
+                    // Handle grouped notifications and bail out early if we
+                    // can to avoid extracting signals.
+                    handleGroupedNotificationLocked(r, old, callingUid, callingPid);
+                    boolean ignoreNotification =
+                            removeUnusedGroupedNotificationLocked(r, callingUid, callingPid);
 
                     // This conditional is a dirty hack to limit the logging done on
                     //     behalf of the download manager without affecting other apps.
                     if (!pkg.equals("com.android.providers.downloads")
                             || Log.isLoggable("DownloadManager", Log.VERBOSE)) {
+                        int enqueueStatus = EVENTLOG_ENQUEUE_STATUS_NEW;
+                        if (ignoreNotification) {
+                            enqueueStatus = EVENTLOG_ENQUEUE_STATUS_IGNORED;
+                        } else if (old != null) {
+                            enqueueStatus = EVENTLOG_ENQUEUE_STATUS_UPDATE;
+                        }
                         EventLogTags.writeNotificationEnqueue(callingUid, callingPid,
                                 pkg, id, tag, userId, notification.toString(),
-                                (old != null) ? 1 : 0);
+                                enqueueStatus);
                     }
+
+                    if (ignoreNotification) {
+                        return;
+                    }
+
+                    mRankingHelper.extractSignals(r);
+
                     // 3. Apply local rules
 
                     // blocked apps
@@ -1805,16 +1846,6 @@
                         return;
                     }
 
-                    // Clear out group children of the old notification if the update causes the
-                    // group summary to go away. This happens when the old notification was a
-                    // summary and the new one isn't, or when the old notification was a summary
-                    // and its group key changed.
-                    if (old != null && old.getNotification().isGroupSummary() &&
-                            (!notification.isGroupSummary() ||
-                                    !old.getGroupKey().equals(r.getGroupKey()))) {
-                        cancelGroupChildrenLocked(old, callingUid, callingPid, null);
-                    }
-
                     int index = indexOfNotificationLocked(n.getKey());
                     if (index < 0) {
                         mNotificationList.add(r);
@@ -1864,6 +1895,90 @@
         idOut[0] = id;
     }
 
+    /**
+     * Ensures that grouped notification receive their special treatment.
+     *
+     * <p>Cancels group children if the new notification causes a group to lose
+     * its summary.</p>
+     *
+     * <p>Updates mSummaryByGroupKey.</p>
+     */
+    private void handleGroupedNotificationLocked(NotificationRecord r, NotificationRecord old,
+            int callingUid, int callingPid) {
+        StatusBarNotification sbn = r.sbn;
+        Notification n = sbn.getNotification();
+        String group = sbn.getGroupKey();
+        boolean isSummary = n.isGroupSummary();
+
+        Notification oldN = old != null ? old.sbn.getNotification() : null;
+        String oldGroup = old != null ? old.sbn.getGroupKey() : null;
+        boolean oldIsSummary = old != null && oldN.isGroupSummary();
+
+        if (oldIsSummary) {
+            NotificationRecord removedSummary = mSummaryByGroupKey.remove(oldGroup);
+            if (removedSummary != old) {
+                String removedKey =
+                        removedSummary != null ? removedSummary.getKey() : "<null>";
+                Slog.w(TAG, "Removed summary didn't match old notification: old=" + old.getKey() +
+                        ", removed=" + removedKey);
+            }
+        }
+        if (isSummary) {
+            mSummaryByGroupKey.put(group, r);
+        }
+
+        // Clear out group children of the old notification if the update
+        // causes the group summary to go away. This happens when the old
+        // notification was a summary and the new one isn't, or when the old
+        // notification was a summary and its group key changed.
+        if (oldIsSummary && (!isSummary || !oldGroup.equals(group))) {
+            cancelGroupChildrenLocked(old, callingUid, callingPid, null,
+                    REASON_GROUP_SUMMARY_CANCELED);
+        }
+    }
+
+    /**
+     * Performs group notification optimizations if SysUI is the only active
+     * notification listener and returns whether the given notification should
+     * be ignored.
+     *
+     * <p>Returns true if the given notification is a child of a group with a
+     * summary, which means that SysUI will never show it, and hence the new
+     * notification can be safely ignored.</p>
+     *
+     * <p>For summaries, cancels all children of that group, as SysUI will
+     * never show them anymore.</p>
+     *
+     * @return true if the given notification can be ignored as an optimization
+     */
+    private boolean removeUnusedGroupedNotificationLocked(NotificationRecord r,
+            int callingUid, int callingPid) {
+        // No optimizations are possible if listeners want groups.
+        if (mListeners.notificationGroupsDesired()) {
+            return false;
+        }
+
+        StatusBarNotification sbn = r.sbn;
+        String group = sbn.getGroupKey();
+        boolean isSummary = sbn.getNotification().isGroupSummary();
+        boolean isChild = sbn.getNotification().isGroupChild();
+
+        NotificationRecord summary = mSummaryByGroupKey.get(group);
+        if (isChild && summary != null) {
+            // Child with an active summary -> ignore
+            if (DBG) {
+                Slog.d(TAG, "Ignoring group child " + sbn.getKey() + " due to existing summary "
+                        + summary.getKey());
+            }
+            return true;
+        } else if (isSummary) {
+            // Summary -> cancel children
+            cancelGroupChildrenLocked(r, callingUid, callingPid, null,
+                    REASON_GROUP_OPTIMIZATION);
+        }
+        return false;
+    }
+
     private void buzzBeepBlinkLocked(NotificationRecord record) {
         boolean buzzBeepBlinked = false;
         final Notification notification = record.sbn.getNotification();
@@ -2386,6 +2501,11 @@
         }
 
         mNotificationsByKey.remove(r.sbn.getKey());
+        String groupKey = r.getGroupKey();
+        NotificationRecord groupSummary = mSummaryByGroupKey.get(groupKey);
+        if (groupSummary != null && groupSummary.getKey().equals(r.getKey())) {
+            mSummaryByGroupKey.remove(groupKey);
+        }
 
         // Save it for users of getHistoricalNotifications()
         mArchive.record(r.sbn);
@@ -2433,7 +2553,8 @@
                         mNotificationList.remove(index);
 
                         cancelNotificationLocked(r, sendDelete, reason);
-                        cancelGroupChildrenLocked(r, callingUid, callingPid, listenerName);
+                        cancelGroupChildrenLocked(r, callingUid, callingPid, listenerName,
+                                REASON_GROUP_SUMMARY_CANCELED);
                         updateLightsLocked();
                     }
                 }
@@ -2512,7 +2633,7 @@
                 final int M = canceledNotifications.size();
                 for (int i = 0; i < M; i++) {
                     cancelGroupChildrenLocked(canceledNotifications.get(i), callingUid, callingPid,
-                            listenerName);
+                            listenerName, REASON_GROUP_SUMMARY_CANCELED);
                 }
             }
             if (canceledNotifications != null) {
@@ -2556,14 +2677,14 @@
         int M = canceledNotifications != null ? canceledNotifications.size() : 0;
         for (int i = 0; i < M; i++) {
             cancelGroupChildrenLocked(canceledNotifications.get(i), callingUid, callingPid,
-                    listenerName);
+                    listenerName, REASON_GROUP_SUMMARY_CANCELED);
         }
         updateLightsLocked();
     }
 
     // Warning: The caller is responsible for invoking updateLightsLocked().
     private void cancelGroupChildrenLocked(NotificationRecord r, int callingUid, int callingPid,
-            String listenerName) {
+            String listenerName, int reason) {
         Notification n = r.getNotification();
         if (!n.isGroupSummary()) {
             return;
@@ -2583,11 +2704,10 @@
             StatusBarNotification childSbn = childR.sbn;
             if (childR.getNotification().isGroupChild() &&
                     childR.getGroupKey().equals(r.getGroupKey())) {
-                EventLogTags.writeNotificationCancel(callingUid, callingPid,
-                        pkg, childSbn.getId(), childSbn.getTag(), userId, 0, 0,
-                        REASON_GROUP_SUMMARY_CANCELED, listenerName);
+                EventLogTags.writeNotificationCancel(callingUid, callingPid, pkg, childSbn.getId(),
+                        childSbn.getTag(), userId, 0, 0, reason, listenerName);
                 mNotificationList.remove(i);
-                cancelNotificationLocked(childR, false, REASON_GROUP_SUMMARY_CANCELED);
+                cancelNotificationLocked(childR, false, reason);
             }
         }
     }
@@ -2783,6 +2903,7 @@
     public class NotificationListeners extends ManagedServices {
 
         private final ArraySet<ManagedServiceInfo> mLightTrimListeners = new ArraySet<>();
+        private boolean mNotificationGroupsDesired;
 
         public NotificationListeners() {
             super(getContext(), mHandler, mNotificationList, mUserProfiles);
@@ -2810,6 +2931,7 @@
             final INotificationListener listener = (INotificationListener) info.service;
             final NotificationRankingUpdate update;
             synchronized (mNotificationList) {
+                updateNotificationGroupsDesiredLocked();
                 update = makeRankingUpdateLocked(info);
             }
             try {
@@ -2825,6 +2947,7 @@
                 updateListenerHintsLocked();
             }
             mLightTrimListeners.remove(removed);
+            updateNotificationGroupsDesiredLocked();
         }
 
         public void setOnNotificationPostedTrimLocked(ManagedServiceInfo info, int trim) {
@@ -3028,6 +3151,31 @@
             }
             return false;
         }
+
+        /**
+         * Returns whether any of the currently registered listeners wants to receive notification
+         * groups.
+         *
+         * <p>Currently we assume groups are desired by non-SystemUI listeners.</p>
+         */
+        public boolean notificationGroupsDesired() {
+            return mNotificationGroupsDesired;
+        }
+
+        private void updateNotificationGroupsDesiredLocked() {
+            mNotificationGroupsDesired = true;
+            // No listeners, no groups.
+            if (mServices.isEmpty()) {
+                mNotificationGroupsDesired = false;
+                return;
+            }
+            // One listener: Check whether it's SysUI.
+            if (mServices.size() == 1 &&
+                    mServices.get(0).component.getPackageName().equals("com.android.systemui")) {
+                mNotificationGroupsDesired = false;
+                return;
+            }
+        }
     }
 
     public static final class DumpFilter {