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