SysUI: Display group children when no summary is present

Also move common notification filtering into NotificationData
such that clients won't have to do it themselves anymore.

Probably also fixes a couple of multi-user issues.

Bug: 16302692
Bug: 16224431
Change-Id: Ic3c11cd1a224044486f5544e068ca5297d30c521
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 518a6c2..c7fdbed 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -1732,6 +1732,20 @@
     }
 
     /**
+     * @hide
+     */
+    public boolean isGroupSummary() {
+        return mGroupKey != null && (flags & FLAG_GROUP_SUMMARY) != 0;
+    }
+
+    /**
+     * @hide
+     */
+    public boolean isGroupChild() {
+        return mGroupKey != null && (flags & FLAG_GROUP_SUMMARY) == 0;
+    }
+
+    /**
      * Builder class for {@link Notification} objects.
      *
      * Provides a convenient way to set the various fields of a {@link Notification} and generate
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
index f8cdd40..8eca0ae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
@@ -36,7 +36,6 @@
 import android.content.res.Configuration;
 import android.database.ContentObserver;
 import android.graphics.Rect;
-import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Build;
@@ -96,7 +95,8 @@
 
 public abstract class BaseStatusBar extends SystemUI implements
         CommandQueue.Callbacks, ActivatableNotificationView.OnActivatedListener,
-        RecentsComponent.Callbacks, ExpandableNotificationRow.ExpansionLogger {
+        RecentsComponent.Callbacks, ExpandableNotificationRow.ExpansionLogger,
+        NotificationData.Environment {
     public static final String TAG = "StatusBar";
     public static final boolean DEBUG = false;
     public static final boolean MULTIUSER_DEBUG = false;
@@ -132,7 +132,7 @@
     protected H mHandler = createHandler();
 
     // all notifications
-    protected NotificationData mNotificationData = new NotificationData();
+    protected NotificationData mNotificationData;
     protected NotificationStackScrollLayout mStackScroller;
 
     // for heads up notifications
@@ -201,6 +201,7 @@
     protected NotificationOverflowContainer mKeyguardIconOverflowContainer;
     protected DismissView mDismissView;
 
+    @Override  // NotificationData.Environment
     public boolean isDeviceProvisioned() {
         return mDeviceProvisioned;
     }
@@ -304,10 +305,6 @@
                 @Override
                 public void run() {
                     for (StatusBarNotification sbn : notifications) {
-                        if (shouldFilterOut(sbn.getNotification())) {
-                            if (DEBUG) Log.d(TAG, "Ignoring notification: " + sbn);
-                            continue;
-                        }
                         addNotification(sbn, currentRanking);
                     }
                 }
@@ -322,20 +319,8 @@
                 @Override
                 public void run() {
                     Notification n = sbn.getNotification();
-                    boolean isUpdate = mNotificationData.findByKey(sbn.getKey()) != null
+                    boolean isUpdate = mNotificationData.get(sbn.getKey()) != null
                             || isHeadsUp(sbn.getKey());
-                    if (shouldFilterOut(n)) {
-                        if (DEBUG) Log.d(TAG, "Ignoring notification: " + sbn);
-                        // If this is an update, i.e. the notification existed
-                        // before but wasn't filtered out, remove the old
-                        // instance. Otherwise just update the ranking.
-                        if (isUpdate) {
-                            removeNotification(sbn.getKey(), rankingMap);
-                        } else {
-                            updateNotificationRanking(rankingMap);
-                        }
-                        return;
-                    }
                     if (isUpdate) {
                         updateNotification(sbn, rankingMap);
                     } else {
@@ -368,11 +353,6 @@
             });
         }
 
-        private boolean shouldFilterOut(Notification n) {
-            // Don't accept group children.
-            return n.getGroup() != null
-                    && (n.flags & Notification.FLAG_GROUP_SUMMARY) == 0;
-        }
     };
 
     private void updateCurrentProfilesCache() {
@@ -391,6 +371,8 @@
         mWindowManagerService = WindowManagerGlobal.getWindowManagerService();
         mDisplay = mWindowManager.getDefaultDisplay();
 
+        mNotificationData = new NotificationData(this);
+
         mDreamManager = IDreamManager.Stub.asInterface(
                 ServiceManager.checkService(DreamService.DREAM_SERVICE));
         mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
@@ -499,7 +481,8 @@
       return mHeadsUpNotificationView != null && mHeadsUpNotificationView.isShowing(key);
     }
 
-    public boolean notificationIsForCurrentProfiles(StatusBarNotification n) {
+    @Override  // NotificationData.Environment
+    public boolean isNotificationForCurrentProfiles(StatusBarNotification n) {
         final int thisUserId = mCurrentUserId;
         final int notificationUserId = n.getUserId();
         if (DEBUG && MULTIUSER_DEBUG) {
@@ -881,6 +864,16 @@
         return mUsersAllowingPrivateNotifications.get(userHandle);
     }
 
+    /**
+     * Returns true if we're on a secure lockscreen and the user wants to hide "sensitive"
+     * notification data. If so, private notifications should show their (possibly
+     * auto-generated) publicVersion, and secret notifications should be totally invisible.
+     */
+    @Override  // NotificationData.Environment
+    public boolean shouldHideSensitiveContents(int userid) {
+        return isLockscreenPublicMode() && !userAllowsPrivateNotificationsInPublic(userid);
+    }
+
     public void onNotificationClear(StatusBarNotification notification) {
         try {
             mBarService.onNotificationClear(
@@ -1381,11 +1374,14 @@
     protected void updateRowStates() {
         int maxKeyguardNotifications = getMaxKeyguardNotifications();
         mKeyguardIconOverflowContainer.getIconsView().removeAllViews();
-        final int N = mNotificationData.size();
+
+        ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
+        final int N = activeNotifications.size();
+
         int visibleNotifications = 0;
         boolean onKeyguard = mState == StatusBarState.KEYGUARD;
         for (int i = 0; i < N; i++) {
-            NotificationData.Entry entry = mNotificationData.get(i);
+            NotificationData.Entry entry = activeNotifications.get(i);
             if (onKeyguard) {
                 entry.row.setExpansionDisabled(true);
             } else {
@@ -1461,7 +1457,7 @@
         if (wasHeadsUp) {
             oldEntry = mHeadsUpNotificationView.getEntry();
         } else {
-            oldEntry = mNotificationData.findByKey(key);
+            oldEntry = mNotificationData.get(key);
         }
         if (oldEntry == null) {
             return;
@@ -1620,6 +1616,7 @@
                             notification.getNotification().tickerText);
                     oldEntry.icon.set(ic);
                     inflateViews(oldEntry, mStackScroller, wasHeadsUp);
+                    mNotificationData.updateRanking(ranking);
                     updateNotifications();
                 }
             }
@@ -1630,7 +1627,7 @@
         updateNotificationVetoButton(oldEntry.row, notification);
 
         // Is this for you?
-        boolean isForCurrentUser = notificationIsForCurrentProfiles(notification);
+        boolean isForCurrentUser = isNotificationForCurrentProfiles(notification);
         if (DEBUG) Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you");
 
         // Restart the ticker if it's still running
@@ -1725,14 +1722,6 @@
         return interrupt;
     }
 
-    // Q: What kinds of notifications should show during setup?
-    // A: Almost none! Only things coming from the system (package is "android") that also
-    // have special "kind" tags marking them as relevant for setup (see below).
-    protected boolean showNotificationEvenIfUnprovisioned(StatusBarNotification sbn) {
-        return "android".equals(sbn.getPackageName())
-                && sbn.getNotification().extras.getBoolean(Notification.EXTRA_ALLOW_DURING_SETUP);
-    }
-
     public boolean inKeyguardRestrictedInputMode() {
         return KeyguardTouchDelegate.getInstance(mContext).isInputRestricted();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
index a1ab7ec..7581cc9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
@@ -20,18 +20,22 @@
 import android.service.notification.NotificationListenerService.Ranking;
 import android.service.notification.NotificationListenerService.RankingMap;
 import android.service.notification.StatusBarNotification;
+import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.view.View;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 
 /**
  * The list of currently displaying notifications.
- *
- * TODO: Rename to NotificationList.
  */
 public class NotificationData {
+
+    private final Environment mEnvironment;
+
     public static final class Entry {
         public String key;
         public StatusBarNotification notification;
@@ -44,7 +48,6 @@
         public boolean autoRedacted; // whether the redacted notification was generated by us
         public boolean legacy; // whether the notification has a legacy, dark background
 
-        public Entry() {}
         public Entry(StatusBarNotification n, StatusBarIconView ic) {
             this.key = n.getKey();
             this.notification = n;
@@ -58,12 +61,6 @@
             return expandedBig;
         }
         public View getPublicContentView() { return expandedPublic; }
-        /**
-         * Set the flag indicating that this is being touched by the user.
-         */
-        public void setUserLocked(boolean userLocked) {
-            row.setUserLocked(userLocked);
-        }
 
         public void setInterruption() {
             interruption = true;
@@ -90,7 +87,9 @@
         }
     }
 
-    private final ArrayList<Entry> mEntries = new ArrayList<Entry>();
+    private final ArrayMap<String, Entry> mEntries = new ArrayMap<>();
+    private final ArrayList<Entry> mSortedAndFiltered = new ArrayList<>();
+
     private RankingMap mRankingMap;
     private final Ranking mTmpRanking = new Ranking();
     private final Comparator<Entry> mRankingComparator = new Comparator<Entry>() {
@@ -118,36 +117,37 @@
         }
     };
 
-    public int size() {
-        return mEntries.size();
+    public NotificationData(Environment environment) {
+        mEnvironment = environment;
     }
 
-    public Entry get(int i) {
-        return mEntries.get(i);
+    /**
+     * Returns the sorted list of active notifications (depending on {@link Environment}
+     *
+     * <p>
+     * This call doesn't update the list of active notifications. Call {@link #filterAndSort()}
+     * when the environment changes.
+     * <p>
+     * Don't hold on to or modify the returned list.
+     */
+    public ArrayList<Entry> getActiveNotifications() {
+        return mSortedAndFiltered;
     }
 
-    public Entry findByKey(String key) {
-        for (Entry e : mEntries) {
-            if (e.key.equals(key)) {
-                return e;
-            }
-        }
-        return null;
+    public Entry get(String key) {
+        return mEntries.get(key);
     }
 
     public void add(Entry entry, RankingMap ranking) {
-        mEntries.add(entry);
+        mEntries.put(entry.notification.getKey(), entry);
         updateRankingAndSort(ranking);
     }
 
     public Entry remove(String key, RankingMap ranking) {
-        Entry e = findByKey(key);
-        if (e == null) {
-            return null;
-        }
-        mEntries.remove(e);
+        Entry removed = mEntries.remove(key);
+        if (removed == null) return null;
         updateRankingAndSort(ranking);
-        return e;
+        return removed;
     }
 
     public void updateRanking(RankingMap ranking) {
@@ -155,45 +155,79 @@
     }
 
     public boolean isAmbient(String key) {
-        // TODO: Remove when switching to NotificationListener.
-        if (mRankingMap == null) {
-            for (Entry entry : mEntries) {
-                if (key.equals(entry.key)) {
-                    return entry.notification.getNotification().priority ==
-                            Notification.PRIORITY_MIN;
-                }
-            }
-        } else {
-            mRankingMap.getRanking(key, mTmpRanking);
-            return mTmpRanking.isAmbient();
-        }
-        return false;
+        mRankingMap.getRanking(key, mTmpRanking);
+        return mTmpRanking.isAmbient();
     }
 
     private void updateRankingAndSort(RankingMap ranking) {
         if (ranking != null) {
             mRankingMap = ranking;
         }
-        Collections.sort(mEntries, mRankingComparator);
+        filterAndSort();
     }
 
-    /**
-     * Return whether there are any visible items (i.e. items without an error).
-     */
-    public boolean hasVisibleItems() {
-        for (Entry e : mEntries) {
-            if (e.expanded != null) { // the view successfully inflated
-                return true;
+    // TODO: This should not be public. Instead the Environment should notify this class when
+    // anything changed, and this class should call back the UI so it updates itself.
+    public void filterAndSort() {
+        mSortedAndFiltered.clear();
+
+        ArraySet<String> groupsWithSummaries = null;
+        final int N = mEntries.size();
+        for (int i = 0; i < N; i++) {
+            Entry entry = mEntries.valueAt(i);
+            StatusBarNotification sbn = entry.notification;
+
+            if (shouldFilterOut(sbn)) {
+                continue;
             }
+
+            if (sbn.getNotification().isGroupSummary()) {
+                if (groupsWithSummaries == null) {
+                    groupsWithSummaries = new ArraySet<>();
+                }
+                groupsWithSummaries.add(sbn.getGroupKey());
+            }
+            mSortedAndFiltered.add(entry);
+        }
+
+        // Second pass: Filter out group children with summary.
+        if (groupsWithSummaries != null) {
+            final int M = mSortedAndFiltered.size();
+            for (int i = M - 1; i >= 0; i--) {
+                Entry ent = mSortedAndFiltered.get(i);
+                StatusBarNotification sbn = ent.notification;
+                if (sbn.getNotification().isGroupChild() &&
+                        groupsWithSummaries.contains(sbn.getGroupKey())) {
+                    mSortedAndFiltered.remove(i);
+                }
+            }
+        }
+
+        Collections.sort(mSortedAndFiltered, mRankingComparator);
+    }
+
+    private boolean shouldFilterOut(StatusBarNotification sbn) {
+        if (!(mEnvironment.isDeviceProvisioned() ||
+                showNotificationEvenIfUnprovisioned(sbn))) {
+            return true;
+        }
+
+        if (!mEnvironment.isNotificationForCurrentProfiles(sbn)) {
+            return true;
+        }
+
+        if (sbn.getNotification().visibility == Notification.VISIBILITY_SECRET &&
+                mEnvironment.shouldHideSensitiveContents(sbn.getUserId())) {
+            return true;
         }
         return false;
     }
 
     /**
-     * Return whether there are any clearable items (that aren't errors).
+     * Return whether there are any clearable notifications (that aren't errors).
      */
-    public boolean hasClearableItems() {
-        for (Entry e : mEntries) {
+    public boolean hasActiveClearableNotifications() {
+        for (Entry e : mSortedAndFiltered) {
             if (e.expanded != null) { // the view successfully inflated
                 if (e.notification.isClearable()) {
                     return true;
@@ -202,4 +236,54 @@
         }
         return false;
     }
+
+    // Q: What kinds of notifications should show during setup?
+    // A: Almost none! Only things coming from the system (package is "android") that also
+    // have special "kind" tags marking them as relevant for setup (see below).
+    public static boolean showNotificationEvenIfUnprovisioned(StatusBarNotification sbn) {
+        return "android".equals(sbn.getPackageName())
+                && sbn.getNotification().extras.getBoolean(Notification.EXTRA_ALLOW_DURING_SETUP);
+    }
+
+    public void dump(PrintWriter pw, String indent) {
+        int N = mSortedAndFiltered.size();
+        pw.print(indent);
+        pw.println("active notifications: " + N);
+        for (int i = 0; i < N; i++) {
+            NotificationData.Entry e = mSortedAndFiltered.get(i);
+            dumpEntry(pw, indent, i, e);
+        }
+
+        int M = mEntries.size();
+        pw.print(indent);
+        pw.println("inactive notifications: " + M);
+        for (int i = 0; i < M; i++) {
+            Entry entry = mEntries.valueAt(i);
+            if (!mSortedAndFiltered.contains(entry)) {
+                dumpEntry(pw, indent, i, entry);
+            }
+        }
+    }
+
+    private void dumpEntry(PrintWriter pw, String indent, int i, Entry e) {
+        pw.print(indent);
+        pw.println("  [" + i + "] key=" + e.key + " icon=" + e.icon);
+        StatusBarNotification n = e.notification;
+        pw.print(indent);
+        pw.println("      pkg=" + n.getPackageName() + " id=" + n.getId() + " score=" + n
+                .getScore());
+        pw.print(indent);
+        pw.println("      notification=" + n.getNotification());
+        pw.print(indent);
+        pw.println("      tickerText=\"" + n.getNotification().tickerText + "\"");
+    }
+
+    /**
+     * Provides access to keyguard state and user settings dependent data.
+     */
+    public interface Environment {
+        public boolean shouldHideSensitiveContents(int userId);
+        public boolean isDeviceProvisioned();
+        public boolean isNotificationForCurrentProfiles(StatusBarNotification sbn);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
index 6644a6c..cccfb7b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -1346,7 +1346,7 @@
         float factor = 1f;
         if (mNotificationStackScroller.getNotGoneChildCount() > 0) {
             factor = 0.6f;
-        } else if (!mStatusBar.hasNotifications()) {
+        } else if (!mStatusBar.hasActiveNotifications()) {
             factor = 0.4f;
         }
         mEmptyDragAmount = amount * factor;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index 044f69c..20a4092 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -453,8 +453,6 @@
     private Runnable mLaunchTransitionEndRunnable;
     private boolean mLaunchTransitionFadingAway;
 
-    private boolean mHasNotifications;
-
     private static final int VISIBLE_LOCATIONS = ViewState.LOCATION_FIRST_CARD
             | ViewState.LOCATION_TOP_STACK_PEEKING
             | ViewState.LOCATION_MAIN_AREA
@@ -498,9 +496,10 @@
             //    notifications.
             // 3. Report newly visible and no-longer visible notifications.
             // 4. Keep currently visible notifications for next report.
-            int N = mNotificationData.size();
+            ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
+            int N = activeNotifications.size();
             for (int i = 0; i < N; i++) {
-                Entry entry = mNotificationData.get(i);
+                Entry entry = activeNotifications.get(i);
                 String key = entry.notification.getKey();
                 boolean previouslyVisible = mCurrentlyVisibleNotifications.contains(key);
                 boolean currentlyVisible =
@@ -1314,7 +1313,7 @@
             // Recalculate the position of the sliding windows and the titles.
             updateExpandedViewPos(EXPANDED_LEAVE_ALONE);
 
-            if (CLOSE_PANEL_WHEN_EMPTIED && mNotificationData.size() == 0
+            if (CLOSE_PANEL_WHEN_EMPTIED && !hasActiveNotifications()
                     && !mNotificationPanel.isTracking()) {
                 if (mState == StatusBarState.SHADE) {
                     animateCollapsePanels();
@@ -1342,27 +1341,15 @@
     private void updateNotificationShade() {
         if (mStackScroller == null) return;
 
-        int N = mNotificationData.size();
-
-        ArrayList<View> toShow = new ArrayList<View>();
-
-        final boolean provisioned = isDeviceProvisioned();
-        // If the device hasn't been through Setup, we only show system notifications
+        ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
+        ArrayList<ExpandableNotificationRow> toShow = new ArrayList<>(activeNotifications.size());
+        final int N = activeNotifications.size();
         for (int i=0; i<N; i++) {
-            Entry ent = mNotificationData.get(i);
-            if (!(provisioned || showNotificationEvenIfUnprovisioned(ent.notification))) continue;
+            Entry ent = activeNotifications.get(i);
+            int vis = ent.notification.getNotification().visibility;
 
-            if (!notificationIsForCurrentProfiles(ent.notification)) continue;
-
+            // Display public version of the notification if we need to redact.
             final boolean hideSensitive = shouldHideSensitiveContents(ent.notification.getUserId());
-            final int vis = ent.notification.getNotification().visibility;
-
-            // when isLockscreenPublicMode() we suppress VISIBILITY_SECRET notifications
-            if (vis == Notification.VISIBILITY_SECRET && hideSensitive) {
-                continue;
-            }
-
-            // when isLockscreenPublicMode() we show the public form of VISIBILITY_PRIVATE notifications
             boolean showingPublic = vis == Notification.VISIBILITY_PRIVATE && hideSensitive;
             ent.row.setShowingPublic(showingPublic);
             if (ent.autoRedacted && ent.legacy) {
@@ -1419,40 +1406,24 @@
         updateSpeedbump();
         updateClearAll();
 
-        mNotificationPanel.setQsExpansionEnabled(provisioned && mUserSetup);
+        mNotificationPanel.setQsExpansionEnabled(isDeviceProvisioned() && mUserSetup);
         mShadeUpdates.check();
     }
 
     private void updateClearAll() {
-        boolean showDismissView = false;
-        if (mState != StatusBarState.KEYGUARD) {
-            for (int i = 0; i < mNotificationData.size(); i++) {
-                Entry entry = mNotificationData.get(i);
-                if (entry.row.getParent() == null) {
-                    // This view isn't even added, so the stack scroller doesn't
-                    // know about it. Ignore completely.
-                    continue;
-                }
-                if (entry.row.getVisibility() != View.GONE && entry.expanded != null
-                        && entry.notification.isClearable()) {
-                    showDismissView = true;
-                    break;
-                }
-            }
-        }
+        boolean showDismissView =
+                mState != StatusBarState.KEYGUARD &&
+                mNotificationData.hasActiveClearableNotifications();
         mStackScroller.updateDismissView(showDismissView);
     }
 
     private void updateSpeedbump() {
         int speedbumpIndex = -1;
         int currentIndex = 0;
-        for (int i = 0; i < mNotificationData.size(); i++) {
-            Entry entry = mNotificationData.get(i);
-            if (entry.row.getParent() == null) {
-                // This view isn't even added, so the stack scroller doesn't
-                // know about it. Ignore completely.
-                continue;
-            }
+        ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
+        final int N = activeNotifications.size();
+        for (int i = 0; i < N; i++) {
+            Entry entry = activeNotifications.get(i);
             if (entry.row.getVisibility() != View.GONE &&
                     mNotificationData.isAmbient(entry.key)) {
                 speedbumpIndex = currentIndex;
@@ -1468,47 +1439,35 @@
         // TODO: Move this into updateNotificationIcons()?
         if (mNotificationIcons == null) return;
 
+        mNotificationData.filterAndSort();
+
         updateNotificationShade();
         updateNotificationIcons();
     }
 
-    /**
-     * Returns true if we're on a secure lockscreen and the user wants to hide "sensitive"
-     * notification data. If so, private notifications should show their (possibly
-     * auto-generated) publicVersion, and secret notifications should be totally invisible.
-     */
-    private boolean shouldHideSensitiveContents(int userid) {
-        return isLockscreenPublicMode() && !userAllowsPrivateNotificationsInPublic(userid);
-    }
-
     private void updateNotificationIcons() {
         final LinearLayout.LayoutParams params
             = new LinearLayout.LayoutParams(mIconSize + 2*mIconHPadding, mNaturalBarHeight);
 
-        int N = mNotificationData.size();
+        ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
+        final int N = activeNotifications.size();
+        ArrayList<StatusBarIconView> toShow = new ArrayList<>(N);
 
-        if (DEBUG) {
-            Log.d(TAG, "refreshing icons: " + N + " notifications, mNotificationIcons=" +
-                    mNotificationIcons);
-        }
-
-        ArrayList<View> toShow = new ArrayList<View>();
-
-        final boolean provisioned = isDeviceProvisioned();
-        // If the device hasn't been through Setup, we only show system notifications
-        for (int i=0; i<N; i++) {
-            Entry ent = mNotificationData.get(i);
-            if (!((provisioned && ent.notification.getScore() >= HIDE_ICONS_BELOW_SCORE)
-                    || showNotificationEvenIfUnprovisioned(ent.notification))) continue;
-            if (!notificationIsForCurrentProfiles(ent.notification)) continue;
-            if (ent.notification.getNotification().visibility == Notification.VISIBILITY_SECRET
-                    && shouldHideSensitiveContents(ent.notification.getUserId())) {
-                // in "public" mode (atop a secure keyguard), secret notifs are totally hidden
+        // Filter out notifications with low scores.
+        for (int i = 0; i < N; i++) {
+            Entry ent = activeNotifications.get(i);
+            if (ent.notification.getScore() < HIDE_ICONS_BELOW_SCORE &&
+                    !NotificationData.showNotificationEvenIfUnprovisioned(ent.notification)) {
                 continue;
             }
             toShow.add(ent.icon);
         }
 
+        if (DEBUG) {
+            Log.d(TAG, "refreshing icons: " + toShow.size() +
+                    " notifications, mNotificationIcons=" + mNotificationIcons);
+        }
+
         ArrayList<View> toRemove = new ArrayList<View>();
         for (int i=0; i<mNotificationIcons.getChildCount(); i++) {
             View child = mNotificationIcons.getChildAt(i);
@@ -1584,17 +1543,17 @@
 
     @Override
     protected void setAreThereNotifications() {
-        final boolean any = mNotificationData.size() > 0;
-
-        final boolean clearable = any && mNotificationData.hasClearableItems();
 
         if (SPEW) {
-            Log.d(TAG, "setAreThereNotifications: N=" + mNotificationData.size()
-                    + " any=" + any + " clearable=" + clearable);
+            final boolean clearable = hasActiveNotifications() &&
+                    mNotificationData.hasActiveClearableNotifications();
+            Log.d(TAG, "setAreThereNotifications: N=" +
+                    mNotificationData.getActiveNotifications().size() + " any=" +
+                    hasActiveNotifications() + " clearable=" + clearable);
         }
 
         final View nlo = mStatusBarView.findViewById(R.id.notification_lights_out);
-        final boolean showDot = (any&&!areLightsOn());
+        final boolean showDot = hasActiveNotifications() && !areLightsOn();
         if (showDot != (nlo.getAlpha() == 1.0f)) {
             if (showDot) {
                 nlo.setAlpha(0f);
@@ -1616,20 +1575,18 @@
         findAndUpdateMediaNotifications();
 
         updateCarrierLabelVisibility(false);
-
-        // TODO: Multiuser handling!
-        mHasNotifications = any;
     }
 
     public void findAndUpdateMediaNotifications() {
         boolean metaDataChanged = false;
 
         synchronized (mNotificationData) {
-            final int N = mNotificationData.size();
+            ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
+            final int N = activeNotifications.size();
             Entry mediaNotification = null;
             MediaController controller = null;
-            for (int i=0; i<N; i++) {
-                final Entry entry = mNotificationData.get(i);
+            for (int i = 0; i < N; i++) {
+                final Entry entry = activeNotifications.get(i);
                 if (isMediaNotification(entry)) {
                     final MediaSession.Token token = entry.notification.getNotification().extras
                             .getParcelable(Notification.EXTRA_MEDIA_SESSION);
@@ -1667,7 +1624,7 @@
                                 final String pkg = aController.getPackageName();
 
                                 for (int i = 0; i < N; i++) {
-                                    final Entry entry = mNotificationData.get(i);
+                                    final Entry entry = activeNotifications.get(i);
                                     if (entry.notification.getPackageName().equals(pkg)) {
                                         if (DEBUG_MEDIA) {
                                             Log.v(TAG, "DEBUG_MEDIA: found controller matching "
@@ -2602,7 +2559,7 @@
         if (!isDeviceProvisioned()) return;
 
         // not for you
-        if (!notificationIsForCurrentProfiles(n)) return;
+        if (!isNotificationForCurrentProfiles(n)) return;
 
         // Show the ticker if one is requested. Also don't do this
         // until status bar window is attached to the window manager,
@@ -2753,16 +2710,7 @@
 
         if (DUMPTRUCK) {
             synchronized (mNotificationData) {
-                int N = mNotificationData.size();
-                pw.println("  notification icons: " + N);
-                for (int i=0; i<N; i++) {
-                    NotificationData.Entry e = mNotificationData.get(i);
-                    pw.println("    [" + i + "] key=" + e.key + " icon=" + e.icon);
-                    StatusBarNotification n = e.notification;
-                    pw.println("         pkg=" + n.getPackageName() + " id=" + n.getId() + " score=" + n.getScore());
-                    pw.println("         notification=" + n.getNotification());
-                    pw.println("         tickerText=\"" + n.getNotification().tickerText + "\"");
-                }
+                mNotificationData.dump(pw, "  ");
             }
 
             int N = mStatusIcons.getChildCount();
@@ -3610,7 +3558,7 @@
 
     @Override
     public boolean onDraggedDown(View startingChild) {
-        if (mHasNotifications) {
+        if (hasActiveNotifications()) {
 
             // We have notifications, go to locked shade.
             goToLockedShade(startingChild);
@@ -3752,8 +3700,8 @@
         notifyUiVisibilityChanged(mSystemUiVisibility);
     }
 
-    public boolean hasNotifications() {
-        return mHasNotifications;
+    public boolean hasActiveNotifications() {
+        return !mNotificationData.getActiveNotifications().isEmpty();
     }
 
     private final class ShadeUpdates {
@@ -3762,8 +3710,9 @@
 
         public void check() {
             mNewVisibleNotifications.clear();
-            for (int i = 0; i < mNotificationData.size(); i++) {
-                final Entry entry = mNotificationData.get(i);
+            ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
+            for (int i = 0; i < activeNotifications.size(); i++) {
+                final Entry entry = activeNotifications.get(i);
                 final boolean visible = entry.row != null
                         && entry.row.getVisibility() == View.VISIBLE;
                 if (visible) {