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