• 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.phone;
18 
19 import android.service.notification.StatusBarNotification;
20 import android.support.annotation.Nullable;
21 
22 import com.android.systemui.statusbar.ExpandableNotificationRow;
23 import com.android.systemui.statusbar.NotificationData;
24 import com.android.systemui.statusbar.StatusBarState;
25 import com.android.systemui.statusbar.policy.HeadsUpManager;
26 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
27 
28 import java.io.FileDescriptor;
29 import java.io.PrintWriter;
30 import java.util.ArrayList;
31 import java.util.HashMap;
32 import java.util.HashSet;
33 import java.util.Iterator;
34 import java.util.Map;
35 
36 /**
37  * A class to handle notifications and their corresponding groups.
38  */
39 public class NotificationGroupManager implements OnHeadsUpChangedListener {
40 
41     private final HashMap<String, NotificationGroup> mGroupMap = new HashMap<>();
42     private OnGroupChangeListener mListener;
43     private int mBarState = -1;
44     private HashMap<String, StatusBarNotification> mIsolatedEntries = new HashMap<>();
45     private HeadsUpManager mHeadsUpManager;
46     private boolean mIsUpdatingUnchangedGroup;
47 
setOnGroupChangeListener(OnGroupChangeListener listener)48     public void setOnGroupChangeListener(OnGroupChangeListener listener) {
49         mListener = listener;
50     }
51 
isGroupExpanded(StatusBarNotification sbn)52     public boolean isGroupExpanded(StatusBarNotification sbn) {
53         NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
54         if (group == null) {
55             return false;
56         }
57         return group.expanded;
58     }
59 
setGroupExpanded(StatusBarNotification sbn, boolean expanded)60     public void setGroupExpanded(StatusBarNotification sbn, boolean expanded) {
61         NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
62         if (group == null) {
63             return;
64         }
65         setGroupExpanded(group, expanded);
66     }
67 
setGroupExpanded(NotificationGroup group, boolean expanded)68     private void setGroupExpanded(NotificationGroup group, boolean expanded) {
69         group.expanded = expanded;
70         if (group.summary != null) {
71             mListener.onGroupExpansionChanged(group.summary.row, expanded);
72         }
73     }
74 
onEntryRemoved(NotificationData.Entry removed)75     public void onEntryRemoved(NotificationData.Entry removed) {
76         onEntryRemovedInternal(removed, removed.notification);
77         mIsolatedEntries.remove(removed.key);
78     }
79 
80     /**
81      * An entry was removed.
82      *
83      * @param removed the removed entry
84      * @param sbn the notification the entry has, which doesn't need to be the same as it's internal
85      *            notification
86      */
onEntryRemovedInternal(NotificationData.Entry removed, final StatusBarNotification sbn)87     private void onEntryRemovedInternal(NotificationData.Entry removed,
88             final StatusBarNotification sbn) {
89         String groupKey = getGroupKey(sbn);
90         final NotificationGroup group = mGroupMap.get(groupKey);
91         if (group == null) {
92             // When an app posts 2 different notifications as summary of the same group, then a
93             // cancellation of the first notification removes this group.
94             // This situation is not supported and we will not allow such notifications anymore in
95             // the close future. See b/23676310 for reference.
96             return;
97         }
98         if (isGroupChild(sbn)) {
99             group.children.remove(removed);
100         } else {
101             group.summary = null;
102         }
103         updateSuppression(group);
104         if (group.children.isEmpty()) {
105             if (group.summary == null) {
106                 mGroupMap.remove(groupKey);
107             }
108         }
109     }
110 
onEntryAdded(final NotificationData.Entry added)111     public void onEntryAdded(final NotificationData.Entry added) {
112         final StatusBarNotification sbn = added.notification;
113         boolean isGroupChild = isGroupChild(sbn);
114         String groupKey = getGroupKey(sbn);
115         NotificationGroup group = mGroupMap.get(groupKey);
116         if (group == null) {
117             group = new NotificationGroup();
118             mGroupMap.put(groupKey, group);
119         }
120         if (isGroupChild) {
121             group.children.add(added);
122             updateSuppression(group);
123         } else {
124             group.summary = added;
125             group.expanded = added.row.areChildrenExpanded();
126             updateSuppression(group);
127             if (!group.children.isEmpty()) {
128                 HashSet<NotificationData.Entry> childrenCopy =
129                         (HashSet<NotificationData.Entry>) group.children.clone();
130                 for (NotificationData.Entry child : childrenCopy) {
131                     onEntryBecomingChild(child);
132                 }
133                 mListener.onGroupCreatedFromChildren(group);
134             }
135         }
136     }
137 
onEntryBecomingChild(NotificationData.Entry entry)138     private void onEntryBecomingChild(NotificationData.Entry entry) {
139         if (entry.row.isHeadsUp()) {
140             onHeadsUpStateChanged(entry, true);
141         }
142     }
143 
updateSuppression(NotificationGroup group)144     private void updateSuppression(NotificationGroup group) {
145         if (group == null) {
146             return;
147         }
148         boolean prevSuppressed = group.suppressed;
149         group.suppressed = group.summary != null && !group.expanded
150                 && (group.children.size() == 1
151                 || (group.children.size() == 0
152                         && group.summary.notification.getNotification().isGroupSummary()
153                         && hasIsolatedChildren(group)));
154         if (prevSuppressed != group.suppressed) {
155             if (group.suppressed) {
156                 handleSuppressedSummaryHeadsUpped(group.summary);
157             }
158             if (!mIsUpdatingUnchangedGroup) {
159                 mListener.onGroupsChanged();
160             }
161         }
162     }
163 
hasIsolatedChildren(NotificationGroup group)164     private boolean hasIsolatedChildren(NotificationGroup group) {
165         return getNumberOfIsolatedChildren(group.summary.notification.getGroupKey()) != 0;
166     }
167 
getNumberOfIsolatedChildren(String groupKey)168     private int getNumberOfIsolatedChildren(String groupKey) {
169         int count = 0;
170         for (StatusBarNotification sbn : mIsolatedEntries.values()) {
171             if (sbn.getGroupKey().equals(groupKey) && isIsolated(sbn)) {
172                 count++;
173             }
174         }
175         return count;
176     }
177 
getIsolatedChild(String groupKey)178     private NotificationData.Entry getIsolatedChild(String groupKey) {
179         for (StatusBarNotification sbn : mIsolatedEntries.values()) {
180             if (sbn.getGroupKey().equals(groupKey) && isIsolated(sbn)) {
181                 return mGroupMap.get(sbn.getKey()).summary;
182             }
183         }
184         return null;
185     }
186 
onEntryUpdated(NotificationData.Entry entry, StatusBarNotification oldNotification)187     public void onEntryUpdated(NotificationData.Entry entry,
188             StatusBarNotification oldNotification) {
189         String oldKey = oldNotification.getGroupKey();
190         String newKey = entry.notification.getGroupKey();
191         boolean groupKeysChanged = !oldKey.equals(newKey);
192         boolean wasGroupChild = isGroupChild(oldNotification);
193         boolean isGroupChild = isGroupChild(entry.notification);
194         mIsUpdatingUnchangedGroup = !groupKeysChanged && wasGroupChild == isGroupChild;
195         if (mGroupMap.get(getGroupKey(oldNotification)) != null) {
196             onEntryRemovedInternal(entry, oldNotification);
197         }
198         onEntryAdded(entry);
199         mIsUpdatingUnchangedGroup = false;
200         if (isIsolated(entry.notification)) {
201             mIsolatedEntries.put(entry.key, entry.notification);
202             if (groupKeysChanged) {
203                 updateSuppression(mGroupMap.get(oldKey));
204                 updateSuppression(mGroupMap.get(newKey));
205             }
206         } else if (!wasGroupChild && isGroupChild) {
207             onEntryBecomingChild(entry);
208         }
209     }
210 
isSummaryOfSuppressedGroup(StatusBarNotification sbn)211     public boolean isSummaryOfSuppressedGroup(StatusBarNotification sbn) {
212         return isGroupSuppressed(getGroupKey(sbn)) && sbn.getNotification().isGroupSummary();
213     }
214 
isOnlyChild(StatusBarNotification sbn)215     private boolean isOnlyChild(StatusBarNotification sbn) {
216         return !sbn.getNotification().isGroupSummary()
217                 && getTotalNumberOfChildren(sbn) == 1;
218     }
219 
isOnlyChildInGroup(StatusBarNotification sbn)220     public boolean isOnlyChildInGroup(StatusBarNotification sbn) {
221         if (!isOnlyChild(sbn)) {
222             return false;
223         }
224         ExpandableNotificationRow logicalGroupSummary = getLogicalGroupSummary(sbn);
225         return logicalGroupSummary != null
226                 && !logicalGroupSummary.getStatusBarNotification().equals(sbn);
227     }
228 
getTotalNumberOfChildren(StatusBarNotification sbn)229     private int getTotalNumberOfChildren(StatusBarNotification sbn) {
230         int isolatedChildren = getNumberOfIsolatedChildren(sbn.getGroupKey());
231         NotificationGroup group = mGroupMap.get(sbn.getGroupKey());
232         int realChildren = group != null ? group.children.size() : 0;
233         return isolatedChildren + realChildren;
234     }
235 
isGroupSuppressed(String groupKey)236     private boolean isGroupSuppressed(String groupKey) {
237         NotificationGroup group = mGroupMap.get(groupKey);
238         return group != null && group.suppressed;
239     }
240 
setStatusBarState(int newState)241     public void setStatusBarState(int newState) {
242         if (mBarState == newState) {
243             return;
244         }
245         mBarState = newState;
246         if (mBarState == StatusBarState.KEYGUARD) {
247             collapseAllGroups();
248         }
249     }
250 
collapseAllGroups()251     public void collapseAllGroups() {
252         // Because notifications can become isolated when the group becomes suppressed it can
253         // lead to concurrent modifications while looping. We need to make a copy.
254         ArrayList<NotificationGroup> groupCopy = new ArrayList<>(mGroupMap.values());
255         int size = groupCopy.size();
256         for (int i = 0; i < size; i++) {
257             NotificationGroup group =  groupCopy.get(i);
258             if (group.expanded) {
259                 setGroupExpanded(group, false);
260             }
261             updateSuppression(group);
262         }
263     }
264 
265     /**
266      * @return whether a given notification is a child in a group which has a summary
267      */
isChildInGroupWithSummary(StatusBarNotification sbn)268     public boolean isChildInGroupWithSummary(StatusBarNotification sbn) {
269         if (!isGroupChild(sbn)) {
270             return false;
271         }
272         NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
273         if (group == null || group.summary == null || group.suppressed) {
274             return false;
275         }
276         if (group.children.isEmpty()) {
277             // If the suppression of a group changes because the last child was removed, this can
278             // still be called temporarily because the child hasn't been fully removed yet. Let's
279             // make sure we still return false in that case.
280             return false;
281         }
282         return true;
283     }
284 
285     /**
286      * @return whether a given notification is a summary in a group which has children
287      */
isSummaryOfGroup(StatusBarNotification sbn)288     public boolean isSummaryOfGroup(StatusBarNotification sbn) {
289         if (!isGroupSummary(sbn)) {
290             return false;
291         }
292         NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
293         if (group == null) {
294             return false;
295         }
296         return !group.children.isEmpty();
297     }
298 
299     /**
300      * Get the summary of a specified status bar notification. For isolated notification this return
301      * itself.
302      */
getGroupSummary(StatusBarNotification sbn)303     public ExpandableNotificationRow getGroupSummary(StatusBarNotification sbn) {
304         return getGroupSummary(getGroupKey(sbn));
305     }
306 
307     /**
308      * Similar to {@link #getGroupSummary(StatusBarNotification)} but doesn't get the visual summary
309      * but the logical summary, i.e when a child is isolated, it still returns the summary as if
310      * it wasn't isolated.
311      */
getLogicalGroupSummary( StatusBarNotification sbn)312     public ExpandableNotificationRow getLogicalGroupSummary(
313             StatusBarNotification sbn) {
314         return getGroupSummary(sbn.getGroupKey());
315     }
316 
317     @Nullable
getGroupSummary(String groupKey)318     private ExpandableNotificationRow getGroupSummary(String groupKey) {
319         NotificationGroup group = mGroupMap.get(groupKey);
320         return group == null ? null
321                 : group.summary == null ? null
322                         : group.summary.row;
323     }
324 
325     /** @return group expansion state after toggling. */
toggleGroupExpansion(StatusBarNotification sbn)326     public boolean toggleGroupExpansion(StatusBarNotification sbn) {
327         NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
328         if (group == null) {
329             return false;
330         }
331         setGroupExpanded(group, !group.expanded);
332         return group.expanded;
333     }
334 
isIsolated(StatusBarNotification sbn)335     private boolean isIsolated(StatusBarNotification sbn) {
336         return mIsolatedEntries.containsKey(sbn.getKey());
337     }
338 
isGroupSummary(StatusBarNotification sbn)339     private boolean isGroupSummary(StatusBarNotification sbn) {
340         if (isIsolated(sbn)) {
341             return true;
342         }
343         return sbn.getNotification().isGroupSummary();
344     }
345 
isGroupChild(StatusBarNotification sbn)346     private boolean isGroupChild(StatusBarNotification sbn) {
347         if (isIsolated(sbn)) {
348             return false;
349         }
350         return sbn.isGroup() && !sbn.getNotification().isGroupSummary();
351     }
352 
getGroupKey(StatusBarNotification sbn)353     private String getGroupKey(StatusBarNotification sbn) {
354         if (isIsolated(sbn)) {
355             return sbn.getKey();
356         }
357         return sbn.getGroupKey();
358     }
359 
360     @Override
onHeadsUpPinnedModeChanged(boolean inPinnedMode)361     public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) {
362     }
363 
364     @Override
onHeadsUpPinned(ExpandableNotificationRow headsUp)365     public void onHeadsUpPinned(ExpandableNotificationRow headsUp) {
366     }
367 
368     @Override
onHeadsUpUnPinned(ExpandableNotificationRow headsUp)369     public void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) {
370     }
371 
372     @Override
onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp)373     public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {
374         final StatusBarNotification sbn = entry.notification;
375         if (entry.row.isHeadsUp()) {
376             if (shouldIsolate(sbn)) {
377                 // We will be isolated now, so lets update the groups
378                 onEntryRemovedInternal(entry, entry.notification);
379 
380                 mIsolatedEntries.put(sbn.getKey(), sbn);
381 
382                 onEntryAdded(entry);
383                 // We also need to update the suppression of the old group, because this call comes
384                 // even before the groupManager knows about the notification at all.
385                 // When the notification gets added afterwards it is already isolated and therefore
386                 // it doesn't lead to an update.
387                 updateSuppression(mGroupMap.get(entry.notification.getGroupKey()));
388                 mListener.onGroupsChanged();
389             } else {
390                 handleSuppressedSummaryHeadsUpped(entry);
391             }
392         } else {
393             if (mIsolatedEntries.containsKey(sbn.getKey())) {
394                 // not isolated anymore, we need to update the groups
395                 onEntryRemovedInternal(entry, entry.notification);
396                 mIsolatedEntries.remove(sbn.getKey());
397                 onEntryAdded(entry);
398                 mListener.onGroupsChanged();
399             }
400         }
401     }
402 
handleSuppressedSummaryHeadsUpped(NotificationData.Entry entry)403     private void handleSuppressedSummaryHeadsUpped(NotificationData.Entry entry) {
404         StatusBarNotification sbn = entry.notification;
405         if (!isGroupSuppressed(sbn.getGroupKey())
406                 || !sbn.getNotification().isGroupSummary()
407                 || !entry.row.isHeadsUp()) {
408             return;
409         }
410         // The parent of a suppressed group got huned, lets hun the child!
411         NotificationGroup notificationGroup = mGroupMap.get(sbn.getGroupKey());
412         if (notificationGroup != null) {
413             Iterator<NotificationData.Entry> iterator = notificationGroup.children.iterator();
414             NotificationData.Entry child = iterator.hasNext() ? iterator.next() : null;
415             if (child == null) {
416                 child = getIsolatedChild(sbn.getGroupKey());
417             }
418             if (child != null) {
419                 if (mHeadsUpManager.isHeadsUp(child.key)) {
420                     mHeadsUpManager.updateNotification(child, true);
421                 } else {
422                     mHeadsUpManager.showNotification(child);
423                 }
424             }
425         }
426         mHeadsUpManager.releaseImmediately(entry.key);
427     }
428 
shouldIsolate(StatusBarNotification sbn)429     private boolean shouldIsolate(StatusBarNotification sbn) {
430         NotificationGroup notificationGroup = mGroupMap.get(sbn.getGroupKey());
431         return (sbn.isGroup() && !sbn.getNotification().isGroupSummary())
432                 && (sbn.getNotification().fullScreenIntent != null
433                         || notificationGroup == null
434                         || !notificationGroup.expanded
435                         || isGroupNotFullyVisible(notificationGroup));
436     }
437 
isGroupNotFullyVisible(NotificationGroup notificationGroup)438     private boolean isGroupNotFullyVisible(NotificationGroup notificationGroup) {
439         return notificationGroup.summary == null
440                 || notificationGroup.summary.row.getClipTopAmount() > 0
441                 || notificationGroup.summary.row.getTranslationY() < 0;
442     }
443 
setHeadsUpManager(HeadsUpManager headsUpManager)444     public void setHeadsUpManager(HeadsUpManager headsUpManager) {
445         mHeadsUpManager = headsUpManager;
446     }
447 
dump(FileDescriptor fd, PrintWriter pw, String[] args)448     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
449         pw.println("GroupManager state:");
450         pw.println("  number of groups: " +  mGroupMap.size());
451         for (Map.Entry<String, NotificationGroup>  entry : mGroupMap.entrySet()) {
452             pw.println("\n    key: " + entry.getKey()); pw.println(entry.getValue());
453         }
454         pw.println("\n    isolated entries: " +  mIsolatedEntries.size());
455         for (Map.Entry<String, StatusBarNotification> entry : mIsolatedEntries.entrySet()) {
456             pw.print("      "); pw.print(entry.getKey());
457             pw.print(", "); pw.println(entry.getValue());
458         }
459     }
460 
461     public static class NotificationGroup {
462         public final HashSet<NotificationData.Entry> children = new HashSet<>();
463         public NotificationData.Entry summary;
464         public boolean expanded;
465         /**
466          * Is this notification group suppressed, i.e its summary is hidden
467          */
468         public boolean suppressed;
469 
470         @Override
toString()471         public String toString() {
472             String result = "    summary:\n      "
473                     + (summary != null ? summary.notification : "null");
474             result += "\n    children size: " + children.size();
475             for (NotificationData.Entry child : children) {
476                 result += "\n      " + child.notification;
477             }
478             return result;
479         }
480     }
481 
482     public interface OnGroupChangeListener {
483         /**
484          * The expansion of a group has changed.
485          *
486          * @param changedRow the row for which the expansion has changed, which is also the summary
487          * @param expanded a boolean indicating the new expanded state
488          */
onGroupExpansionChanged(ExpandableNotificationRow changedRow, boolean expanded)489         void onGroupExpansionChanged(ExpandableNotificationRow changedRow, boolean expanded);
490 
491         /**
492          * A group of children just received a summary notification and should therefore become
493          * children of it.
494          *
495          * @param group the group created
496          */
onGroupCreatedFromChildren(NotificationGroup group)497         void onGroupCreatedFromChildren(NotificationGroup group);
498 
499         /**
500          * The groups have changed. This can happen if the isolation of a child has changes or if a
501          * group became suppressed / unsuppressed
502          */
onGroupsChanged()503         void onGroupsChanged();
504     }
505 }
506