blob: a27ec28b7a073dd7fc2ae2ae99ed53a863cae542 [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;
Julia Reynoldse46bb372016-03-17 11:05:58 -040029import java.util.Objects;
Selim Cinek25fd4e2b2015-02-20 17:46:07 +010030
31/**
32 * A class to handle notifications and their corresponding groups.
33 */
Selim Cinekef5127e2015-12-21 16:55:58 -080034public class NotificationGroupManager implements HeadsUpManager.OnHeadsUpChangedListener {
Selim Cinek25fd4e2b2015-02-20 17:46:07 +010035
36 private final HashMap<String, NotificationGroup> mGroupMap = new HashMap<>();
37 private OnGroupChangeListener mListener;
38 private int mBarState = -1;
Selim Cineka6c0cef2016-03-18 11:42:25 -070039 private HashMap<String, StatusBarNotification> mIsolatedEntries = new HashMap<>();
Selim Cinek25fd4e2b2015-02-20 17:46:07 +010040
41 public void setOnGroupChangeListener(OnGroupChangeListener listener) {
42 mListener = listener;
43 }
44
45 public boolean isGroupExpanded(StatusBarNotification sbn) {
Selim Cinekef5127e2015-12-21 16:55:58 -080046 NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
Selim Cinek25fd4e2b2015-02-20 17:46:07 +010047 if (group == null) {
48 return false;
49 }
50 return group.expanded;
51 }
52
53 public void setGroupExpanded(StatusBarNotification sbn, boolean expanded) {
Selim Cinekef5127e2015-12-21 16:55:58 -080054 NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
Selim Cinek25fd4e2b2015-02-20 17:46:07 +010055 if (group == null) {
56 return;
57 }
58 setGroupExpanded(group, expanded);
59 }
60
61 private void setGroupExpanded(NotificationGroup group, boolean expanded) {
62 group.expanded = expanded;
63 if (group.summary != null) {
64 mListener.onGroupExpansionChanged(group.summary.row, expanded);
65 }
66 }
67
68 public void onEntryRemoved(NotificationData.Entry removed) {
69 onEntryRemovedInternal(removed, removed.notification);
70 }
71
72 /**
73 * An entry was removed.
74 *
75 * @param removed the removed entry
76 * @param sbn the notification the entry has, which doesn't need to be the same as it's internal
77 * notification
78 */
79 private void onEntryRemovedInternal(NotificationData.Entry removed,
80 final StatusBarNotification sbn) {
Selim Cinekef5127e2015-12-21 16:55:58 -080081 String groupKey = getGroupKey(sbn);
Selim Cinek25fd4e2b2015-02-20 17:46:07 +010082 final NotificationGroup group = mGroupMap.get(groupKey);
Selim Cinek0b4aeab2015-09-01 17:07:38 -070083 if (group == null) {
84 // When an app posts 2 different notifications as summary of the same group, then a
85 // cancellation of the first notification removes this group.
86 // This situation is not supported and we will not allow such notifications anymore in
87 // the close future. See b/23676310 for reference.
88 return;
89 }
Selim Cinekef5127e2015-12-21 16:55:58 -080090 if (isGroupChild(sbn)) {
Selim Cinek25fd4e2b2015-02-20 17:46:07 +010091 group.children.remove(removed);
Selim Cineke73ad212015-11-03 19:11:08 -080092 } else {
93 group.summary = null;
Selim Cinek25fd4e2b2015-02-20 17:46:07 +010094 }
Selim Cinek2a739342016-03-17 10:28:55 -070095 updateSuppression(group);
Selim Cinek25fd4e2b2015-02-20 17:46:07 +010096 if (group.children.isEmpty()) {
97 if (group.summary == null) {
98 mGroupMap.remove(groupKey);
Selim Cinek25fd4e2b2015-02-20 17:46:07 +010099 }
100 }
101 }
102
Selim Cinekef5127e2015-12-21 16:55:58 -0800103 public void onEntryAdded(final NotificationData.Entry added) {
104 final StatusBarNotification sbn = added.notification;
105 boolean isGroupChild = isGroupChild(sbn);
106 String groupKey = getGroupKey(sbn);
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100107 NotificationGroup group = mGroupMap.get(groupKey);
108 if (group == null) {
109 group = new NotificationGroup();
110 mGroupMap.put(groupKey, group);
111 }
Selim Cinekef5127e2015-12-21 16:55:58 -0800112 if (isGroupChild) {
Selim Cineke73ad212015-11-03 19:11:08 -0800113 group.children.add(added);
Selim Cinek2a739342016-03-17 10:28:55 -0700114 updateSuppression(group);
Selim Cineke73ad212015-11-03 19:11:08 -0800115 } else {
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100116 group.summary = added;
Selim Cinekb5605e52015-02-20 18:21:41 +0100117 group.expanded = added.row.areChildrenExpanded();
Selim Cinek2a739342016-03-17 10:28:55 -0700118 updateSuppression(group);
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100119 if (!group.children.isEmpty()) {
120 mListener.onGroupCreatedFromChildren(group);
121 }
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100122 }
123 }
124
Julia Reynoldse46bb372016-03-17 11:05:58 -0400125 public void onEntryBundlingUpdated(final NotificationData.Entry updated,
126 final String overrideGroupKey) {
127 final StatusBarNotification oldSbn = updated.notification.clone();
128 if (!Objects.equals(oldSbn.getOverrideGroupKey(), overrideGroupKey)) {
129 updated.notification.setOverrideGroupKey(overrideGroupKey);
130 onEntryUpdated(updated, oldSbn);
131 }
132 }
133
Selim Cinek2a739342016-03-17 10:28:55 -0700134 private void updateSuppression(NotificationGroup group) {
Selim Cinek23c80342016-03-17 18:27:36 -0700135 if (group == null) {
136 return;
137 }
Selim Cinek2a739342016-03-17 10:28:55 -0700138 boolean prevSuppressed = group.suppressed;
Selim Cinek23c80342016-03-17 18:27:36 -0700139 group.suppressed = group.summary != null && !group.expanded
140 && (group.children.size() == 1
141 || (group.children.size() == 0
Julia Reynoldse46bb372016-03-17 11:05:58 -0400142 && group.summary.notification.getNotification().isGroupSummary()
Selim Cinek23c80342016-03-17 18:27:36 -0700143 && hasIsolatedChildren(group)));
Selim Cinek2a739342016-03-17 10:28:55 -0700144 if (prevSuppressed != group.suppressed) {
145 mListener.onGroupsChanged();
146 }
147 }
148
Selim Cinek23c80342016-03-17 18:27:36 -0700149 private boolean hasIsolatedChildren(NotificationGroup group) {
150 return getNumberOfIsolatedChildren(group.summary.notification.getGroupKey()) != 0;
151 }
152
153 private int getNumberOfIsolatedChildren(String groupKey) {
154 int count = 0;
Selim Cineka6c0cef2016-03-18 11:42:25 -0700155 for (StatusBarNotification sbn : mIsolatedEntries.values()) {
Selim Cinek23c80342016-03-17 18:27:36 -0700156 if (sbn.getGroupKey().equals(groupKey) && isIsolated(sbn)) {
157 count++;
158 }
159 }
160 return count;
161 }
162
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100163 public void onEntryUpdated(NotificationData.Entry entry,
164 StatusBarNotification oldNotification) {
Selim Cinekef5127e2015-12-21 16:55:58 -0800165 if (mGroupMap.get(getGroupKey(oldNotification)) != null) {
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100166 onEntryRemovedInternal(entry, oldNotification);
167 }
168 onEntryAdded(entry);
Selim Cineka6c0cef2016-03-18 11:42:25 -0700169 if (isIsolated(entry.notification)) {
170 mIsolatedEntries.put(entry.key, entry.notification);
Selim Cinek23c80342016-03-17 18:27:36 -0700171 String oldKey = oldNotification.getGroupKey();
172 String newKey = entry.notification.getGroupKey();
173 if (!oldKey.equals(newKey)) {
174 updateSuppression(mGroupMap.get(oldKey));
175 updateSuppression(mGroupMap.get(newKey));
176 }
177 }
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100178 }
179
Selim Cinek2a739342016-03-17 10:28:55 -0700180 public boolean isSummaryOfSuppressedGroup(StatusBarNotification sbn) {
Selim Cinek23c80342016-03-17 18:27:36 -0700181 return isGroupSuppressed(getGroupKey(sbn)) && sbn.getNotification().isGroupSummary();
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100182 }
183
Selim Cinek23c80342016-03-17 18:27:36 -0700184 public boolean isOnlyChildInSuppressedGroup(StatusBarNotification sbn) {
185 return isGroupSuppressed(sbn.getGroupKey())
Julia Reynoldse46bb372016-03-17 11:05:58 -0400186 && !sbn.getNotification().isGroupSummary()
Selim Cinek23c80342016-03-17 18:27:36 -0700187 && getTotalNumberOfChildren(sbn) == 1;
Selim Cinek2a739342016-03-17 10:28:55 -0700188 }
189
Selim Cinek23c80342016-03-17 18:27:36 -0700190 private int getTotalNumberOfChildren(StatusBarNotification sbn) {
191 return getNumberOfIsolatedChildren(sbn.getGroupKey())
192 + mGroupMap.get(sbn.getGroupKey()).children.size();
193 }
194
195 private boolean isGroupSuppressed(String groupKey) {
196 NotificationGroup group = mGroupMap.get(groupKey);
Selim Cinek2a739342016-03-17 10:28:55 -0700197 return group != null && group.suppressed;
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100198 }
199
Selim Cinek9c4c4142015-12-04 16:44:56 -0800200 public void setStatusBarState(int newState) {
201 if (mBarState == newState) {
202 return;
203 }
204 mBarState = newState;
205 if (mBarState == StatusBarState.KEYGUARD) {
Selim Cinek9184f9c2016-02-02 17:36:53 -0800206 collapseAllGroups();
207 }
208 }
209
210 public void collapseAllGroups() {
211 for (NotificationGroup group : mGroupMap.values()) {
212 if (group.expanded) {
213 setGroupExpanded(group, false);
Selim Cinek9c4c4142015-12-04 16:44:56 -0800214 }
Selim Cinek2a739342016-03-17 10:28:55 -0700215 updateSuppression(group);
Selim Cinek9c4c4142015-12-04 16:44:56 -0800216 }
217 }
218
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100219 /**
220 * @return whether a given notification is a child in a group which has a summary
221 */
222 public boolean isChildInGroupWithSummary(StatusBarNotification sbn) {
Selim Cinekef5127e2015-12-21 16:55:58 -0800223 if (!isGroupChild(sbn)) {
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100224 return false;
225 }
Selim Cinekef5127e2015-12-21 16:55:58 -0800226 NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
Selim Cinek2a739342016-03-17 10:28:55 -0700227 if (group == null || group.summary == null || group.suppressed) {
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100228 return false;
229 }
230 return true;
231 }
232
Selim Cinek263398f2015-10-21 17:40:23 -0700233 /**
234 * @return whether a given notification is a summary in a group which has children
235 */
236 public boolean isSummaryOfGroup(StatusBarNotification sbn) {
Selim Cinekef5127e2015-12-21 16:55:58 -0800237 if (!isGroupSummary(sbn)) {
Selim Cinek263398f2015-10-21 17:40:23 -0700238 return false;
239 }
Selim Cinekef5127e2015-12-21 16:55:58 -0800240 NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
Selim Cinek263398f2015-10-21 17:40:23 -0700241 if (group == null) {
242 return false;
243 }
244 return !group.children.isEmpty();
245 }
246
Selim Cinek23c80342016-03-17 18:27:36 -0700247 /**
248 * Get the summary of a specified status bar notification. For isolated notification this return
249 * itself.
250 */
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100251 public ExpandableNotificationRow getGroupSummary(StatusBarNotification sbn) {
Selim Cinek23c80342016-03-17 18:27:36 -0700252 return getGroupSummary(getGroupKey(sbn));
253 }
254
255 /**
256 * Similar to {@link #getGroupSummary(StatusBarNotification)} but doesn't get the visual summary
257 * but the logical summary, i.e when a child is isolated, it still returns the summary as if
258 * it wasn't isolated.
259 */
260 public ExpandableNotificationRow getLogicalGroupSummary(
261 StatusBarNotification sbn) {
262 return getGroupSummary(sbn.getGroupKey());
263 }
264
265 @Nullable
266 private ExpandableNotificationRow getGroupSummary(String groupKey) {
267 NotificationGroup group = mGroupMap.get(groupKey);
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100268 return group == null ? null
269 : group.summary == null ? null
Selim Cinek23c80342016-03-17 18:27:36 -0700270 : group.summary.row;
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100271 }
272
Selim Cinek83bc7832015-10-22 13:26:54 -0700273 public void toggleGroupExpansion(StatusBarNotification sbn) {
Selim Cinekef5127e2015-12-21 16:55:58 -0800274 NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
Selim Cinek83bc7832015-10-22 13:26:54 -0700275 if (group == null) {
276 return;
277 }
278 setGroupExpanded(group, !group.expanded);
279 }
280
Selim Cinekef5127e2015-12-21 16:55:58 -0800281 private boolean isIsolated(StatusBarNotification sbn) {
Selim Cineka6c0cef2016-03-18 11:42:25 -0700282 return mIsolatedEntries.containsKey(sbn.getKey());
Selim Cinekef5127e2015-12-21 16:55:58 -0800283 }
284
285 private boolean isGroupSummary(StatusBarNotification sbn) {
286 if (isIsolated(sbn)) {
287 return true;
288 }
289 return sbn.getNotification().isGroupSummary();
290 }
Julia Reynoldse46bb372016-03-17 11:05:58 -0400291
Selim Cinekef5127e2015-12-21 16:55:58 -0800292 private boolean isGroupChild(StatusBarNotification sbn) {
293 if (isIsolated(sbn)) {
294 return false;
295 }
Julia Reynoldse46bb372016-03-17 11:05:58 -0400296 return sbn.isGroup() && !sbn.getNotification().isGroupSummary();
Selim Cinekef5127e2015-12-21 16:55:58 -0800297 }
298
299 private String getGroupKey(StatusBarNotification sbn) {
300 if (isIsolated(sbn)) {
301 return sbn.getKey();
302 }
303 return sbn.getGroupKey();
304 }
305
306 @Override
307 public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) {
308 }
309
310 @Override
311 public void onHeadsUpPinned(ExpandableNotificationRow headsUp) {
312 }
313
314 @Override
315 public void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) {
316 }
317
318 @Override
319 public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) {
320 final StatusBarNotification sbn = entry.notification;
321 if (entry.row.isHeadsUp()) {
Selim Cineka6c0cef2016-03-18 11:42:25 -0700322 if (shouldIsolate(sbn)) {
Selim Cinek23c80342016-03-17 18:27:36 -0700323 // We will be isolated now, so lets update the groups
324 onEntryRemovedInternal(entry, entry.notification);
Selim Cineka6c0cef2016-03-18 11:42:25 -0700325
326 mIsolatedEntries.put(sbn.getKey(), sbn);
327
Selim Cinek23c80342016-03-17 18:27:36 -0700328 onEntryAdded(entry);
329 // We also need to update the suppression of the old group, because this call comes
330 // even before the groupManager knows about the notification at all.
331 // When the notification gets added afterwards it is already isolated and therefore
332 // it doesn't lead to an update.
333 updateSuppression(mGroupMap.get(entry.notification.getGroupKey()));
334 mListener.onGroupsChanged();
Selim Cinekef5127e2015-12-21 16:55:58 -0800335 }
336 } else {
Selim Cineka6c0cef2016-03-18 11:42:25 -0700337 if (mIsolatedEntries.containsKey(sbn.getKey())) {
338 // not isolated anymore, we need to update the groups
339 onEntryRemovedInternal(entry, entry.notification);
340 mIsolatedEntries.remove(sbn.getKey());
341 onEntryAdded(entry);
342 mListener.onGroupsChanged();
Selim Cinekef5127e2015-12-21 16:55:58 -0800343 }
344 }
345 }
346
Selim Cineka6c0cef2016-03-18 11:42:25 -0700347 private boolean shouldIsolate(StatusBarNotification sbn) {
348 NotificationGroup notificationGroup = mGroupMap.get(sbn.getGroupKey());
Julia Reynoldse46bb372016-03-17 11:05:58 -0400349 return (sbn.isGroup() && !sbn.getNotification().isGroupSummary())
Selim Cineka6c0cef2016-03-18 11:42:25 -0700350 && (sbn.getNotification().fullScreenIntent != null
351 || notificationGroup == null
352 || !notificationGroup.expanded
353 || isGroupNotFullyVisible(notificationGroup));
354 }
355
356 private boolean isGroupNotFullyVisible(NotificationGroup notificationGroup) {
357 return notificationGroup.summary == null
358 || notificationGroup.summary.row.getClipTopOptimization() > 0
359 || notificationGroup.summary.row.getClipTopAmount() > 0
360 || notificationGroup.summary.row.getTranslationY() < 0;
361 }
362
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100363 public static class NotificationGroup {
364 public final HashSet<NotificationData.Entry> children = new HashSet<>();
365 public NotificationData.Entry summary;
366 public boolean expanded;
Selim Cinek2a739342016-03-17 10:28:55 -0700367 /**
368 * Is this notification group suppressed, i.e its summary is hidden
369 */
370 public boolean suppressed;
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100371 }
372
373 public interface OnGroupChangeListener {
374 /**
375 * The expansion of a group has changed.
376 *
377 * @param changedRow the row for which the expansion has changed, which is also the summary
378 * @param expanded a boolean indicating the new expanded state
379 */
380 void onGroupExpansionChanged(ExpandableNotificationRow changedRow, boolean expanded);
381
382 /**
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100383 * A group of children just received a summary notification and should therefore become
384 * children of it.
385 *
386 * @param group the group created
387 */
388 void onGroupCreatedFromChildren(NotificationGroup group);
Selim Cinekef5127e2015-12-21 16:55:58 -0800389
390 /**
Selim Cinek2a739342016-03-17 10:28:55 -0700391 * The groups have changed. This can happen if the isolation of a child has changes or if a
392 * group became suppressed / unsuppressed
Selim Cinekef5127e2015-12-21 16:55:58 -0800393 */
Selim Cinek2a739342016-03-17 10:28:55 -0700394 void onGroupsChanged();
Selim Cinek25fd4e2b2015-02-20 17:46:07 +0100395 }
396}