• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.statusbar.notification.collection.legacy;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.app.Notification;
22 import android.service.notification.StatusBarNotification;
23 import android.util.ArraySet;
24 import android.util.Log;
25 
26 import com.android.systemui.Dumpable;
27 import com.android.systemui.dagger.SysUISingleton;
28 import com.android.systemui.plugins.statusbar.StatusBarStateController;
29 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
30 import com.android.systemui.statusbar.StatusBarState;
31 import com.android.systemui.statusbar.notification.collection.ListEntry;
32 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
33 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
34 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
35 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
36 import com.android.systemui.statusbar.phone.StatusBar;
37 import com.android.systemui.statusbar.policy.HeadsUpManager;
38 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
39 import com.android.wm.shell.bubbles.Bubbles;
40 
41 import java.io.FileDescriptor;
42 import java.io.PrintWriter;
43 import java.util.ArrayList;
44 import java.util.HashMap;
45 import java.util.HashSet;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.Objects;
49 import java.util.Optional;
50 import java.util.TreeSet;
51 
52 import javax.inject.Inject;
53 
54 import dagger.Lazy;
55 
56 /**
57  * A class to handle notifications and their corresponding groups.
58  * This includes:
59  * 1. Determining whether an entry is a member of a group and whether it is a summary or a child
60  * 2. Tracking group expansion states
61  */
62 @SysUISingleton
63 public class NotificationGroupManagerLegacy implements OnHeadsUpChangedListener, StateListener,
64         GroupMembershipManager, GroupExpansionManager, Dumpable {
65 
66     private static final String TAG = "NotifGroupManager";
67     private static final boolean DEBUG = StatusBar.DEBUG;
68     private static final boolean SPEW = StatusBar.SPEW;
69     /**
70      * The maximum amount of time (in ms) between the posting of notifications that can be
71      * considered part of the same update batch.
72      */
73     private static final long POST_BATCH_MAX_AGE = 5000;
74     private final HashMap<String, NotificationGroup> mGroupMap = new HashMap<>();
75     private final ArraySet<OnGroupExpansionChangeListener> mExpansionChangeListeners =
76             new ArraySet<>();
77     private final ArraySet<OnGroupChangeListener> mGroupChangeListeners = new ArraySet<>();
78     private final Lazy<PeopleNotificationIdentifier> mPeopleNotificationIdentifier;
79     private final Optional<Bubbles> mBubblesOptional;
80     private final EventBuffer mEventBuffer = new EventBuffer();
81     private int mBarState = -1;
82     private HashMap<String, StatusBarNotification> mIsolatedEntries = new HashMap<>();
83     private HeadsUpManager mHeadsUpManager;
84     private boolean mIsUpdatingUnchangedGroup;
85 
86     @Inject
NotificationGroupManagerLegacy( StatusBarStateController statusBarStateController, Lazy<PeopleNotificationIdentifier> peopleNotificationIdentifier, Optional<Bubbles> bubblesOptional)87     public NotificationGroupManagerLegacy(
88             StatusBarStateController statusBarStateController,
89             Lazy<PeopleNotificationIdentifier> peopleNotificationIdentifier,
90             Optional<Bubbles> bubblesOptional) {
91         statusBarStateController.addCallback(this);
92         mPeopleNotificationIdentifier = peopleNotificationIdentifier;
93         mBubblesOptional = bubblesOptional;
94     }
95 
96     /**
97      * Add a listener for changes to groups.
98      */
registerGroupChangeListener(OnGroupChangeListener listener)99     public void registerGroupChangeListener(OnGroupChangeListener listener) {
100         mGroupChangeListeners.add(listener);
101     }
102 
103     @Override
registerGroupExpansionChangeListener(OnGroupExpansionChangeListener listener)104     public void registerGroupExpansionChangeListener(OnGroupExpansionChangeListener listener) {
105         mExpansionChangeListeners.add(listener);
106     }
107 
108     @Override
isGroupExpanded(NotificationEntry entry)109     public boolean isGroupExpanded(NotificationEntry entry) {
110         NotificationGroup group = mGroupMap.get(getGroupKey(entry.getSbn()));
111         if (group == null) {
112             return false;
113         }
114         return group.expanded;
115     }
116 
117     /**
118      * @return if the group that this notification is associated with logically is expanded
119      */
isLogicalGroupExpanded(StatusBarNotification sbn)120     public boolean isLogicalGroupExpanded(StatusBarNotification sbn) {
121         NotificationGroup group = mGroupMap.get(sbn.getGroupKey());
122         if (group == null) {
123             return false;
124         }
125         return group.expanded;
126     }
127 
128     @Override
setGroupExpanded(NotificationEntry entry, boolean expanded)129     public void setGroupExpanded(NotificationEntry entry, boolean expanded) {
130         NotificationGroup group = mGroupMap.get(getGroupKey(entry.getSbn()));
131         if (group == null) {
132             return;
133         }
134         setGroupExpanded(group, expanded);
135     }
136 
setGroupExpanded(NotificationGroup group, boolean expanded)137     private void setGroupExpanded(NotificationGroup group, boolean expanded) {
138         group.expanded = expanded;
139         if (group.summary != null) {
140             for (OnGroupExpansionChangeListener listener : mExpansionChangeListeners) {
141                 listener.onGroupExpansionChange(group.summary.getRow(), expanded);
142             }
143         }
144     }
145 
146     /**
147      * When we want to remove an entry from being tracked for grouping
148      */
onEntryRemoved(NotificationEntry removed)149     public void onEntryRemoved(NotificationEntry removed) {
150         if (SPEW) {
151             Log.d(TAG, "onEntryRemoved: entry=" + removed);
152         }
153         onEntryRemovedInternal(removed, removed.getSbn());
154         StatusBarNotification oldSbn = mIsolatedEntries.remove(removed.getKey());
155         if (oldSbn != null) {
156             updateSuppression(mGroupMap.get(oldSbn.getGroupKey()));
157         }
158     }
159 
160     /**
161      * An entry was removed.
162      *
163      * @param removed the removed entry
164      * @param sbn the notification the entry has, which doesn't need to be the same as it's internal
165      *            notification
166      */
onEntryRemovedInternal(NotificationEntry removed, final StatusBarNotification sbn)167     private void onEntryRemovedInternal(NotificationEntry removed,
168             final StatusBarNotification sbn) {
169         onEntryRemovedInternal(removed, sbn.getGroupKey(), sbn.isGroup(),
170                 sbn.getNotification().isGroupSummary());
171     }
172 
onEntryRemovedInternal(NotificationEntry removed, String notifGroupKey, boolean isGroup, boolean isGroupSummary)173     private void onEntryRemovedInternal(NotificationEntry removed, String notifGroupKey, boolean
174             isGroup, boolean isGroupSummary) {
175         String groupKey = getGroupKey(removed.getKey(), notifGroupKey);
176         final NotificationGroup group = mGroupMap.get(groupKey);
177         if (group == null) {
178             // When an app posts 2 different notifications as summary of the same group, then a
179             // cancellation of the first notification removes this group.
180             // This situation is not supported and we will not allow such notifications anymore in
181             // the close future. See b/23676310 for reference.
182             return;
183         }
184         if (SPEW) {
185             Log.d(TAG, "onEntryRemovedInternal: entry=" + removed + " group=" + group.groupKey);
186         }
187         if (isGroupChild(removed.getKey(), isGroup, isGroupSummary)) {
188             group.children.remove(removed.getKey());
189         } else {
190             group.summary = null;
191         }
192         updateSuppression(group);
193         if (group.children.isEmpty()) {
194             if (group.summary == null) {
195                 mGroupMap.remove(groupKey);
196                 for (OnGroupChangeListener listener : mGroupChangeListeners) {
197                     listener.onGroupRemoved(group, groupKey);
198                 }
199             }
200         }
201     }
202 
203     /**
204      * Notify the group manager that a new entry was added
205      */
onEntryAdded(final NotificationEntry added)206     public void onEntryAdded(final NotificationEntry added) {
207         if (SPEW) {
208             Log.d(TAG, "onEntryAdded: entry=" + added);
209         }
210         updateIsolation(added);
211         onEntryAddedInternal(added);
212     }
213 
onEntryAddedInternal(final NotificationEntry added)214     private void onEntryAddedInternal(final NotificationEntry added) {
215         if (added.isRowRemoved()) {
216             added.setDebugThrowable(new Throwable());
217         }
218         final StatusBarNotification sbn = added.getSbn();
219         boolean isGroupChild = isGroupChild(sbn);
220         String groupKey = getGroupKey(sbn);
221         NotificationGroup group = mGroupMap.get(groupKey);
222         if (group == null) {
223             group = new NotificationGroup(groupKey);
224             mGroupMap.put(groupKey, group);
225 
226             for (OnGroupChangeListener listener : mGroupChangeListeners) {
227                 listener.onGroupCreated(group, groupKey);
228             }
229         }
230         if (SPEW) {
231             Log.d(TAG, "onEntryAddedInternal: entry=" + added + " group=" + group.groupKey);
232         }
233         if (isGroupChild) {
234             NotificationEntry existing = group.children.get(added.getKey());
235             if (existing != null && existing != added) {
236                 Throwable existingThrowable = existing.getDebugThrowable();
237                 Log.wtf(TAG, "Inconsistent entries found with the same key " + added.getKey()
238                         + "existing removed: " + existing.isRowRemoved()
239                         + (existingThrowable != null
240                                 ? Log.getStackTraceString(existingThrowable) + "\n" : "")
241                         + " added removed" + added.isRowRemoved(), new Throwable());
242             }
243             group.children.put(added.getKey(), added);
244             addToPostBatchHistory(group, added);
245             updateSuppression(group);
246         } else {
247             group.summary = added;
248             addToPostBatchHistory(group, added);
249             group.expanded = added.areChildrenExpanded();
250             updateSuppression(group);
251             if (!group.children.isEmpty()) {
252                 ArrayList<NotificationEntry> childrenCopy =
253                         new ArrayList<>(group.children.values());
254                 for (NotificationEntry child : childrenCopy) {
255                     onEntryBecomingChild(child);
256                 }
257                 for (OnGroupChangeListener listener : mGroupChangeListeners) {
258                     listener.onGroupCreatedFromChildren(group);
259                 }
260             }
261         }
262     }
263 
addToPostBatchHistory(NotificationGroup group, @Nullable NotificationEntry entry)264     private void addToPostBatchHistory(NotificationGroup group, @Nullable NotificationEntry entry) {
265         if (entry == null) {
266             return;
267         }
268         boolean didAdd = group.postBatchHistory.add(new PostRecord(entry));
269         if (didAdd) {
270             trimPostBatchHistory(group.postBatchHistory);
271         }
272     }
273 
274     /** remove all history that's too old to be in the batch. */
trimPostBatchHistory(@onNull TreeSet<PostRecord> postBatchHistory)275     private void trimPostBatchHistory(@NonNull TreeSet<PostRecord> postBatchHistory) {
276         if (postBatchHistory.size() <= 1) {
277             return;
278         }
279         long batchStartTime = postBatchHistory.last().postTime - POST_BATCH_MAX_AGE;
280         while (!postBatchHistory.isEmpty() && postBatchHistory.first().postTime < batchStartTime) {
281             postBatchHistory.pollFirst();
282         }
283     }
284 
onEntryBecomingChild(NotificationEntry entry)285     private void onEntryBecomingChild(NotificationEntry entry) {
286         updateIsolation(entry);
287     }
288 
updateSuppression(NotificationGroup group)289     private void updateSuppression(NotificationGroup group) {
290         if (group == null) {
291             return;
292         }
293         NotificationEntry prevAlertOverride = group.alertOverride;
294         group.alertOverride = getPriorityConversationAlertOverride(group);
295 
296         int childCount = 0;
297         boolean hasBubbles = false;
298         for (NotificationEntry entry : group.children.values()) {
299             if (mBubblesOptional.isPresent() && mBubblesOptional.get()
300                     .isBubbleNotificationSuppressedFromShade(
301                             entry.getKey(), entry.getSbn().getGroupKey())) {
302                 hasBubbles = true;
303             } else {
304                 childCount++;
305             }
306         }
307 
308         boolean prevSuppressed = group.suppressed;
309         group.suppressed = group.summary != null && !group.expanded
310                 && (childCount == 1
311                 || (childCount == 0
312                 && group.summary.getSbn().getNotification().isGroupSummary()
313                 && (hasIsolatedChildren(group) || hasBubbles)));
314 
315         boolean alertOverrideChanged = prevAlertOverride != group.alertOverride;
316         boolean suppressionChanged = prevSuppressed != group.suppressed;
317         if (alertOverrideChanged || suppressionChanged) {
318             if (DEBUG && alertOverrideChanged) {
319                 Log.d(TAG, "updateSuppression: alertOverride was=" + prevAlertOverride
320                         + " now=" + group.alertOverride + " group:\n" + group);
321             }
322             if (DEBUG && suppressionChanged) {
323                 Log.d(TAG,
324                         "updateSuppression: suppressed changed to " + group.suppressed
325                                 + " group:\n" + group);
326             }
327             if (!mIsUpdatingUnchangedGroup) {
328                 if (alertOverrideChanged) {
329                     mEventBuffer.notifyAlertOverrideChanged(group, prevAlertOverride);
330                 }
331                 if (suppressionChanged) {
332                     for (OnGroupChangeListener listener : mGroupChangeListeners) {
333                         listener.onGroupSuppressionChanged(group, group.suppressed);
334                     }
335                 }
336                 mEventBuffer.notifyGroupsChanged();
337             } else {
338                 if (DEBUG) {
339                     Log.d(TAG, group + " did not notify listeners of above change(s)");
340                 }
341             }
342         }
343     }
344 
345     /**
346      * Finds the isolated logical child of this group which is should be alerted instead.
347      *
348      * Notifications from priority conversations are isolated from their groups to make them more
349      * prominent, however apps may post these with a GroupAlertBehavior that has the group receiving
350      * the alert.  This would lead to the group alerting even though the conversation that was
351      * updated was not actually a part of that group.  This method finds the best priority
352      * conversation in this situation, if there is one, so they can be set as the alertOverride of
353      * the group.
354      *
355      * @param group the group to check
356      * @return the entry which should receive the alert instead of the group, if any.
357      */
358     @Nullable
getPriorityConversationAlertOverride(NotificationGroup group)359     private NotificationEntry getPriorityConversationAlertOverride(NotificationGroup group) {
360         // GOAL: if there is a priority child which wouldn't alert based on its groupAlertBehavior,
361         // but which should be alerting (because priority conversations are isolated), find it.
362         if (group == null || group.summary == null) {
363             if (SPEW) {
364                 Log.d(TAG, "getPriorityConversationAlertOverride: null group or summary");
365             }
366             return null;
367         }
368         if (isIsolated(group.summary.getKey())) {
369             if (SPEW) {
370                 Log.d(TAG, "getPriorityConversationAlertOverride: isolated group");
371             }
372             return null;
373         }
374 
375         // Precondiions:
376         // * Only necessary when all notifications in the group use GROUP_ALERT_SUMMARY
377         // * Only necessary when at least one notification in the group is on a priority channel
378         if (group.summary.getSbn().getNotification().getGroupAlertBehavior()
379                 != Notification.GROUP_ALERT_SUMMARY) {
380             if (SPEW) {
381                 Log.d(TAG, "getPriorityConversationAlertOverride: summary != GROUP_ALERT_SUMMARY");
382             }
383             return null;
384         }
385 
386         // Get the important children first, copy the keys for the final importance check,
387         // then add the non-isolated children to the map for unified lookup.
388         HashMap<String, NotificationEntry> children = getImportantConversations(group);
389         if (children == null || children.isEmpty()) {
390             if (SPEW) {
391                 Log.d(TAG, "getPriorityConversationAlertOverride: no important conversations");
392             }
393             return null;
394         }
395         HashSet<String> importantChildKeys = new HashSet<>(children.keySet());
396         children.putAll(group.children);
397 
398         // Ensure all children have GROUP_ALERT_SUMMARY
399         for (NotificationEntry child : children.values()) {
400             if (child.getSbn().getNotification().getGroupAlertBehavior()
401                     != Notification.GROUP_ALERT_SUMMARY) {
402                 if (SPEW) {
403                     Log.d(TAG, "getPriorityConversationAlertOverride: "
404                             + "child != GROUP_ALERT_SUMMARY");
405                 }
406                 return null;
407             }
408         }
409 
410         // Create a merged post history from all the children
411         TreeSet<PostRecord> combinedHistory = new TreeSet<>(group.postBatchHistory);
412         for (String importantChildKey : importantChildKeys) {
413             NotificationGroup importantChildGroup = mGroupMap.get(importantChildKey);
414             combinedHistory.addAll(importantChildGroup.postBatchHistory);
415         }
416         trimPostBatchHistory(combinedHistory);
417 
418         // This is a streamlined implementation of the following idea:
419         // * From the subset of notifications in the latest 'batch' of updates.  A batch is:
420         //   * Notifs posted less than POST_BATCH_MAX_AGE before the most recently posted.
421         //   * Only including notifs newer than the second-to-last post of any notification.
422         // * Find the newest child in the batch -- the with the largest 'when' value.
423         // * If the newest child is a priority conversation, set that as the override.
424         HashSet<String> batchKeys = new HashSet<>();
425         long newestChildWhen = -1;
426         NotificationEntry newestChild = null;
427         // Iterate backwards through the post history, tracking the child with the smallest sort key
428         for (PostRecord record : combinedHistory.descendingSet()) {
429             if (batchKeys.contains(record.key)) {
430                 // Once you see a notification again, the batch has ended
431                 break;
432             }
433             batchKeys.add(record.key);
434             NotificationEntry child = children.get(record.key);
435             if (child != null) {
436                 long childWhen = child.getSbn().getNotification().when;
437                 if (newestChild == null || childWhen > newestChildWhen) {
438                     newestChildWhen = childWhen;
439                     newestChild = child;
440                 }
441             }
442         }
443         if (newestChild != null && importantChildKeys.contains(newestChild.getKey())) {
444             if (SPEW) {
445                 Log.d(TAG, "getPriorityConversationAlertOverride: result=" + newestChild);
446             }
447             return newestChild;
448         }
449         if (SPEW) {
450             Log.d(TAG, "getPriorityConversationAlertOverride: result=null, newestChild="
451                     + newestChild);
452         }
453         return null;
454     }
455 
hasIsolatedChildren(NotificationGroup group)456     private boolean hasIsolatedChildren(NotificationGroup group) {
457         return getNumberOfIsolatedChildren(group.summary.getSbn().getGroupKey()) != 0;
458     }
459 
getNumberOfIsolatedChildren(String groupKey)460     private int getNumberOfIsolatedChildren(String groupKey) {
461         int count = 0;
462         for (StatusBarNotification sbn : mIsolatedEntries.values()) {
463             if (sbn.getGroupKey().equals(groupKey) && isIsolated(sbn.getKey())) {
464                 count++;
465             }
466         }
467         return count;
468     }
469 
470     @Nullable
getImportantConversations(NotificationGroup group)471     private HashMap<String, NotificationEntry> getImportantConversations(NotificationGroup group) {
472         String groupKey = group.summary.getSbn().getGroupKey();
473         HashMap<String, NotificationEntry> result = null;
474         for (StatusBarNotification sbn : mIsolatedEntries.values()) {
475             if (sbn.getGroupKey().equals(groupKey)) {
476                 NotificationEntry entry = mGroupMap.get(sbn.getKey()).summary;
477                 if (isImportantConversation(entry)) {
478                     if (result == null) {
479                         result = new HashMap<>();
480                     }
481                     result.put(sbn.getKey(), entry);
482                 }
483             }
484         }
485         return result;
486     }
487 
488     /**
489      * Update an entry's group information
490      * @param entry notification entry to update
491      * @param oldNotification previous notification info before this update
492      */
onEntryUpdated(NotificationEntry entry, StatusBarNotification oldNotification)493     public void onEntryUpdated(NotificationEntry entry, StatusBarNotification oldNotification) {
494         if (SPEW) {
495             Log.d(TAG, "onEntryUpdated: entry=" + entry);
496         }
497         onEntryUpdated(entry, oldNotification.getGroupKey(), oldNotification.isGroup(),
498                 oldNotification.getNotification().isGroupSummary());
499     }
500 
501     /**
502      * Updates an entry's group information
503      * @param entry notification entry to update
504      * @param oldGroupKey the notification's previous group key before this update
505      * @param oldIsGroup whether this notification was a group before this update
506      * @param oldIsGroupSummary whether this notification was a group summary before this update
507      */
onEntryUpdated(NotificationEntry entry, String oldGroupKey, boolean oldIsGroup, boolean oldIsGroupSummary)508     public void onEntryUpdated(NotificationEntry entry, String oldGroupKey, boolean oldIsGroup,
509             boolean oldIsGroupSummary) {
510         String newGroupKey = entry.getSbn().getGroupKey();
511         boolean groupKeysChanged = !oldGroupKey.equals(newGroupKey);
512         boolean wasGroupChild = isGroupChild(entry.getKey(), oldIsGroup, oldIsGroupSummary);
513         boolean isGroupChild = isGroupChild(entry.getSbn());
514         mIsUpdatingUnchangedGroup = !groupKeysChanged && wasGroupChild == isGroupChild;
515         if (mGroupMap.get(getGroupKey(entry.getKey(), oldGroupKey)) != null) {
516             onEntryRemovedInternal(entry, oldGroupKey, oldIsGroup, oldIsGroupSummary);
517         }
518         onEntryAddedInternal(entry);
519         mIsUpdatingUnchangedGroup = false;
520         if (isIsolated(entry.getSbn().getKey())) {
521             mIsolatedEntries.put(entry.getKey(), entry.getSbn());
522             if (groupKeysChanged) {
523                 updateSuppression(mGroupMap.get(oldGroupKey));
524                 updateSuppression(mGroupMap.get(newGroupKey));
525             }
526         } else if (!wasGroupChild && isGroupChild) {
527             onEntryBecomingChild(entry);
528         }
529     }
530 
531     /**
532      * Whether the given notification is the summary of a group that is being suppressed
533      */
isSummaryOfSuppressedGroup(StatusBarNotification sbn)534     public boolean isSummaryOfSuppressedGroup(StatusBarNotification sbn) {
535         return sbn.getNotification().isGroupSummary() && isGroupSuppressed(getGroupKey(sbn));
536     }
537 
538     /**
539      * If the given notification is a summary, get the group for it.
540      */
getGroupForSummary(StatusBarNotification sbn)541     public NotificationGroup getGroupForSummary(StatusBarNotification sbn) {
542         if (sbn.getNotification().isGroupSummary()) {
543             return mGroupMap.get(getGroupKey(sbn));
544         }
545         return null;
546     }
547 
isOnlyChild(StatusBarNotification sbn)548     private boolean isOnlyChild(StatusBarNotification sbn) {
549         return !sbn.getNotification().isGroupSummary()
550                 && getTotalNumberOfChildren(sbn) == 1;
551     }
552 
553     @Override
isOnlyChildInGroup(NotificationEntry entry)554     public boolean isOnlyChildInGroup(NotificationEntry entry) {
555         final StatusBarNotification sbn = entry.getSbn();
556         if (!isOnlyChild(sbn)) {
557             return false;
558         }
559         NotificationEntry logicalGroupSummary = getLogicalGroupSummary(entry);
560         return logicalGroupSummary != null && !logicalGroupSummary.getSbn().equals(sbn);
561     }
562 
getTotalNumberOfChildren(StatusBarNotification sbn)563     private int getTotalNumberOfChildren(StatusBarNotification sbn) {
564         int isolatedChildren = getNumberOfIsolatedChildren(sbn.getGroupKey());
565         NotificationGroup group = mGroupMap.get(sbn.getGroupKey());
566         int realChildren = group != null ? group.children.size() : 0;
567         return isolatedChildren + realChildren;
568     }
569 
isGroupSuppressed(String groupKey)570     private boolean isGroupSuppressed(String groupKey) {
571         NotificationGroup group = mGroupMap.get(groupKey);
572         return group != null && group.suppressed;
573     }
574 
setStatusBarState(int newState)575     private void setStatusBarState(int newState) {
576         mBarState = newState;
577         if (mBarState == StatusBarState.KEYGUARD) {
578             collapseGroups();
579         }
580     }
581 
582     @Override
collapseGroups()583     public void collapseGroups() {
584         // Because notifications can become isolated when the group becomes suppressed it can
585         // lead to concurrent modifications while looping. We need to make a copy.
586         ArrayList<NotificationGroup> groupCopy = new ArrayList<>(mGroupMap.values());
587         int size = groupCopy.size();
588         for (int i = 0; i < size; i++) {
589             NotificationGroup group =  groupCopy.get(i);
590             if (group.expanded) {
591                 setGroupExpanded(group, false);
592             }
593             updateSuppression(group);
594         }
595     }
596 
597     @Override
isChildInGroup(NotificationEntry entry)598     public boolean isChildInGroup(NotificationEntry entry) {
599         final StatusBarNotification sbn = entry.getSbn();
600         if (!isGroupChild(sbn)) {
601             return false;
602         }
603         NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
604         if (group == null || group.summary == null || group.suppressed) {
605             return false;
606         }
607         if (group.children.isEmpty()) {
608             // If the suppression of a group changes because the last child was removed, this can
609             // still be called temporarily because the child hasn't been fully removed yet. Let's
610             // make sure we still return false in that case.
611             return false;
612         }
613         return true;
614     }
615 
616     @Override
isGroupSummary(NotificationEntry entry)617     public boolean isGroupSummary(NotificationEntry entry) {
618         final StatusBarNotification sbn = entry.getSbn();
619         if (!isGroupSummary(sbn)) {
620             return false;
621         }
622         NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
623         if (group == null || group.summary == null) {
624             return false;
625         }
626         return !group.children.isEmpty() && Objects.equals(group.summary.getSbn(), sbn);
627     }
628 
629     @Override
getGroupSummary(NotificationEntry entry)630     public NotificationEntry getGroupSummary(NotificationEntry entry) {
631         return getGroupSummary(getGroupKey(entry.getSbn()));
632     }
633 
634     @Override
getLogicalGroupSummary(NotificationEntry entry)635     public NotificationEntry getLogicalGroupSummary(NotificationEntry entry) {
636         return getGroupSummary(entry.getSbn().getGroupKey());
637     }
638 
639     @Nullable
getGroupSummary(String groupKey)640     private NotificationEntry getGroupSummary(String groupKey) {
641         NotificationGroup group = mGroupMap.get(groupKey);
642         //TODO: see if this can become an Entry
643         return group == null ? null
644                 : group.summary;
645     }
646 
647     /**
648      * Get the children that are logically in the summary's group, whether or not they are isolated.
649      *
650      * @param summary summary of a group
651      * @return list of the children
652      */
getLogicalChildren(StatusBarNotification summary)653     public ArrayList<NotificationEntry> getLogicalChildren(StatusBarNotification summary) {
654         NotificationGroup group = mGroupMap.get(summary.getGroupKey());
655         if (group == null) {
656             return null;
657         }
658         ArrayList<NotificationEntry> children = new ArrayList<>(group.children.values());
659         for (StatusBarNotification sbn : mIsolatedEntries.values()) {
660             if (sbn.getGroupKey().equals(summary.getGroupKey())) {
661                 children.add(mGroupMap.get(sbn.getKey()).summary);
662             }
663         }
664         return children;
665     }
666 
667     @Override
getChildren(ListEntry listEntrySummary)668     public @Nullable List<NotificationEntry> getChildren(ListEntry listEntrySummary) {
669         NotificationEntry summary = listEntrySummary.getRepresentativeEntry();
670         NotificationGroup group = mGroupMap.get(summary.getSbn().getGroupKey());
671         if (group == null) {
672             return null;
673         }
674         return new ArrayList<>(group.children.values());
675     }
676 
677     /**
678      * If there is a {@link NotificationGroup} associated with the provided entry, this method
679      * will update the suppression of that group.
680      */
updateSuppression(NotificationEntry entry)681     public void updateSuppression(NotificationEntry entry) {
682         NotificationGroup group = mGroupMap.get(getGroupKey(entry.getSbn()));
683         if (group != null) {
684             updateSuppression(group);
685         }
686     }
687 
688     /**
689      * Get the group key. May differ from the one in the notification due to the notification
690      * being temporarily isolated.
691      *
692      * @param sbn notification to check
693      * @return the key of the notification
694      */
getGroupKey(StatusBarNotification sbn)695     public String getGroupKey(StatusBarNotification sbn) {
696         return getGroupKey(sbn.getKey(), sbn.getGroupKey());
697     }
698 
getGroupKey(String key, String groupKey)699     private String getGroupKey(String key, String groupKey) {
700         if (isIsolated(key)) {
701             return key;
702         }
703         return groupKey;
704     }
705 
706     @Override
toggleGroupExpansion(NotificationEntry entry)707     public boolean toggleGroupExpansion(NotificationEntry entry) {
708         NotificationGroup group = mGroupMap.get(getGroupKey(entry.getSbn()));
709         if (group == null) {
710             return false;
711         }
712         setGroupExpanded(group, !group.expanded);
713         return group.expanded;
714     }
715 
isIsolated(String sbnKey)716     private boolean isIsolated(String sbnKey) {
717         return mIsolatedEntries.containsKey(sbnKey);
718     }
719 
720     /**
721      * Is this notification the summary of a group?
722      */
isGroupSummary(StatusBarNotification sbn)723     public boolean isGroupSummary(StatusBarNotification sbn) {
724         if (isIsolated(sbn.getKey())) {
725             return true;
726         }
727         return sbn.getNotification().isGroupSummary();
728     }
729 
730     /**
731      * Whether a notification is visually a group child.
732      *
733      * @param sbn notification to check
734      * @return true if it is visually a group child
735      */
isGroupChild(StatusBarNotification sbn)736     public boolean isGroupChild(StatusBarNotification sbn) {
737         return isGroupChild(sbn.getKey(), sbn.isGroup(), sbn.getNotification().isGroupSummary());
738     }
739 
isGroupChild(String key, boolean isGroup, boolean isGroupSummary)740     private boolean isGroupChild(String key, boolean isGroup, boolean isGroupSummary) {
741         if (isIsolated(key)) {
742             return false;
743         }
744         return isGroup && !isGroupSummary;
745     }
746 
747     @Override
onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp)748     public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) {
749         updateIsolation(entry);
750     }
751 
752     /**
753      * Whether a notification that is normally part of a group should be temporarily isolated from
754      * the group and put in their own group visually.  This generally happens when the notification
755      * is alerting.
756      *
757      * @param entry the notification to check
758      * @return true if the entry should be isolated
759      */
shouldIsolate(NotificationEntry entry)760     private boolean shouldIsolate(NotificationEntry entry) {
761         StatusBarNotification sbn = entry.getSbn();
762         if (!sbn.isGroup() || sbn.getNotification().isGroupSummary()) {
763             return false;
764         }
765         if (isImportantConversation(entry)) {
766             return true;
767         }
768         if (mHeadsUpManager != null && !mHeadsUpManager.isAlerting(entry.getKey())) {
769             return false;
770         }
771         NotificationGroup notificationGroup = mGroupMap.get(sbn.getGroupKey());
772         return (sbn.getNotification().fullScreenIntent != null
773                     || notificationGroup == null
774                     || !notificationGroup.expanded
775                     || isGroupNotFullyVisible(notificationGroup));
776     }
777 
isImportantConversation(NotificationEntry entry)778     private boolean isImportantConversation(NotificationEntry entry) {
779         int peopleNotificationType =
780                 mPeopleNotificationIdentifier.get().getPeopleNotificationType(entry);
781         return peopleNotificationType == PeopleNotificationIdentifier.TYPE_IMPORTANT_PERSON;
782     }
783 
784     /**
785      * Isolate a notification from its group so that it visually shows as its own group.
786      *
787      * @param entry the notification to isolate
788      */
isolateNotification(NotificationEntry entry)789     private void isolateNotification(NotificationEntry entry) {
790         if (SPEW) {
791             Log.d(TAG, "isolateNotification: entry=" + entry);
792         }
793         // We will be isolated now, so lets update the groups
794         onEntryRemovedInternal(entry, entry.getSbn());
795 
796         mIsolatedEntries.put(entry.getKey(), entry.getSbn());
797 
798         onEntryAddedInternal(entry);
799         // We also need to update the suppression of the old group, because this call comes
800         // even before the groupManager knows about the notification at all.
801         // When the notification gets added afterwards it is already isolated and therefore
802         // it doesn't lead to an update.
803         updateSuppression(mGroupMap.get(entry.getSbn().getGroupKey()));
804         for (OnGroupChangeListener listener : mGroupChangeListeners) {
805             listener.onGroupsChanged();
806         }
807     }
808 
809     /**
810      * Update the isolation of an entry, splitting it from the group.
811      */
updateIsolation(NotificationEntry entry)812     public void updateIsolation(NotificationEntry entry) {
813         // We need to buffer a few events because we do isolation changes in 3 steps:
814         // removeInternal, update mIsolatedEntries, addInternal.  This means that often the
815         // alertOverride will update on the removal, however processing the event in that case can
816         // cause problems because the mIsolatedEntries map is not in its final state, so the event
817         // listener may be unable to correctly determine the true state of the group.  By delaying
818         // the alertOverride change until after the add phase, we can ensure that listeners only
819         // have to handle a consistent state.
820         mEventBuffer.startBuffering();
821         boolean isIsolated = isIsolated(entry.getSbn().getKey());
822         if (shouldIsolate(entry)) {
823             if (!isIsolated) {
824                 isolateNotification(entry);
825             }
826         } else if (isIsolated) {
827             stopIsolatingNotification(entry);
828         }
829         mEventBuffer.flushAndStopBuffering();
830     }
831 
832     /**
833      * Stop isolating a notification and re-group it with its original logical group.
834      *
835      * @param entry the notification to un-isolate
836      */
stopIsolatingNotification(NotificationEntry entry)837     private void stopIsolatingNotification(NotificationEntry entry) {
838         if (SPEW) {
839             Log.d(TAG, "stopIsolatingNotification: entry=" + entry);
840         }
841         // not isolated anymore, we need to update the groups
842         onEntryRemovedInternal(entry, entry.getSbn());
843         mIsolatedEntries.remove(entry.getKey());
844         onEntryAddedInternal(entry);
845         for (OnGroupChangeListener listener : mGroupChangeListeners) {
846             listener.onGroupsChanged();
847         }
848     }
849 
isGroupNotFullyVisible(NotificationGroup notificationGroup)850     private boolean isGroupNotFullyVisible(NotificationGroup notificationGroup) {
851         return notificationGroup.summary == null
852                 || notificationGroup.summary.isGroupNotFullyVisible();
853     }
854 
855     /**
856      * Directly set the heads up manager to avoid circular dependencies
857      */
setHeadsUpManager(HeadsUpManager headsUpManager)858     public void setHeadsUpManager(HeadsUpManager headsUpManager) {
859         mHeadsUpManager = headsUpManager;
860     }
861 
862     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)863     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
864         pw.println("GroupManagerLegacy state:");
865         pw.println("  number of groups: " +  mGroupMap.size());
866         for (Map.Entry<String, NotificationGroup>  entry : mGroupMap.entrySet()) {
867             pw.println("\n    key: " + entry.getKey()); pw.println(entry.getValue());
868         }
869         pw.println("\n    isolated entries: " +  mIsolatedEntries.size());
870         for (Map.Entry<String, StatusBarNotification> entry : mIsolatedEntries.entrySet()) {
871             pw.print("      "); pw.print(entry.getKey());
872             pw.print(", "); pw.println(entry.getValue());
873         }
874     }
875 
876     @Override
onStateChanged(int newState)877     public void onStateChanged(int newState) {
878         setStatusBarState(newState);
879     }
880 
881     /**
882      * A record of a notification being posted, containing the time of the post and the key of the
883      * notification entry.  These are stored in a TreeSet by the NotificationGroup and used to
884      * calculate a batch of notifications.
885      */
886     public static class PostRecord implements Comparable<PostRecord> {
887         public final long postTime;
888         public final String key;
889 
890         /** constructs a record containing the post time and key from the notification entry */
PostRecord(@onNull NotificationEntry entry)891         public PostRecord(@NonNull NotificationEntry entry) {
892             this.postTime = entry.getSbn().getPostTime();
893             this.key = entry.getKey();
894         }
895 
896         @Override
compareTo(PostRecord o)897         public int compareTo(PostRecord o) {
898             int postTimeComparison = Long.compare(this.postTime, o.postTime);
899             return postTimeComparison == 0
900                     ? String.CASE_INSENSITIVE_ORDER.compare(this.key, o.key)
901                     : postTimeComparison;
902         }
903 
904         @Override
equals(Object o)905         public boolean equals(Object o) {
906             if (this == o) return true;
907             if (o == null || getClass() != o.getClass()) return false;
908             PostRecord that = (PostRecord) o;
909             return postTime == that.postTime && key.equals(that.key);
910         }
911 
912         @Override
hashCode()913         public int hashCode() {
914             return Objects.hash(postTime, key);
915         }
916     }
917 
918     /**
919      * Represents a notification group in the notification shade.
920      */
921     public static class NotificationGroup {
922         public final String groupKey;
923         public final HashMap<String, NotificationEntry> children = new HashMap<>();
924         public final TreeSet<PostRecord> postBatchHistory = new TreeSet<>();
925         public NotificationEntry summary;
926         public boolean expanded;
927         /**
928          * Is this notification group suppressed, i.e its summary is hidden
929          */
930         public boolean suppressed;
931         /**
932          * The child (which is isolated from this group) to which the alert should be transferred,
933          * due to priority conversations.
934          */
935         public NotificationEntry alertOverride;
936 
NotificationGroup(String groupKey)937         NotificationGroup(String groupKey) {
938             this.groupKey = groupKey;
939         }
940 
941         @Override
toString()942         public String toString() {
943             StringBuilder sb = new StringBuilder();
944             sb.append("    groupKey: ").append(groupKey);
945             sb.append("\n    summary:");
946             appendEntry(sb, summary);
947             sb.append("\n    children size: ").append(children.size());
948             for (NotificationEntry child : children.values()) {
949                 appendEntry(sb, child);
950             }
951             sb.append("\n    alertOverride:");
952             appendEntry(sb, alertOverride);
953             sb.append("\n    summary suppressed: ").append(suppressed);
954             return sb.toString();
955         }
956 
appendEntry(StringBuilder sb, NotificationEntry entry)957         private void appendEntry(StringBuilder sb, NotificationEntry entry) {
958             sb.append("\n      ").append(entry != null ? entry.getSbn() : "null");
959             if (entry != null && entry.getDebugThrowable() != null) {
960                 sb.append(Log.getStackTraceString(entry.getDebugThrowable()));
961             }
962         }
963     }
964 
965     /**
966      * This class is a toggleable buffer for a subset of events of {@link OnGroupChangeListener}.
967      * When buffering, instead of notifying the listeners it will set internal state that will allow
968      * it to notify listeners of those events later
969      */
970     private class EventBuffer {
971         private final HashMap<String, NotificationEntry> mOldAlertOverrideByGroup = new HashMap<>();
972         private boolean mIsBuffering = false;
973         private boolean mDidGroupsChange = false;
974 
notifyAlertOverrideChanged(NotificationGroup group, NotificationEntry oldAlertOverride)975         void notifyAlertOverrideChanged(NotificationGroup group,
976                 NotificationEntry oldAlertOverride) {
977             if (mIsBuffering) {
978                 // The value in this map is the override before the event.  If there is an entry
979                 // already in the map, then we are effectively coalescing two events, which means
980                 // we need to preserve the original initial value.
981                 mOldAlertOverrideByGroup.putIfAbsent(group.groupKey, oldAlertOverride);
982             } else {
983                 for (OnGroupChangeListener listener : mGroupChangeListeners) {
984                     listener.onGroupAlertOverrideChanged(group, oldAlertOverride,
985                             group.alertOverride);
986                 }
987             }
988         }
989 
notifyGroupsChanged()990         void notifyGroupsChanged() {
991             if (mIsBuffering) {
992                 mDidGroupsChange = true;
993             } else {
994                 for (OnGroupChangeListener listener : mGroupChangeListeners) {
995                     listener.onGroupsChanged();
996                 }
997             }
998         }
999 
startBuffering()1000         void startBuffering() {
1001             mIsBuffering = true;
1002         }
1003 
flushAndStopBuffering()1004         void flushAndStopBuffering() {
1005             // stop buffering so that we can call our own helpers
1006             mIsBuffering = false;
1007             // alert all group alert override changes for groups that were not removed
1008             for (Map.Entry<String, NotificationEntry> entry : mOldAlertOverrideByGroup.entrySet()) {
1009                 NotificationGroup group = mGroupMap.get(entry.getKey());
1010                 if (group == null) {
1011                     // The group can be null if this alertOverride changed before the group was
1012                     // permanently removed, meaning that there's no guarantee that listeners will
1013                     // that field clear.
1014                     continue;
1015                 }
1016                 NotificationEntry oldAlertOverride = entry.getValue();
1017                 if (group.alertOverride == oldAlertOverride) {
1018                     // If the final alertOverride equals the initial, it means we coalesced two
1019                     // events which undid the change, so we can drop it entirely.
1020                     continue;
1021                 }
1022                 notifyAlertOverrideChanged(group, oldAlertOverride);
1023             }
1024             mOldAlertOverrideByGroup.clear();
1025             // alert that groups changed
1026             if (mDidGroupsChange) {
1027                 notifyGroupsChanged();
1028                 mDidGroupsChange = false;
1029             }
1030         }
1031     }
1032 
1033     /**
1034      * Listener for group changes not including group expansion changes which are handled by
1035      * {@link OnGroupExpansionChangeListener}.
1036      */
1037     public interface OnGroupChangeListener {
1038         /**
1039          * A new group has been created.
1040          *
1041          * @param group the group that was created
1042          * @param groupKey the group's key
1043          */
onGroupCreated( NotificationGroup group, String groupKey)1044         default void onGroupCreated(
1045                 NotificationGroup group,
1046                 String groupKey) {}
1047 
1048         /**
1049          * A group has been removed.
1050          *
1051          * @param group the group that was removed
1052          * @param groupKey the group's key
1053          */
onGroupRemoved( NotificationGroup group, String groupKey)1054         default void onGroupRemoved(
1055                 NotificationGroup group,
1056                 String groupKey) {}
1057 
1058         /**
1059          * The suppression of a group has changed.
1060          *
1061          * @param group the group that has changed
1062          * @param suppressed true if the group is now suppressed, false o/w
1063          */
onGroupSuppressionChanged( NotificationGroup group, boolean suppressed)1064         default void onGroupSuppressionChanged(
1065                 NotificationGroup group,
1066                 boolean suppressed) {}
1067 
1068         /**
1069          * The alert override of a group has changed.
1070          *
1071          * @param group the group that has changed
1072          * @param oldAlertOverride the previous notification to which the group's alerts were sent
1073          * @param newAlertOverride the notification to which the group's alerts should now be sent
1074          */
onGroupAlertOverrideChanged( NotificationGroup group, @Nullable NotificationEntry oldAlertOverride, @Nullable NotificationEntry newAlertOverride)1075         default void onGroupAlertOverrideChanged(
1076                 NotificationGroup group,
1077                 @Nullable NotificationEntry oldAlertOverride,
1078                 @Nullable NotificationEntry newAlertOverride) {}
1079 
1080         /**
1081          * A group of children just received a summary notification and should therefore become
1082          * children of it.
1083          *
1084          * @param group the group created
1085          */
onGroupCreatedFromChildren(NotificationGroup group)1086         default void onGroupCreatedFromChildren(NotificationGroup group) {}
1087 
1088         /**
1089          * The groups have changed. This can happen if the isolation of a child has changes or if a
1090          * group became suppressed / unsuppressed
1091          */
onGroupsChanged()1092         default void onGroupsChanged() {}
1093     }
1094 }
1095