blob: fffb20a7425c7d20612d033d41b07055054a1898 [file] [log] [blame]
Selim Cinek25fd4e2b2015-02-20 17:46:07 +01001/*
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
17package com.android.systemui.statusbar.phone;
18
Selim Cinek25fd4e2b2015-02-20 17:46:07 +010019import android.service.notification.StatusBarNotification;
Selim Cinek23c80342016-03-17 18:27:36 -070020import android.support.annotation.Nullable;
Selim Cinek25fd4e2b2015-02-20 17:46:07 +010021
22import com.android.systemui.statusbar.ExpandableNotificationRow;
23import com.android.systemui.statusbar.NotificationData;
Selim Cinek9c4c4142015-12-04 16:44:56 -080024import com.android.systemui.statusbar.StatusBarState;
Selim Cinekef5127e2015-12-21 16:55:58 -080025import com.android.systemui.statusbar.policy.HeadsUpManager;
Selim Cinek25fd4e2b2015-02-20 17:46:07 +010026
27import java.util.HashMap;
28import java.util.HashSet;
Selim Cinek967ed2a2016-04-08 18:29:11 -070029import java.util.Iterator;
Julia Reynoldse46bb372016-03-17 11:05:58 -040030import java.util.Objects;
Selim Cinek25fd4e2b2015-02-20 17:46:07 +010031
32/**
33 * A class to handle notifications and their corresponding groups.
34 */
Selim Cinekef5127e2015-12-21 16:55:58 -080035public class NotificationGroupManager implements HeadsUpManager.OnHeadsUpChangedListener {
Selim Cinek25fd4e2b2015-02-20 17:46:07 +010036
37 private final HashMap<String, NotificationGroup> mGroupMap = new HashMap<>();
38 private OnGroupChangeListener mListener;
39 private int mBarState = -1;
Selim Cineka6c0cef2016-03-18 11:42:25 -070040 private HashMap<String, StatusBarNotification> mIsolatedEntries = new HashMap<>();
Selim Cinek967ed2a2016-04-08 18:29:11 -070041 private HeadsUpManager mHeadsUpManager;
Selim Cinek25fd4e2b2015-02-20 17:46:07 +010042
43 public void setOnGroupChangeListener(OnGroupChangeListener listener) {
44 mListener = listener;
45 }
46
47 public boolean isGroupExpanded(StatusBarNotification sbn) {
Selim Cinekef5127e2015-12-21 16:55:58 -080048 NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
Selim Cinek25fd4e2b2015-02-20 17:46:07 +010049 if (group == null) {
50 return false;
51 }
52 return group.expanded;
53 }
54
55 public void setGroupExpanded(StatusBarNotification sbn, boolean expanded) {
Selim Cinekef5127e2015-12-21 16:55:58 -080056 NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
Selim Cinek25fd4e2b2015-02-20 17:46:07 +010057 if (group == null) {
58 return;
59 }
60 setGroupExpanded(group, expanded);
61 }
62
63 private void setGroupExpanded(NotificationGroup group, boolean expanded) {
64 group.expanded = expanded;
65 if (group.summary != null) {
66 mListener.onGroupExpansionChanged(group.summary.row, expanded);
67 }
68 }
69
70 public void onEntryRemoved(NotificationData.Entry removed) {
71 onEntryRemovedInternal(removed, removed.notification);
72 }
73
74 /**
75 * An entry was removed.
76 *
77 * @param removed the removed entry
78 * @param sbn the notification the entry has, which doesn't need to be the same as it's internal
79 * notification
80 */
81 private void onEntryRemovedInternal(NotificationData.Entry removed,
82 final StatusBarNotification sbn) {
Selim Cinekef5127e2015-12-21 16:55:58 -080083 String groupKey = getGroupKey(sbn);
Selim Cinek25fd4e2b2015-02-20 17:46:07 +010084 final NotificationGroup group = mGroupMap.get(groupKey);
Selim Cinek0b4aeab2015-09-01 17:07:38 -070085 if (group == null) {
86 // When an app posts 2 different notifications as summary of the same group, then a
87 // cancellation of the first notification removes this group.
88 // This situation is not supported and we will not allow such notifications anymore in
89 // the close future. See b/23676310 for reference.
90 return;
91 }
Selim Cinekef5127e2015-12-21 16:55:58 -080092 if (isGroupChild(sbn)) {
Selim Cinek25fd4e2b2015-02-20 17:46:07 +010093 group.children.remove(removed);
Selim Cineke73ad212015-11-03 19:11:08 -080094 } else {
95 group.summary = null;
Selim Cinek25fd4e2b2015-02-20 17:46:07 +010096 }
Selim Cinek2a739342016-03-17 10:28:55 -070097 updateSuppression(group);
Selim Cinek25fd4e2b2015-02-20 17:46:07 +010098 if (group.children.isEmpty()) {
99 if (group.summary == null) {
100 mGroupMap.remove(groupKey);
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100101 }
102 }
103 }
104
Selim Cinekef5127e2015-12-21 16:55:58 -0800105 public void onEntryAdded(final NotificationData.Entry added) {
106 final StatusBarNotification sbn = added.notification;
107 boolean isGroupChild = isGroupChild(sbn);
108 String groupKey = getGroupKey(sbn);
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100109 NotificationGroup group = mGroupMap.get(groupKey);
110 if (group == null) {
111 group = new NotificationGroup();
112 mGroupMap.put(groupKey, group);
113 }
Selim Cinekef5127e2015-12-21 16:55:58 -0800114 if (isGroupChild) {
Selim Cineke73ad212015-11-03 19:11:08 -0800115 group.children.add(added);
Selim Cinek2a739342016-03-17 10:28:55 -0700116 updateSuppression(group);
Selim Cineke73ad212015-11-03 19:11:08 -0800117 } else {
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100118 group.summary = added;
Selim Cinekb5605e52015-02-20 18:21:41 +0100119 group.expanded = added.row.areChildrenExpanded();
Selim Cinek2a739342016-03-17 10:28:55 -0700120 updateSuppression(group);
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100121 if (!group.children.isEmpty()) {
122 mListener.onGroupCreatedFromChildren(group);
123 }
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100124 }
125 }
126
Julia Reynoldse46bb372016-03-17 11:05:58 -0400127 public void onEntryBundlingUpdated(final NotificationData.Entry updated,
128 final String overrideGroupKey) {
129 final StatusBarNotification oldSbn = updated.notification.clone();
130 if (!Objects.equals(oldSbn.getOverrideGroupKey(), overrideGroupKey)) {
131 updated.notification.setOverrideGroupKey(overrideGroupKey);
132 onEntryUpdated(updated, oldSbn);
133 }
134 }
135
Selim Cinek2a739342016-03-17 10:28:55 -0700136 private void updateSuppression(NotificationGroup group) {
Selim Cinek23c80342016-03-17 18:27:36 -0700137 if (group == null) {
138 return;
139 }
Selim Cinek2a739342016-03-17 10:28:55 -0700140 boolean prevSuppressed = group.suppressed;
Selim Cinek23c80342016-03-17 18:27:36 -0700141 group.suppressed = group.summary != null && !group.expanded
142 && (group.children.size() == 1
143 || (group.children.size() == 0
Julia Reynoldse46bb372016-03-17 11:05:58 -0400144 && group.summary.notification.getNotification().isGroupSummary()
Selim Cinek23c80342016-03-17 18:27:36 -0700145 && hasIsolatedChildren(group)));
Selim Cinek2a739342016-03-17 10:28:55 -0700146 if (prevSuppressed != group.suppressed) {
Selim Cinek967ed2a2016-04-08 18:29:11 -0700147 if (group.suppressed) {
148 handleSuppressedSummaryHeadsUpped(group.summary);
149 }
Selim Cinek2a739342016-03-17 10:28:55 -0700150 mListener.onGroupsChanged();
151 }
152 }
153
Selim Cinek23c80342016-03-17 18:27:36 -0700154 private boolean hasIsolatedChildren(NotificationGroup group) {
155 return getNumberOfIsolatedChildren(group.summary.notification.getGroupKey()) != 0;
156 }
157
158 private int getNumberOfIsolatedChildren(String groupKey) {
159 int count = 0;
Selim Cineka6c0cef2016-03-18 11:42:25 -0700160 for (StatusBarNotification sbn : mIsolatedEntries.values()) {
Selim Cinek23c80342016-03-17 18:27:36 -0700161 if (sbn.getGroupKey().equals(groupKey) && isIsolated(sbn)) {
162 count++;
163 }
164 }
165 return count;
166 }
167
Selim Cinek967ed2a2016-04-08 18:29:11 -0700168 private NotificationData.Entry getIsolatedChild(String groupKey) {
169 for (StatusBarNotification sbn : mIsolatedEntries.values()) {
170 if (sbn.getGroupKey().equals(groupKey) && isIsolated(sbn)) {
171 return mGroupMap.get(sbn.getKey()).summary;
172 }
173 }
174 return null;
175 }
176
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100177 public void onEntryUpdated(NotificationData.Entry entry,
178 StatusBarNotification oldNotification) {
Selim Cinekef5127e2015-12-21 16:55:58 -0800179 if (mGroupMap.get(getGroupKey(oldNotification)) != null) {
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100180 onEntryRemovedInternal(entry, oldNotification);
181 }
182 onEntryAdded(entry);
Selim Cineka6c0cef2016-03-18 11:42:25 -0700183 if (isIsolated(entry.notification)) {
184 mIsolatedEntries.put(entry.key, entry.notification);
Selim Cinek23c80342016-03-17 18:27:36 -0700185 String oldKey = oldNotification.getGroupKey();
186 String newKey = entry.notification.getGroupKey();
187 if (!oldKey.equals(newKey)) {
188 updateSuppression(mGroupMap.get(oldKey));
189 updateSuppression(mGroupMap.get(newKey));
190 }
191 }
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100192 }
193
Selim Cinek2a739342016-03-17 10:28:55 -0700194 public boolean isSummaryOfSuppressedGroup(StatusBarNotification sbn) {
Selim Cinek23c80342016-03-17 18:27:36 -0700195 return isGroupSuppressed(getGroupKey(sbn)) && sbn.getNotification().isGroupSummary();
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100196 }
197
Selim Cinek23c80342016-03-17 18:27:36 -0700198 public boolean isOnlyChildInSuppressedGroup(StatusBarNotification sbn) {
199 return isGroupSuppressed(sbn.getGroupKey())
Julia Reynoldse46bb372016-03-17 11:05:58 -0400200 && !sbn.getNotification().isGroupSummary()
Selim Cinek23c80342016-03-17 18:27:36 -0700201 && getTotalNumberOfChildren(sbn) == 1;
Selim Cinek2a739342016-03-17 10:28:55 -0700202 }
203
Selim Cinek23c80342016-03-17 18:27:36 -0700204 private int getTotalNumberOfChildren(StatusBarNotification sbn) {
205 return getNumberOfIsolatedChildren(sbn.getGroupKey())
206 + mGroupMap.get(sbn.getGroupKey()).children.size();
207 }
208
209 private boolean isGroupSuppressed(String groupKey) {
210 NotificationGroup group = mGroupMap.get(groupKey);
Selim Cinek2a739342016-03-17 10:28:55 -0700211 return group != null && group.suppressed;
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100212 }
213
Selim Cinek9c4c4142015-12-04 16:44:56 -0800214 public void setStatusBarState(int newState) {
215 if (mBarState == newState) {
216 return;
217 }
218 mBarState = newState;
219 if (mBarState == StatusBarState.KEYGUARD) {
Selim Cinek9184f9c2016-02-02 17:36:53 -0800220 collapseAllGroups();
221 }
222 }
223
224 public void collapseAllGroups() {
225 for (NotificationGroup group : mGroupMap.values()) {
226 if (group.expanded) {
227 setGroupExpanded(group, false);
Selim Cinek9c4c4142015-12-04 16:44:56 -0800228 }
Selim Cinek2a739342016-03-17 10:28:55 -0700229 updateSuppression(group);
Selim Cinek9c4c4142015-12-04 16:44:56 -0800230 }
231 }
232
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100233 /**
234 * @return whether a given notification is a child in a group which has a summary
235 */
236 public boolean isChildInGroupWithSummary(StatusBarNotification sbn) {
Selim Cinekef5127e2015-12-21 16:55:58 -0800237 if (!isGroupChild(sbn)) {
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100238 return false;
239 }
Selim Cinekef5127e2015-12-21 16:55:58 -0800240 NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
Selim Cinek2a739342016-03-17 10:28:55 -0700241 if (group == null || group.summary == null || group.suppressed) {
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100242 return false;
243 }
244 return true;
245 }
246
Selim Cinek263398f2015-10-21 17:40:23 -0700247 /**
248 * @return whether a given notification is a summary in a group which has children
249 */
250 public boolean isSummaryOfGroup(StatusBarNotification sbn) {
Selim Cinekef5127e2015-12-21 16:55:58 -0800251 if (!isGroupSummary(sbn)) {
Selim Cinek263398f2015-10-21 17:40:23 -0700252 return false;
253 }
Selim Cinekef5127e2015-12-21 16:55:58 -0800254 NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
Selim Cinek263398f2015-10-21 17:40:23 -0700255 if (group == null) {
256 return false;
257 }
258 return !group.children.isEmpty();
259 }
260
Selim Cinek23c80342016-03-17 18:27:36 -0700261 /**
262 * Get the summary of a specified status bar notification. For isolated notification this return
263 * itself.
264 */
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100265 public ExpandableNotificationRow getGroupSummary(StatusBarNotification sbn) {
Selim Cinek23c80342016-03-17 18:27:36 -0700266 return getGroupSummary(getGroupKey(sbn));
267 }
268
269 /**
270 * Similar to {@link #getGroupSummary(StatusBarNotification)} but doesn't get the visual summary
271 * but the logical summary, i.e when a child is isolated, it still returns the summary as if
272 * it wasn't isolated.
273 */
274 public ExpandableNotificationRow getLogicalGroupSummary(
275 StatusBarNotification sbn) {
276 return getGroupSummary(sbn.getGroupKey());
277 }
278
279 @Nullable
280 private ExpandableNotificationRow getGroupSummary(String groupKey) {
281 NotificationGroup group = mGroupMap.get(groupKey);
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100282 return group == null ? null
283 : group.summary == null ? null
Selim Cinek23c80342016-03-17 18:27:36 -0700284 : group.summary.row;
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100285 }
286
Selim Cinek83bc7832015-10-22 13:26:54 -0700287 public void toggleGroupExpansion(StatusBarNotification sbn) {
Selim Cinekef5127e2015-12-21 16:55:58 -0800288 NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
Selim Cinek83bc7832015-10-22 13:26:54 -0700289 if (group == null) {
290 return;
291 }
292 setGroupExpanded(group, !group.expanded);
293 }
294
Selim Cinekef5127e2015-12-21 16:55:58 -0800295 private boolean isIsolated(StatusBarNotification sbn) {
Selim Cineka6c0cef2016-03-18 11:42:25 -0700296 return mIsolatedEntries.containsKey(sbn.getKey());
Selim Cinekef5127e2015-12-21 16:55:58 -0800297 }
298
299 private boolean isGroupSummary(StatusBarNotification sbn) {
300 if (isIsolated(sbn)) {
301 return true;
302 }
303 return sbn.getNotification().isGroupSummary();
304 }
Julia Reynoldse46bb372016-03-17 11:05:58 -0400305
Selim Cinekef5127e2015-12-21 16:55:58 -0800306 private boolean isGroupChild(StatusBarNotification sbn) {
307 if (isIsolated(sbn)) {
308 return false;
309 }
Julia Reynoldse46bb372016-03-17 11:05:58 -0400310 return sbn.isGroup() && !sbn.getNotification().isGroupSummary();
Selim Cinekef5127e2015-12-21 16:55:58 -0800311 }
312
313 private String getGroupKey(StatusBarNotification sbn) {
314 if (isIsolated(sbn)) {
315 return sbn.getKey();
316 }
317 return sbn.getGroupKey();
318 }
319
320 @Override
321 public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) {
322 }
323
324 @Override
325 public void onHeadsUpPinned(ExpandableNotificationRow headsUp) {
326 }
327
328 @Override
329 public void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) {
330 }
331
332 @Override
333 public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {
334 final StatusBarNotification sbn = entry.notification;
335 if (entry.row.isHeadsUp()) {
Selim Cineka6c0cef2016-03-18 11:42:25 -0700336 if (shouldIsolate(sbn)) {
Selim Cinek23c80342016-03-17 18:27:36 -0700337 // We will be isolated now, so lets update the groups
338 onEntryRemovedInternal(entry, entry.notification);
Selim Cineka6c0cef2016-03-18 11:42:25 -0700339
340 mIsolatedEntries.put(sbn.getKey(), sbn);
341
Selim Cinek23c80342016-03-17 18:27:36 -0700342 onEntryAdded(entry);
343 // We also need to update the suppression of the old group, because this call comes
344 // even before the groupManager knows about the notification at all.
345 // When the notification gets added afterwards it is already isolated and therefore
346 // it doesn't lead to an update.
347 updateSuppression(mGroupMap.get(entry.notification.getGroupKey()));
348 mListener.onGroupsChanged();
Selim Cinek967ed2a2016-04-08 18:29:11 -0700349 } else {
350 handleSuppressedSummaryHeadsUpped(entry);
351
Selim Cinekef5127e2015-12-21 16:55:58 -0800352 }
353 } else {
Selim Cineka6c0cef2016-03-18 11:42:25 -0700354 if (mIsolatedEntries.containsKey(sbn.getKey())) {
355 // not isolated anymore, we need to update the groups
356 onEntryRemovedInternal(entry, entry.notification);
357 mIsolatedEntries.remove(sbn.getKey());
358 onEntryAdded(entry);
359 mListener.onGroupsChanged();
Selim Cinekef5127e2015-12-21 16:55:58 -0800360 }
361 }
362 }
363
Selim Cinek967ed2a2016-04-08 18:29:11 -0700364 private void handleSuppressedSummaryHeadsUpped(NotificationData.Entry entry) {
365 StatusBarNotification sbn = entry.notification;
366 if (!isGroupSuppressed(sbn.getGroupKey())
367 || !sbn.getNotification().isGroupSummary()
368 || !entry.row.isHeadsUp()) {
369 return;
370 }
371 // The parent of a suppressed group got huned, lets hun the child!
372 NotificationGroup notificationGroup = mGroupMap.get(sbn.getGroupKey());
373 if (notificationGroup != null) {
374 Iterator<NotificationData.Entry> iterator = notificationGroup.children.iterator();
375 NotificationData.Entry child = iterator.hasNext() ? iterator.next() : null;
376 if (child == null) {
377 child = getIsolatedChild(sbn.getGroupKey());
378 }
379 if (child != null) {
380 if (mHeadsUpManager.isHeadsUp(child.key)) {
381 mHeadsUpManager.updateNotification(child, true);
382 } else {
383 mHeadsUpManager.showNotification(child);
384 }
385 }
386 }
387 mHeadsUpManager.releaseImmediately(entry.key);
388 }
389
Selim Cineka6c0cef2016-03-18 11:42:25 -0700390 private boolean shouldIsolate(StatusBarNotification sbn) {
391 NotificationGroup notificationGroup = mGroupMap.get(sbn.getGroupKey());
Julia Reynoldse46bb372016-03-17 11:05:58 -0400392 return (sbn.isGroup() && !sbn.getNotification().isGroupSummary())
Selim Cineka6c0cef2016-03-18 11:42:25 -0700393 && (sbn.getNotification().fullScreenIntent != null
394 || notificationGroup == null
395 || !notificationGroup.expanded
396 || isGroupNotFullyVisible(notificationGroup));
397 }
398
399 private boolean isGroupNotFullyVisible(NotificationGroup notificationGroup) {
400 return notificationGroup.summary == null
401 || notificationGroup.summary.row.getClipTopOptimization() > 0
402 || notificationGroup.summary.row.getClipTopAmount() > 0
403 || notificationGroup.summary.row.getTranslationY() < 0;
404 }
405
Selim Cinek967ed2a2016-04-08 18:29:11 -0700406 public void setHeadsUpManager(HeadsUpManager headsUpManager) {
407 mHeadsUpManager = headsUpManager;
408 }
409
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100410 public static class NotificationGroup {
411 public final HashSet<NotificationData.Entry> children = new HashSet<>();
412 public NotificationData.Entry summary;
413 public boolean expanded;
Selim Cinek2a739342016-03-17 10:28:55 -0700414 /**
415 * Is this notification group suppressed, i.e its summary is hidden
416 */
417 public boolean suppressed;
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100418 }
419
420 public interface OnGroupChangeListener {
421 /**
422 * The expansion of a group has changed.
423 *
424 * @param changedRow the row for which the expansion has changed, which is also the summary
425 * @param expanded a boolean indicating the new expanded state
426 */
427 void onGroupExpansionChanged(ExpandableNotificationRow changedRow, boolean expanded);
428
429 /**
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100430 * A group of children just received a summary notification and should therefore become
431 * children of it.
432 *
433 * @param group the group created
434 */
435 void onGroupCreatedFromChildren(NotificationGroup group);
Selim Cinekef5127e2015-12-21 16:55:58 -0800436
437 /**
Selim Cinek2a739342016-03-17 10:28:55 -0700438 * The groups have changed. This can happen if the isolation of a child has changes or if a
439 * group became suppressed / unsuppressed
Selim Cinekef5127e2015-12-21 16:55:58 -0800440 */
Selim Cinek2a739342016-03-17 10:28:55 -0700441 void onGroupsChanged();
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100442 }
443}