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.phone;
18 
19 import android.annotation.Nullable;
20 import android.service.notification.StatusBarNotification;
21 import android.util.ArraySet;
22 import android.util.Log;
23 
24 import com.android.systemui.Dependency;
25 import com.android.systemui.bubbles.BubbleController;
26 import com.android.systemui.plugins.statusbar.StatusBarStateController;
27 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
28 import com.android.systemui.statusbar.StatusBarState;
29 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
30 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
31 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
32 import com.android.systemui.statusbar.policy.HeadsUpManager;
33 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
34 
35 import java.io.FileDescriptor;
36 import java.io.PrintWriter;
37 import java.util.ArrayList;
38 import java.util.HashMap;
39 import java.util.Map;
40 import java.util.Objects;
41 
42 import javax.inject.Inject;
43 import javax.inject.Singleton;
44 
45 import dagger.Lazy;
46 
47 /**
48  * A class to handle notifications and their corresponding groups.
49  */
50 @Singleton
51 public class NotificationGroupManager implements OnHeadsUpChangedListener, StateListener {
52 
53     private static final String TAG = "NotificationGroupManager";
54     private final HashMap<String, NotificationGroup> mGroupMap = new HashMap<>();
55     private final ArraySet<OnGroupChangeListener> mListeners = new ArraySet<>();
56     private final Lazy<PeopleNotificationIdentifier> mPeopleNotificationIdentifier;
57     private int mBarState = -1;
58     private HashMap<String, StatusBarNotification> mIsolatedEntries = new HashMap<>();
59     private HeadsUpManager mHeadsUpManager;
60     private boolean mIsUpdatingUnchangedGroup;
61     @Nullable private BubbleController mBubbleController = null;
62 
63     @Inject
NotificationGroupManager( StatusBarStateController statusBarStateController, Lazy<PeopleNotificationIdentifier> peopleNotificationIdentifier)64     public NotificationGroupManager(
65             StatusBarStateController statusBarStateController,
66             Lazy<PeopleNotificationIdentifier> peopleNotificationIdentifier) {
67         statusBarStateController.addCallback(this);
68         mPeopleNotificationIdentifier = peopleNotificationIdentifier;
69     }
70 
getBubbleController()71     private BubbleController getBubbleController() {
72         if (mBubbleController == null) {
73             mBubbleController = Dependency.get(BubbleController.class);
74         }
75         return mBubbleController;
76     }
77 
78     /**
79      * Add a listener for changes to groups.
80      *
81      * @param listener listener to add
82      */
addOnGroupChangeListener(OnGroupChangeListener listener)83     public void addOnGroupChangeListener(OnGroupChangeListener listener) {
84         mListeners.add(listener);
85     }
86 
isGroupExpanded(StatusBarNotification sbn)87     public boolean isGroupExpanded(StatusBarNotification sbn) {
88         NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
89         if (group == null) {
90             return false;
91         }
92         return group.expanded;
93     }
94 
95     /**
96      * @return if the group that this notification is associated with logically is expanded
97      */
isLogicalGroupExpanded(StatusBarNotification sbn)98     public boolean isLogicalGroupExpanded(StatusBarNotification sbn) {
99         NotificationGroup group = mGroupMap.get(sbn.getGroupKey());
100         if (group == null) {
101             return false;
102         }
103         return group.expanded;
104     }
105 
setGroupExpanded(StatusBarNotification sbn, boolean expanded)106     public void setGroupExpanded(StatusBarNotification sbn, boolean expanded) {
107         NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
108         if (group == null) {
109             return;
110         }
111         setGroupExpanded(group, expanded);
112     }
113 
setGroupExpanded(NotificationGroup group, boolean expanded)114     private void setGroupExpanded(NotificationGroup group, boolean expanded) {
115         group.expanded = expanded;
116         if (group.summary != null) {
117             for (OnGroupChangeListener listener : mListeners) {
118                 listener.onGroupExpansionChanged(group.summary.getRow(), expanded);
119             }
120         }
121     }
122 
onEntryRemoved(NotificationEntry removed)123     public void onEntryRemoved(NotificationEntry removed) {
124         onEntryRemovedInternal(removed, removed.getSbn());
125         mIsolatedEntries.remove(removed.getKey());
126     }
127 
128     /**
129      * An entry was removed.
130      *
131      * @param removed the removed entry
132      * @param sbn the notification the entry has, which doesn't need to be the same as it's internal
133      *            notification
134      */
onEntryRemovedInternal(NotificationEntry removed, final StatusBarNotification sbn)135     private void onEntryRemovedInternal(NotificationEntry removed,
136             final StatusBarNotification sbn) {
137         onEntryRemovedInternal(removed, sbn.getGroupKey(), sbn.isGroup(),
138                 sbn.getNotification().isGroupSummary());
139     }
140 
onEntryRemovedInternal(NotificationEntry removed, String notifGroupKey, boolean isGroup, boolean isGroupSummary)141     private void onEntryRemovedInternal(NotificationEntry removed, String notifGroupKey, boolean
142             isGroup, boolean isGroupSummary) {
143         String groupKey = getGroupKey(removed.getKey(), notifGroupKey);
144         final NotificationGroup group = mGroupMap.get(groupKey);
145         if (group == null) {
146             // When an app posts 2 different notifications as summary of the same group, then a
147             // cancellation of the first notification removes this group.
148             // This situation is not supported and we will not allow such notifications anymore in
149             // the close future. See b/23676310 for reference.
150             return;
151         }
152         if (isGroupChild(removed.getKey(), isGroup, isGroupSummary)) {
153             group.children.remove(removed.getKey());
154         } else {
155             group.summary = null;
156         }
157         updateSuppression(group);
158         if (group.children.isEmpty()) {
159             if (group.summary == null) {
160                 mGroupMap.remove(groupKey);
161                 for (OnGroupChangeListener listener : mListeners) {
162                     listener.onGroupRemoved(group, groupKey);
163                 }
164             }
165         }
166     }
167 
168     /**
169      * Notify the group manager that a new entry was added
170      */
onEntryAdded(final NotificationEntry added)171     public void onEntryAdded(final NotificationEntry added) {
172         updateIsolation(added);
173         onEntryAddedInternal(added);
174     }
175 
onEntryAddedInternal(final NotificationEntry added)176     private void onEntryAddedInternal(final NotificationEntry added) {
177         if (added.isRowRemoved()) {
178             added.setDebugThrowable(new Throwable());
179         }
180         final StatusBarNotification sbn = added.getSbn();
181         boolean isGroupChild = isGroupChild(sbn);
182         String groupKey = getGroupKey(sbn);
183         NotificationGroup group = mGroupMap.get(groupKey);
184         if (group == null) {
185             group = new NotificationGroup();
186             mGroupMap.put(groupKey, group);
187             for (OnGroupChangeListener listener : mListeners) {
188                 listener.onGroupCreated(group, groupKey);
189             }
190         }
191         if (isGroupChild) {
192             NotificationEntry existing = group.children.get(added.getKey());
193             if (existing != null && existing != added) {
194                 Throwable existingThrowable = existing.getDebugThrowable();
195                 Log.wtf(TAG, "Inconsistent entries found with the same key " + added.getKey()
196                         + "existing removed: " + existing.isRowRemoved()
197                         + (existingThrowable != null
198                                 ? Log.getStackTraceString(existingThrowable) + "\n": "")
199                         + " added removed" + added.isRowRemoved()
200                         , new Throwable());
201             }
202             group.children.put(added.getKey(), added);
203             updateSuppression(group);
204         } else {
205             group.summary = added;
206             group.expanded = added.areChildrenExpanded();
207             updateSuppression(group);
208             if (!group.children.isEmpty()) {
209                 ArrayList<NotificationEntry> childrenCopy
210                         = new ArrayList<>(group.children.values());
211                 for (NotificationEntry child : childrenCopy) {
212                     onEntryBecomingChild(child);
213                 }
214                 for (OnGroupChangeListener listener : mListeners) {
215                     listener.onGroupCreatedFromChildren(group);
216                 }
217             }
218         }
219     }
220 
onEntryBecomingChild(NotificationEntry entry)221     private void onEntryBecomingChild(NotificationEntry entry) {
222         updateIsolation(entry);
223     }
224 
updateSuppression(NotificationGroup group)225     private void updateSuppression(NotificationGroup group) {
226         if (group == null) {
227             return;
228         }
229         int childCount = 0;
230         boolean hasBubbles = false;
231         for (NotificationEntry entry : group.children.values()) {
232             if (!getBubbleController().isBubbleNotificationSuppressedFromShade(entry)) {
233                 childCount++;
234             } else {
235                 hasBubbles = true;
236             }
237         }
238 
239         boolean prevSuppressed = group.suppressed;
240         group.suppressed = group.summary != null && !group.expanded
241                 && (childCount == 1
242                 || (childCount == 0
243                         && group.summary.getSbn().getNotification().isGroupSummary()
244                         && (hasIsolatedChildren(group) || hasBubbles)));
245         if (prevSuppressed != group.suppressed) {
246             for (OnGroupChangeListener listener : mListeners) {
247                 if (!mIsUpdatingUnchangedGroup) {
248                     listener.onGroupSuppressionChanged(group, group.suppressed);
249                     listener.onGroupsChanged();
250                 }
251             }
252         }
253     }
254 
hasIsolatedChildren(NotificationGroup group)255     private boolean hasIsolatedChildren(NotificationGroup group) {
256         return getNumberOfIsolatedChildren(group.summary.getSbn().getGroupKey()) != 0;
257     }
258 
getNumberOfIsolatedChildren(String groupKey)259     private int getNumberOfIsolatedChildren(String groupKey) {
260         int count = 0;
261         for (StatusBarNotification sbn : mIsolatedEntries.values()) {
262             if (sbn.getGroupKey().equals(groupKey) && isIsolated(sbn.getKey())) {
263                 count++;
264             }
265         }
266         return count;
267     }
268 
269     /**
270      * Update an entry's group information
271      * @param entry notification entry to update
272      * @param oldNotification previous notification info before this update
273      */
onEntryUpdated(NotificationEntry entry, StatusBarNotification oldNotification)274     public void onEntryUpdated(NotificationEntry entry, StatusBarNotification oldNotification) {
275         onEntryUpdated(entry, oldNotification.getGroupKey(), oldNotification.isGroup(),
276                 oldNotification.getNotification().isGroupSummary());
277     }
278 
279     /**
280      * Updates an entry's group information
281      * @param entry notification entry to update
282      * @param oldGroupKey the notification's previous group key before this update
283      * @param oldIsGroup whether this notification was a group before this update
284      * @param oldIsGroupSummary whether this notification was a group summary before this update
285      */
onEntryUpdated(NotificationEntry entry, String oldGroupKey, boolean oldIsGroup, boolean oldIsGroupSummary)286     public void onEntryUpdated(NotificationEntry entry, String oldGroupKey, boolean oldIsGroup,
287             boolean oldIsGroupSummary) {
288         String newGroupKey = entry.getSbn().getGroupKey();
289         boolean groupKeysChanged = !oldGroupKey.equals(newGroupKey);
290         boolean wasGroupChild = isGroupChild(entry.getKey(), oldIsGroup, oldIsGroupSummary);
291         boolean isGroupChild = isGroupChild(entry.getSbn());
292         mIsUpdatingUnchangedGroup = !groupKeysChanged && wasGroupChild == isGroupChild;
293         if (mGroupMap.get(getGroupKey(entry.getKey(), oldGroupKey)) != null) {
294             onEntryRemovedInternal(entry, oldGroupKey, oldIsGroup, oldIsGroupSummary);
295         }
296         onEntryAddedInternal(entry);
297         mIsUpdatingUnchangedGroup = false;
298         if (isIsolated(entry.getSbn().getKey())) {
299             mIsolatedEntries.put(entry.getKey(), entry.getSbn());
300             if (groupKeysChanged) {
301                 updateSuppression(mGroupMap.get(oldGroupKey));
302                 updateSuppression(mGroupMap.get(newGroupKey));
303             }
304         } else if (!wasGroupChild && isGroupChild) {
305             onEntryBecomingChild(entry);
306         }
307     }
308 
isSummaryOfSuppressedGroup(StatusBarNotification sbn)309     public boolean isSummaryOfSuppressedGroup(StatusBarNotification sbn) {
310         return isGroupSuppressed(getGroupKey(sbn)) && sbn.getNotification().isGroupSummary();
311     }
312 
isOnlyChild(StatusBarNotification sbn)313     private boolean isOnlyChild(StatusBarNotification sbn) {
314         return !sbn.getNotification().isGroupSummary()
315                 && getTotalNumberOfChildren(sbn) == 1;
316     }
317 
isOnlyChildInGroup(StatusBarNotification sbn)318     public boolean isOnlyChildInGroup(StatusBarNotification sbn) {
319         if (!isOnlyChild(sbn)) {
320             return false;
321         }
322         NotificationEntry logicalGroupSummary = getLogicalGroupSummary(sbn);
323         return logicalGroupSummary != null
324                 && !logicalGroupSummary.getSbn().equals(sbn);
325     }
326 
getTotalNumberOfChildren(StatusBarNotification sbn)327     private int getTotalNumberOfChildren(StatusBarNotification sbn) {
328         int isolatedChildren = getNumberOfIsolatedChildren(sbn.getGroupKey());
329         NotificationGroup group = mGroupMap.get(sbn.getGroupKey());
330         int realChildren = group != null ? group.children.size() : 0;
331         return isolatedChildren + realChildren;
332     }
333 
isGroupSuppressed(String groupKey)334     private boolean isGroupSuppressed(String groupKey) {
335         NotificationGroup group = mGroupMap.get(groupKey);
336         return group != null && group.suppressed;
337     }
338 
setStatusBarState(int newState)339     private void setStatusBarState(int newState) {
340         mBarState = newState;
341         if (mBarState == StatusBarState.KEYGUARD) {
342             collapseAllGroups();
343         }
344     }
345 
collapseAllGroups()346     public void collapseAllGroups() {
347         // Because notifications can become isolated when the group becomes suppressed it can
348         // lead to concurrent modifications while looping. We need to make a copy.
349         ArrayList<NotificationGroup> groupCopy = new ArrayList<>(mGroupMap.values());
350         int size = groupCopy.size();
351         for (int i = 0; i < size; i++) {
352             NotificationGroup group =  groupCopy.get(i);
353             if (group.expanded) {
354                 setGroupExpanded(group, false);
355             }
356             updateSuppression(group);
357         }
358     }
359 
360     /**
361      * @return whether a given notification is a child in a group which has a summary
362      */
isChildInGroupWithSummary(StatusBarNotification sbn)363     public boolean isChildInGroupWithSummary(StatusBarNotification sbn) {
364         if (!isGroupChild(sbn)) {
365             return false;
366         }
367         NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
368         if (group == null || group.summary == null || group.suppressed) {
369             return false;
370         }
371         if (group.children.isEmpty()) {
372             // If the suppression of a group changes because the last child was removed, this can
373             // still be called temporarily because the child hasn't been fully removed yet. Let's
374             // make sure we still return false in that case.
375             return false;
376         }
377         return true;
378     }
379 
380     /**
381      * @return whether a given notification is a summary in a group which has children
382      */
isSummaryOfGroup(StatusBarNotification sbn)383     public boolean isSummaryOfGroup(StatusBarNotification sbn) {
384         if (!isGroupSummary(sbn)) {
385             return false;
386         }
387         NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
388         if (group == null || group.summary == null) {
389             return false;
390         }
391         return !group.children.isEmpty() && Objects.equals(group.summary.getSbn(), sbn);
392     }
393 
394     /**
395      * Get the summary of a specified status bar notification. For isolated notification this return
396      * itself.
397      */
getGroupSummary(StatusBarNotification sbn)398     public NotificationEntry getGroupSummary(StatusBarNotification sbn) {
399         return getGroupSummary(getGroupKey(sbn));
400     }
401 
402     /**
403      * Similar to {@link #getGroupSummary(StatusBarNotification)} but doesn't get the visual summary
404      * but the logical summary, i.e when a child is isolated, it still returns the summary as if
405      * it wasn't isolated.
406      */
getLogicalGroupSummary(StatusBarNotification sbn)407     public NotificationEntry getLogicalGroupSummary(StatusBarNotification sbn) {
408         return getGroupSummary(sbn.getGroupKey());
409     }
410 
411     @Nullable
getGroupSummary(String groupKey)412     private NotificationEntry getGroupSummary(String groupKey) {
413         NotificationGroup group = mGroupMap.get(groupKey);
414         //TODO: see if this can become an Entry
415         return group == null ? null
416                 : group.summary;
417     }
418 
419     /**
420      * Get the children that are logically in the summary's group, whether or not they are isolated.
421      *
422      * @param summary summary of a group
423      * @return list of the children
424      */
getLogicalChildren(StatusBarNotification summary)425     public ArrayList<NotificationEntry> getLogicalChildren(StatusBarNotification summary) {
426         NotificationGroup group = mGroupMap.get(summary.getGroupKey());
427         if (group == null) {
428             return null;
429         }
430         ArrayList<NotificationEntry> children = new ArrayList<>(group.children.values());
431         for (StatusBarNotification sbn : mIsolatedEntries.values()) {
432             if (sbn.getGroupKey().equals(summary.getGroupKey())) {
433                 children.add(mGroupMap.get(sbn.getKey()).summary);
434             }
435         }
436         return children;
437     }
438 
439     /**
440      * Get the children that are in the summary's group, not including those isolated.
441      *
442      * @param summary summary of a group
443      * @return list of the children
444      */
getChildren(StatusBarNotification summary)445     public @Nullable ArrayList<NotificationEntry> getChildren(StatusBarNotification summary) {
446         NotificationGroup group = mGroupMap.get(summary.getGroupKey());
447         if (group == null) {
448             return null;
449         }
450         return new ArrayList<>(group.children.values());
451     }
452 
453     /**
454      * If there is a {@link NotificationGroup} associated with the provided entry, this method
455      * will update the suppression of that group.
456      */
updateSuppression(NotificationEntry entry)457     public void updateSuppression(NotificationEntry entry) {
458         NotificationGroup group = mGroupMap.get(getGroupKey(entry.getSbn()));
459         if (group != null) {
460             updateSuppression(group);
461         }
462     }
463 
464     /**
465      * Get the group key. May differ from the one in the notification due to the notification
466      * being temporarily isolated.
467      *
468      * @param sbn notification to check
469      * @return the key of the notification
470      */
getGroupKey(StatusBarNotification sbn)471     public String getGroupKey(StatusBarNotification sbn) {
472         return getGroupKey(sbn.getKey(), sbn.getGroupKey());
473     }
474 
getGroupKey(String key, String groupKey)475     private String getGroupKey(String key, String groupKey) {
476         if (isIsolated(key)) {
477             return key;
478         }
479         return groupKey;
480     }
481 
482     /** @return group expansion state after toggling. */
toggleGroupExpansion(StatusBarNotification sbn)483     public boolean toggleGroupExpansion(StatusBarNotification sbn) {
484         NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
485         if (group == null) {
486             return false;
487         }
488         setGroupExpanded(group, !group.expanded);
489         return group.expanded;
490     }
491 
isIsolated(String sbnKey)492     private boolean isIsolated(String sbnKey) {
493         return mIsolatedEntries.containsKey(sbnKey);
494     }
495 
496     /**
497      * Whether a notification is visually a group summary.
498      *
499      * @param sbn notification to check
500      * @return true if it is visually a group summary
501      */
isGroupSummary(StatusBarNotification sbn)502     public boolean isGroupSummary(StatusBarNotification sbn) {
503         if (isIsolated(sbn.getKey())) {
504             return true;
505         }
506         return sbn.getNotification().isGroupSummary();
507     }
508 
509     /**
510      * Whether a notification is visually a group child.
511      *
512      * @param sbn notification to check
513      * @return true if it is visually a group child
514      */
isGroupChild(StatusBarNotification sbn)515     public boolean isGroupChild(StatusBarNotification sbn) {
516         return isGroupChild(sbn.getKey(), sbn.isGroup(), sbn.getNotification().isGroupSummary());
517     }
518 
isGroupChild(String key, boolean isGroup, boolean isGroupSummary)519     private boolean isGroupChild(String key, boolean isGroup, boolean isGroupSummary) {
520         if (isIsolated(key)) {
521             return false;
522         }
523         return isGroup && !isGroupSummary;
524     }
525 
526     @Override
onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp)527     public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) {
528         updateIsolation(entry);
529     }
530 
531     /**
532      * Whether a notification that is normally part of a group should be temporarily isolated from
533      * the group and put in their own group visually.  This generally happens when the notification
534      * is alerting.
535      *
536      * @param entry the notification to check
537      * @return true if the entry should be isolated
538      */
539 
shouldIsolate(NotificationEntry entry)540     private boolean shouldIsolate(NotificationEntry entry) {
541         StatusBarNotification sbn = entry.getSbn();
542         if (!sbn.isGroup() || sbn.getNotification().isGroupSummary()) {
543             return false;
544         }
545         int peopleNotificationType = mPeopleNotificationIdentifier.get().getPeopleNotificationType(
546                 entry.getSbn(), entry.getRanking());
547         if (peopleNotificationType == PeopleNotificationIdentifier.TYPE_IMPORTANT_PERSON) {
548             return true;
549         }
550         if (mHeadsUpManager != null && !mHeadsUpManager.isAlerting(entry.getKey())) {
551             return false;
552         }
553         NotificationGroup notificationGroup = mGroupMap.get(sbn.getGroupKey());
554         return (sbn.getNotification().fullScreenIntent != null
555                     || notificationGroup == null
556                     || !notificationGroup.expanded
557                     || isGroupNotFullyVisible(notificationGroup));
558     }
559 
560     /**
561      * Isolate a notification from its group so that it visually shows as its own group.
562      *
563      * @param entry the notification to isolate
564      */
isolateNotification(NotificationEntry entry)565     private void isolateNotification(NotificationEntry entry) {
566         StatusBarNotification sbn = entry.getSbn();
567 
568         // We will be isolated now, so lets update the groups
569         onEntryRemovedInternal(entry, entry.getSbn());
570 
571         mIsolatedEntries.put(sbn.getKey(), sbn);
572 
573         onEntryAddedInternal(entry);
574         // We also need to update the suppression of the old group, because this call comes
575         // even before the groupManager knows about the notification at all.
576         // When the notification gets added afterwards it is already isolated and therefore
577         // it doesn't lead to an update.
578         updateSuppression(mGroupMap.get(entry.getSbn().getGroupKey()));
579         for (OnGroupChangeListener listener : mListeners) {
580             listener.onGroupsChanged();
581         }
582     }
583 
584     /**
585      * Update the isolation of an entry, splitting it from the group.
586      */
updateIsolation(NotificationEntry entry)587     public void updateIsolation(NotificationEntry entry) {
588         boolean isIsolated = isIsolated(entry.getSbn().getKey());
589         if (shouldIsolate(entry)) {
590             if (!isIsolated) {
591                 isolateNotification(entry);
592             }
593         } else if (isIsolated) {
594             stopIsolatingNotification(entry);
595         }
596     }
597 
598     /**
599      * Stop isolating a notification and re-group it with its original logical group.
600      *
601      * @param entry the notification to un-isolate
602      */
stopIsolatingNotification(NotificationEntry entry)603     private void stopIsolatingNotification(NotificationEntry entry) {
604         StatusBarNotification sbn = entry.getSbn();
605         if (isIsolated(sbn.getKey())) {
606             // not isolated anymore, we need to update the groups
607             onEntryRemovedInternal(entry, entry.getSbn());
608             mIsolatedEntries.remove(sbn.getKey());
609             onEntryAddedInternal(entry);
610             for (OnGroupChangeListener listener : mListeners) {
611                 listener.onGroupsChanged();
612             }
613         }
614     }
615 
isGroupNotFullyVisible(NotificationGroup notificationGroup)616     private boolean isGroupNotFullyVisible(NotificationGroup notificationGroup) {
617         return notificationGroup.summary == null
618                 || notificationGroup.summary.isGroupNotFullyVisible();
619     }
620 
setHeadsUpManager(HeadsUpManager headsUpManager)621     public void setHeadsUpManager(HeadsUpManager headsUpManager) {
622         mHeadsUpManager = headsUpManager;
623     }
624 
dump(FileDescriptor fd, PrintWriter pw, String[] args)625     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
626         pw.println("GroupManager state:");
627         pw.println("  number of groups: " +  mGroupMap.size());
628         for (Map.Entry<String, NotificationGroup>  entry : mGroupMap.entrySet()) {
629             pw.println("\n    key: " + entry.getKey()); pw.println(entry.getValue());
630         }
631         pw.println("\n    isolated entries: " +  mIsolatedEntries.size());
632         for (Map.Entry<String, StatusBarNotification> entry : mIsolatedEntries.entrySet()) {
633             pw.print("      "); pw.print(entry.getKey());
634             pw.print(", "); pw.println(entry.getValue());
635         }
636     }
637 
638     @Override
onStateChanged(int newState)639     public void onStateChanged(int newState) {
640         setStatusBarState(newState);
641     }
642 
643     public static class NotificationGroup {
644         public final HashMap<String, NotificationEntry> children = new HashMap<>();
645         public NotificationEntry summary;
646         public boolean expanded;
647         /**
648          * Is this notification group suppressed, i.e its summary is hidden
649          */
650         public boolean suppressed;
651 
652         @Override
toString()653         public String toString() {
654             String result = "    summary:\n      "
655                     + (summary != null ? summary.getSbn() : "null")
656                     + (summary != null && summary.getDebugThrowable() != null
657                             ? Log.getStackTraceString(summary.getDebugThrowable())
658                             : "");
659             result += "\n    children size: " + children.size();
660             for (NotificationEntry child : children.values()) {
661                 result += "\n      " + child.getSbn()
662                 + (child.getDebugThrowable() != null
663                         ? Log.getStackTraceString(child.getDebugThrowable())
664                         : "");
665             }
666             result += "\n    summary suppressed: " + suppressed;
667             return result;
668         }
669     }
670 
671     public interface OnGroupChangeListener {
672 
673         /**
674          * A new group has been created.
675          *
676          * @param group the group that was created
677          * @param groupKey the group's key
678          */
onGroupCreated(NotificationGroup group, String groupKey)679         default void onGroupCreated(NotificationGroup group, String groupKey) {}
680 
681         /**
682          * A group has been removed.
683          *
684          * @param group the group that was removed
685          * @param groupKey the group's key
686          */
onGroupRemoved(NotificationGroup group, String groupKey)687         default void onGroupRemoved(NotificationGroup group, String groupKey) {}
688 
689         /**
690          * The suppression of a group has changed.
691          *
692          * @param group the group that has changed
693          * @param suppressed true if the group is now suppressed, false o/w
694          */
onGroupSuppressionChanged(NotificationGroup group, boolean suppressed)695         default void onGroupSuppressionChanged(NotificationGroup group, boolean suppressed) {}
696 
697         /**
698          * The expansion of a group has changed.
699          *
700          * @param changedRow the row for which the expansion has changed, which is also the summary
701          * @param expanded a boolean indicating the new expanded state
702          */
onGroupExpansionChanged(ExpandableNotificationRow changedRow, boolean expanded)703         default void onGroupExpansionChanged(ExpandableNotificationRow changedRow,
704                 boolean expanded) {}
705 
706         /**
707          * A group of children just received a summary notification and should therefore become
708          * children of it.
709          *
710          * @param group the group created
711          */
onGroupCreatedFromChildren(NotificationGroup group)712         default void onGroupCreatedFromChildren(NotificationGroup group) {}
713 
714         /**
715          * The groups have changed. This can happen if the isolation of a child has changes or if a
716          * group became suppressed / unsuppressed
717          */
onGroupsChanged()718         default void onGroupsChanged() {}
719     }
720 }
721