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.annotation.Nullable; 20 import android.service.notification.StatusBarNotification; 21 import android.util.ArraySet; 22 import android.util.Log; 23 24 import com.android.systemui.Dependency; 25 import com.android.systemui.bubbles.BubbleController; 26 import com.android.systemui.plugins.statusbar.StatusBarStateController; 27 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; 28 import com.android.systemui.statusbar.StatusBarState; 29 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 30 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; 31 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 32 import com.android.systemui.statusbar.policy.HeadsUpManager; 33 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; 34 35 import java.io.FileDescriptor; 36 import java.io.PrintWriter; 37 import java.util.ArrayList; 38 import java.util.HashMap; 39 import java.util.Map; 40 import java.util.Objects; 41 42 import javax.inject.Inject; 43 import javax.inject.Singleton; 44 45 import dagger.Lazy; 46 47 /** 48 * A class to handle notifications and their corresponding groups. 49 */ 50 @Singleton 51 public class NotificationGroupManager implements OnHeadsUpChangedListener, StateListener { 52 53 private static final String TAG = "NotificationGroupManager"; 54 private final HashMap<String, NotificationGroup> mGroupMap = new HashMap<>(); 55 private final ArraySet<OnGroupChangeListener> mListeners = new ArraySet<>(); 56 private final Lazy<PeopleNotificationIdentifier> mPeopleNotificationIdentifier; 57 private int mBarState = -1; 58 private HashMap<String, StatusBarNotification> mIsolatedEntries = new HashMap<>(); 59 private HeadsUpManager mHeadsUpManager; 60 private boolean mIsUpdatingUnchangedGroup; 61 @Nullable private BubbleController mBubbleController = null; 62 63 @Inject NotificationGroupManager( StatusBarStateController statusBarStateController, Lazy<PeopleNotificationIdentifier> peopleNotificationIdentifier)64 public NotificationGroupManager( 65 StatusBarStateController statusBarStateController, 66 Lazy<PeopleNotificationIdentifier> peopleNotificationIdentifier) { 67 statusBarStateController.addCallback(this); 68 mPeopleNotificationIdentifier = peopleNotificationIdentifier; 69 } 70 getBubbleController()71 private BubbleController getBubbleController() { 72 if (mBubbleController == null) { 73 mBubbleController = Dependency.get(BubbleController.class); 74 } 75 return mBubbleController; 76 } 77 78 /** 79 * Add a listener for changes to groups. 80 * 81 * @param listener listener to add 82 */ addOnGroupChangeListener(OnGroupChangeListener listener)83 public void addOnGroupChangeListener(OnGroupChangeListener listener) { 84 mListeners.add(listener); 85 } 86 isGroupExpanded(StatusBarNotification sbn)87 public boolean isGroupExpanded(StatusBarNotification sbn) { 88 NotificationGroup group = mGroupMap.get(getGroupKey(sbn)); 89 if (group == null) { 90 return false; 91 } 92 return group.expanded; 93 } 94 95 /** 96 * @return if the group that this notification is associated with logically is expanded 97 */ isLogicalGroupExpanded(StatusBarNotification sbn)98 public boolean isLogicalGroupExpanded(StatusBarNotification sbn) { 99 NotificationGroup group = mGroupMap.get(sbn.getGroupKey()); 100 if (group == null) { 101 return false; 102 } 103 return group.expanded; 104 } 105 setGroupExpanded(StatusBarNotification sbn, boolean expanded)106 public void setGroupExpanded(StatusBarNotification sbn, boolean expanded) { 107 NotificationGroup group = mGroupMap.get(getGroupKey(sbn)); 108 if (group == null) { 109 return; 110 } 111 setGroupExpanded(group, expanded); 112 } 113 setGroupExpanded(NotificationGroup group, boolean expanded)114 private void setGroupExpanded(NotificationGroup group, boolean expanded) { 115 group.expanded = expanded; 116 if (group.summary != null) { 117 for (OnGroupChangeListener listener : mListeners) { 118 listener.onGroupExpansionChanged(group.summary.getRow(), expanded); 119 } 120 } 121 } 122 onEntryRemoved(NotificationEntry removed)123 public void onEntryRemoved(NotificationEntry removed) { 124 onEntryRemovedInternal(removed, removed.getSbn()); 125 mIsolatedEntries.remove(removed.getKey()); 126 } 127 128 /** 129 * An entry was removed. 130 * 131 * @param removed the removed entry 132 * @param sbn the notification the entry has, which doesn't need to be the same as it's internal 133 * notification 134 */ onEntryRemovedInternal(NotificationEntry removed, final StatusBarNotification sbn)135 private void onEntryRemovedInternal(NotificationEntry removed, 136 final StatusBarNotification sbn) { 137 onEntryRemovedInternal(removed, sbn.getGroupKey(), sbn.isGroup(), 138 sbn.getNotification().isGroupSummary()); 139 } 140 onEntryRemovedInternal(NotificationEntry removed, String notifGroupKey, boolean isGroup, boolean isGroupSummary)141 private void onEntryRemovedInternal(NotificationEntry removed, String notifGroupKey, boolean 142 isGroup, boolean isGroupSummary) { 143 String groupKey = getGroupKey(removed.getKey(), notifGroupKey); 144 final NotificationGroup group = mGroupMap.get(groupKey); 145 if (group == null) { 146 // When an app posts 2 different notifications as summary of the same group, then a 147 // cancellation of the first notification removes this group. 148 // This situation is not supported and we will not allow such notifications anymore in 149 // the close future. See b/23676310 for reference. 150 return; 151 } 152 if (isGroupChild(removed.getKey(), isGroup, isGroupSummary)) { 153 group.children.remove(removed.getKey()); 154 } else { 155 group.summary = null; 156 } 157 updateSuppression(group); 158 if (group.children.isEmpty()) { 159 if (group.summary == null) { 160 mGroupMap.remove(groupKey); 161 for (OnGroupChangeListener listener : mListeners) { 162 listener.onGroupRemoved(group, groupKey); 163 } 164 } 165 } 166 } 167 168 /** 169 * Notify the group manager that a new entry was added 170 */ onEntryAdded(final NotificationEntry added)171 public void onEntryAdded(final NotificationEntry added) { 172 updateIsolation(added); 173 onEntryAddedInternal(added); 174 } 175 onEntryAddedInternal(final NotificationEntry added)176 private void onEntryAddedInternal(final NotificationEntry added) { 177 if (added.isRowRemoved()) { 178 added.setDebugThrowable(new Throwable()); 179 } 180 final StatusBarNotification sbn = added.getSbn(); 181 boolean isGroupChild = isGroupChild(sbn); 182 String groupKey = getGroupKey(sbn); 183 NotificationGroup group = mGroupMap.get(groupKey); 184 if (group == null) { 185 group = new NotificationGroup(); 186 mGroupMap.put(groupKey, group); 187 for (OnGroupChangeListener listener : mListeners) { 188 listener.onGroupCreated(group, groupKey); 189 } 190 } 191 if (isGroupChild) { 192 NotificationEntry existing = group.children.get(added.getKey()); 193 if (existing != null && existing != added) { 194 Throwable existingThrowable = existing.getDebugThrowable(); 195 Log.wtf(TAG, "Inconsistent entries found with the same key " + added.getKey() 196 + "existing removed: " + existing.isRowRemoved() 197 + (existingThrowable != null 198 ? Log.getStackTraceString(existingThrowable) + "\n": "") 199 + " added removed" + added.isRowRemoved() 200 , new Throwable()); 201 } 202 group.children.put(added.getKey(), added); 203 updateSuppression(group); 204 } else { 205 group.summary = added; 206 group.expanded = added.areChildrenExpanded(); 207 updateSuppression(group); 208 if (!group.children.isEmpty()) { 209 ArrayList<NotificationEntry> childrenCopy 210 = new ArrayList<>(group.children.values()); 211 for (NotificationEntry child : childrenCopy) { 212 onEntryBecomingChild(child); 213 } 214 for (OnGroupChangeListener listener : mListeners) { 215 listener.onGroupCreatedFromChildren(group); 216 } 217 } 218 } 219 } 220 onEntryBecomingChild(NotificationEntry entry)221 private void onEntryBecomingChild(NotificationEntry entry) { 222 updateIsolation(entry); 223 } 224 updateSuppression(NotificationGroup group)225 private void updateSuppression(NotificationGroup group) { 226 if (group == null) { 227 return; 228 } 229 int childCount = 0; 230 boolean hasBubbles = false; 231 for (NotificationEntry entry : group.children.values()) { 232 if (!getBubbleController().isBubbleNotificationSuppressedFromShade(entry)) { 233 childCount++; 234 } else { 235 hasBubbles = true; 236 } 237 } 238 239 boolean prevSuppressed = group.suppressed; 240 group.suppressed = group.summary != null && !group.expanded 241 && (childCount == 1 242 || (childCount == 0 243 && group.summary.getSbn().getNotification().isGroupSummary() 244 && (hasIsolatedChildren(group) || hasBubbles))); 245 if (prevSuppressed != group.suppressed) { 246 for (OnGroupChangeListener listener : mListeners) { 247 if (!mIsUpdatingUnchangedGroup) { 248 listener.onGroupSuppressionChanged(group, group.suppressed); 249 listener.onGroupsChanged(); 250 } 251 } 252 } 253 } 254 hasIsolatedChildren(NotificationGroup group)255 private boolean hasIsolatedChildren(NotificationGroup group) { 256 return getNumberOfIsolatedChildren(group.summary.getSbn().getGroupKey()) != 0; 257 } 258 getNumberOfIsolatedChildren(String groupKey)259 private int getNumberOfIsolatedChildren(String groupKey) { 260 int count = 0; 261 for (StatusBarNotification sbn : mIsolatedEntries.values()) { 262 if (sbn.getGroupKey().equals(groupKey) && isIsolated(sbn.getKey())) { 263 count++; 264 } 265 } 266 return count; 267 } 268 269 /** 270 * Update an entry's group information 271 * @param entry notification entry to update 272 * @param oldNotification previous notification info before this update 273 */ onEntryUpdated(NotificationEntry entry, StatusBarNotification oldNotification)274 public void onEntryUpdated(NotificationEntry entry, StatusBarNotification oldNotification) { 275 onEntryUpdated(entry, oldNotification.getGroupKey(), oldNotification.isGroup(), 276 oldNotification.getNotification().isGroupSummary()); 277 } 278 279 /** 280 * Updates an entry's group information 281 * @param entry notification entry to update 282 * @param oldGroupKey the notification's previous group key before this update 283 * @param oldIsGroup whether this notification was a group before this update 284 * @param oldIsGroupSummary whether this notification was a group summary before this update 285 */ onEntryUpdated(NotificationEntry entry, String oldGroupKey, boolean oldIsGroup, boolean oldIsGroupSummary)286 public void onEntryUpdated(NotificationEntry entry, String oldGroupKey, boolean oldIsGroup, 287 boolean oldIsGroupSummary) { 288 String newGroupKey = entry.getSbn().getGroupKey(); 289 boolean groupKeysChanged = !oldGroupKey.equals(newGroupKey); 290 boolean wasGroupChild = isGroupChild(entry.getKey(), oldIsGroup, oldIsGroupSummary); 291 boolean isGroupChild = isGroupChild(entry.getSbn()); 292 mIsUpdatingUnchangedGroup = !groupKeysChanged && wasGroupChild == isGroupChild; 293 if (mGroupMap.get(getGroupKey(entry.getKey(), oldGroupKey)) != null) { 294 onEntryRemovedInternal(entry, oldGroupKey, oldIsGroup, oldIsGroupSummary); 295 } 296 onEntryAddedInternal(entry); 297 mIsUpdatingUnchangedGroup = false; 298 if (isIsolated(entry.getSbn().getKey())) { 299 mIsolatedEntries.put(entry.getKey(), entry.getSbn()); 300 if (groupKeysChanged) { 301 updateSuppression(mGroupMap.get(oldGroupKey)); 302 updateSuppression(mGroupMap.get(newGroupKey)); 303 } 304 } else if (!wasGroupChild && isGroupChild) { 305 onEntryBecomingChild(entry); 306 } 307 } 308 isSummaryOfSuppressedGroup(StatusBarNotification sbn)309 public boolean isSummaryOfSuppressedGroup(StatusBarNotification sbn) { 310 return isGroupSuppressed(getGroupKey(sbn)) && sbn.getNotification().isGroupSummary(); 311 } 312 isOnlyChild(StatusBarNotification sbn)313 private boolean isOnlyChild(StatusBarNotification sbn) { 314 return !sbn.getNotification().isGroupSummary() 315 && getTotalNumberOfChildren(sbn) == 1; 316 } 317 isOnlyChildInGroup(StatusBarNotification sbn)318 public boolean isOnlyChildInGroup(StatusBarNotification sbn) { 319 if (!isOnlyChild(sbn)) { 320 return false; 321 } 322 NotificationEntry logicalGroupSummary = getLogicalGroupSummary(sbn); 323 return logicalGroupSummary != null 324 && !logicalGroupSummary.getSbn().equals(sbn); 325 } 326 getTotalNumberOfChildren(StatusBarNotification sbn)327 private int getTotalNumberOfChildren(StatusBarNotification sbn) { 328 int isolatedChildren = getNumberOfIsolatedChildren(sbn.getGroupKey()); 329 NotificationGroup group = mGroupMap.get(sbn.getGroupKey()); 330 int realChildren = group != null ? group.children.size() : 0; 331 return isolatedChildren + realChildren; 332 } 333 isGroupSuppressed(String groupKey)334 private boolean isGroupSuppressed(String groupKey) { 335 NotificationGroup group = mGroupMap.get(groupKey); 336 return group != null && group.suppressed; 337 } 338 setStatusBarState(int newState)339 private void setStatusBarState(int newState) { 340 mBarState = newState; 341 if (mBarState == StatusBarState.KEYGUARD) { 342 collapseAllGroups(); 343 } 344 } 345 collapseAllGroups()346 public void collapseAllGroups() { 347 // Because notifications can become isolated when the group becomes suppressed it can 348 // lead to concurrent modifications while looping. We need to make a copy. 349 ArrayList<NotificationGroup> groupCopy = new ArrayList<>(mGroupMap.values()); 350 int size = groupCopy.size(); 351 for (int i = 0; i < size; i++) { 352 NotificationGroup group = groupCopy.get(i); 353 if (group.expanded) { 354 setGroupExpanded(group, false); 355 } 356 updateSuppression(group); 357 } 358 } 359 360 /** 361 * @return whether a given notification is a child in a group which has a summary 362 */ isChildInGroupWithSummary(StatusBarNotification sbn)363 public boolean isChildInGroupWithSummary(StatusBarNotification sbn) { 364 if (!isGroupChild(sbn)) { 365 return false; 366 } 367 NotificationGroup group = mGroupMap.get(getGroupKey(sbn)); 368 if (group == null || group.summary == null || group.suppressed) { 369 return false; 370 } 371 if (group.children.isEmpty()) { 372 // If the suppression of a group changes because the last child was removed, this can 373 // still be called temporarily because the child hasn't been fully removed yet. Let's 374 // make sure we still return false in that case. 375 return false; 376 } 377 return true; 378 } 379 380 /** 381 * @return whether a given notification is a summary in a group which has children 382 */ isSummaryOfGroup(StatusBarNotification sbn)383 public boolean isSummaryOfGroup(StatusBarNotification sbn) { 384 if (!isGroupSummary(sbn)) { 385 return false; 386 } 387 NotificationGroup group = mGroupMap.get(getGroupKey(sbn)); 388 if (group == null || group.summary == null) { 389 return false; 390 } 391 return !group.children.isEmpty() && Objects.equals(group.summary.getSbn(), sbn); 392 } 393 394 /** 395 * Get the summary of a specified status bar notification. For isolated notification this return 396 * itself. 397 */ getGroupSummary(StatusBarNotification sbn)398 public NotificationEntry getGroupSummary(StatusBarNotification sbn) { 399 return getGroupSummary(getGroupKey(sbn)); 400 } 401 402 /** 403 * Similar to {@link #getGroupSummary(StatusBarNotification)} but doesn't get the visual summary 404 * but the logical summary, i.e when a child is isolated, it still returns the summary as if 405 * it wasn't isolated. 406 */ getLogicalGroupSummary(StatusBarNotification sbn)407 public NotificationEntry getLogicalGroupSummary(StatusBarNotification sbn) { 408 return getGroupSummary(sbn.getGroupKey()); 409 } 410 411 @Nullable getGroupSummary(String groupKey)412 private NotificationEntry getGroupSummary(String groupKey) { 413 NotificationGroup group = mGroupMap.get(groupKey); 414 //TODO: see if this can become an Entry 415 return group == null ? null 416 : group.summary; 417 } 418 419 /** 420 * Get the children that are logically in the summary's group, whether or not they are isolated. 421 * 422 * @param summary summary of a group 423 * @return list of the children 424 */ getLogicalChildren(StatusBarNotification summary)425 public ArrayList<NotificationEntry> getLogicalChildren(StatusBarNotification summary) { 426 NotificationGroup group = mGroupMap.get(summary.getGroupKey()); 427 if (group == null) { 428 return null; 429 } 430 ArrayList<NotificationEntry> children = new ArrayList<>(group.children.values()); 431 for (StatusBarNotification sbn : mIsolatedEntries.values()) { 432 if (sbn.getGroupKey().equals(summary.getGroupKey())) { 433 children.add(mGroupMap.get(sbn.getKey()).summary); 434 } 435 } 436 return children; 437 } 438 439 /** 440 * Get the children that are in the summary's group, not including those isolated. 441 * 442 * @param summary summary of a group 443 * @return list of the children 444 */ getChildren(StatusBarNotification summary)445 public @Nullable ArrayList<NotificationEntry> getChildren(StatusBarNotification summary) { 446 NotificationGroup group = mGroupMap.get(summary.getGroupKey()); 447 if (group == null) { 448 return null; 449 } 450 return new ArrayList<>(group.children.values()); 451 } 452 453 /** 454 * If there is a {@link NotificationGroup} associated with the provided entry, this method 455 * will update the suppression of that group. 456 */ updateSuppression(NotificationEntry entry)457 public void updateSuppression(NotificationEntry entry) { 458 NotificationGroup group = mGroupMap.get(getGroupKey(entry.getSbn())); 459 if (group != null) { 460 updateSuppression(group); 461 } 462 } 463 464 /** 465 * Get the group key. May differ from the one in the notification due to the notification 466 * being temporarily isolated. 467 * 468 * @param sbn notification to check 469 * @return the key of the notification 470 */ getGroupKey(StatusBarNotification sbn)471 public String getGroupKey(StatusBarNotification sbn) { 472 return getGroupKey(sbn.getKey(), sbn.getGroupKey()); 473 } 474 getGroupKey(String key, String groupKey)475 private String getGroupKey(String key, String groupKey) { 476 if (isIsolated(key)) { 477 return key; 478 } 479 return groupKey; 480 } 481 482 /** @return group expansion state after toggling. */ toggleGroupExpansion(StatusBarNotification sbn)483 public boolean toggleGroupExpansion(StatusBarNotification sbn) { 484 NotificationGroup group = mGroupMap.get(getGroupKey(sbn)); 485 if (group == null) { 486 return false; 487 } 488 setGroupExpanded(group, !group.expanded); 489 return group.expanded; 490 } 491 isIsolated(String sbnKey)492 private boolean isIsolated(String sbnKey) { 493 return mIsolatedEntries.containsKey(sbnKey); 494 } 495 496 /** 497 * Whether a notification is visually a group summary. 498 * 499 * @param sbn notification to check 500 * @return true if it is visually a group summary 501 */ isGroupSummary(StatusBarNotification sbn)502 public boolean isGroupSummary(StatusBarNotification sbn) { 503 if (isIsolated(sbn.getKey())) { 504 return true; 505 } 506 return sbn.getNotification().isGroupSummary(); 507 } 508 509 /** 510 * Whether a notification is visually a group child. 511 * 512 * @param sbn notification to check 513 * @return true if it is visually a group child 514 */ isGroupChild(StatusBarNotification sbn)515 public boolean isGroupChild(StatusBarNotification sbn) { 516 return isGroupChild(sbn.getKey(), sbn.isGroup(), sbn.getNotification().isGroupSummary()); 517 } 518 isGroupChild(String key, boolean isGroup, boolean isGroupSummary)519 private boolean isGroupChild(String key, boolean isGroup, boolean isGroupSummary) { 520 if (isIsolated(key)) { 521 return false; 522 } 523 return isGroup && !isGroupSummary; 524 } 525 526 @Override onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp)527 public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) { 528 updateIsolation(entry); 529 } 530 531 /** 532 * Whether a notification that is normally part of a group should be temporarily isolated from 533 * the group and put in their own group visually. This generally happens when the notification 534 * is alerting. 535 * 536 * @param entry the notification to check 537 * @return true if the entry should be isolated 538 */ 539 shouldIsolate(NotificationEntry entry)540 private boolean shouldIsolate(NotificationEntry entry) { 541 StatusBarNotification sbn = entry.getSbn(); 542 if (!sbn.isGroup() || sbn.getNotification().isGroupSummary()) { 543 return false; 544 } 545 int peopleNotificationType = mPeopleNotificationIdentifier.get().getPeopleNotificationType( 546 entry.getSbn(), entry.getRanking()); 547 if (peopleNotificationType == PeopleNotificationIdentifier.TYPE_IMPORTANT_PERSON) { 548 return true; 549 } 550 if (mHeadsUpManager != null && !mHeadsUpManager.isAlerting(entry.getKey())) { 551 return false; 552 } 553 NotificationGroup notificationGroup = mGroupMap.get(sbn.getGroupKey()); 554 return (sbn.getNotification().fullScreenIntent != null 555 || notificationGroup == null 556 || !notificationGroup.expanded 557 || isGroupNotFullyVisible(notificationGroup)); 558 } 559 560 /** 561 * Isolate a notification from its group so that it visually shows as its own group. 562 * 563 * @param entry the notification to isolate 564 */ isolateNotification(NotificationEntry entry)565 private void isolateNotification(NotificationEntry entry) { 566 StatusBarNotification sbn = entry.getSbn(); 567 568 // We will be isolated now, so lets update the groups 569 onEntryRemovedInternal(entry, entry.getSbn()); 570 571 mIsolatedEntries.put(sbn.getKey(), sbn); 572 573 onEntryAddedInternal(entry); 574 // We also need to update the suppression of the old group, because this call comes 575 // even before the groupManager knows about the notification at all. 576 // When the notification gets added afterwards it is already isolated and therefore 577 // it doesn't lead to an update. 578 updateSuppression(mGroupMap.get(entry.getSbn().getGroupKey())); 579 for (OnGroupChangeListener listener : mListeners) { 580 listener.onGroupsChanged(); 581 } 582 } 583 584 /** 585 * Update the isolation of an entry, splitting it from the group. 586 */ updateIsolation(NotificationEntry entry)587 public void updateIsolation(NotificationEntry entry) { 588 boolean isIsolated = isIsolated(entry.getSbn().getKey()); 589 if (shouldIsolate(entry)) { 590 if (!isIsolated) { 591 isolateNotification(entry); 592 } 593 } else if (isIsolated) { 594 stopIsolatingNotification(entry); 595 } 596 } 597 598 /** 599 * Stop isolating a notification and re-group it with its original logical group. 600 * 601 * @param entry the notification to un-isolate 602 */ stopIsolatingNotification(NotificationEntry entry)603 private void stopIsolatingNotification(NotificationEntry entry) { 604 StatusBarNotification sbn = entry.getSbn(); 605 if (isIsolated(sbn.getKey())) { 606 // not isolated anymore, we need to update the groups 607 onEntryRemovedInternal(entry, entry.getSbn()); 608 mIsolatedEntries.remove(sbn.getKey()); 609 onEntryAddedInternal(entry); 610 for (OnGroupChangeListener listener : mListeners) { 611 listener.onGroupsChanged(); 612 } 613 } 614 } 615 isGroupNotFullyVisible(NotificationGroup notificationGroup)616 private boolean isGroupNotFullyVisible(NotificationGroup notificationGroup) { 617 return notificationGroup.summary == null 618 || notificationGroup.summary.isGroupNotFullyVisible(); 619 } 620 setHeadsUpManager(HeadsUpManager headsUpManager)621 public void setHeadsUpManager(HeadsUpManager headsUpManager) { 622 mHeadsUpManager = headsUpManager; 623 } 624 dump(FileDescriptor fd, PrintWriter pw, String[] args)625 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 626 pw.println("GroupManager state:"); 627 pw.println(" number of groups: " + mGroupMap.size()); 628 for (Map.Entry<String, NotificationGroup> entry : mGroupMap.entrySet()) { 629 pw.println("\n key: " + entry.getKey()); pw.println(entry.getValue()); 630 } 631 pw.println("\n isolated entries: " + mIsolatedEntries.size()); 632 for (Map.Entry<String, StatusBarNotification> entry : mIsolatedEntries.entrySet()) { 633 pw.print(" "); pw.print(entry.getKey()); 634 pw.print(", "); pw.println(entry.getValue()); 635 } 636 } 637 638 @Override onStateChanged(int newState)639 public void onStateChanged(int newState) { 640 setStatusBarState(newState); 641 } 642 643 public static class NotificationGroup { 644 public final HashMap<String, NotificationEntry> children = new HashMap<>(); 645 public NotificationEntry summary; 646 public boolean expanded; 647 /** 648 * Is this notification group suppressed, i.e its summary is hidden 649 */ 650 public boolean suppressed; 651 652 @Override toString()653 public String toString() { 654 String result = " summary:\n " 655 + (summary != null ? summary.getSbn() : "null") 656 + (summary != null && summary.getDebugThrowable() != null 657 ? Log.getStackTraceString(summary.getDebugThrowable()) 658 : ""); 659 result += "\n children size: " + children.size(); 660 for (NotificationEntry child : children.values()) { 661 result += "\n " + child.getSbn() 662 + (child.getDebugThrowable() != null 663 ? Log.getStackTraceString(child.getDebugThrowable()) 664 : ""); 665 } 666 result += "\n summary suppressed: " + suppressed; 667 return result; 668 } 669 } 670 671 public interface OnGroupChangeListener { 672 673 /** 674 * A new group has been created. 675 * 676 * @param group the group that was created 677 * @param groupKey the group's key 678 */ onGroupCreated(NotificationGroup group, String groupKey)679 default void onGroupCreated(NotificationGroup group, String groupKey) {} 680 681 /** 682 * A group has been removed. 683 * 684 * @param group the group that was removed 685 * @param groupKey the group's key 686 */ onGroupRemoved(NotificationGroup group, String groupKey)687 default void onGroupRemoved(NotificationGroup group, String groupKey) {} 688 689 /** 690 * The suppression of a group has changed. 691 * 692 * @param group the group that has changed 693 * @param suppressed true if the group is now suppressed, false o/w 694 */ onGroupSuppressionChanged(NotificationGroup group, boolean suppressed)695 default void onGroupSuppressionChanged(NotificationGroup group, boolean suppressed) {} 696 697 /** 698 * The expansion of a group has changed. 699 * 700 * @param changedRow the row for which the expansion has changed, which is also the summary 701 * @param expanded a boolean indicating the new expanded state 702 */ onGroupExpansionChanged(ExpandableNotificationRow changedRow, boolean expanded)703 default void onGroupExpansionChanged(ExpandableNotificationRow changedRow, 704 boolean expanded) {} 705 706 /** 707 * A group of children just received a summary notification and should therefore become 708 * children of it. 709 * 710 * @param group the group created 711 */ onGroupCreatedFromChildren(NotificationGroup group)712 default void onGroupCreatedFromChildren(NotificationGroup group) {} 713 714 /** 715 * The groups have changed. This can happen if the isolation of a child has changes or if a 716 * group became suppressed / unsuppressed 717 */ onGroupsChanged()718 default void onGroupsChanged() {} 719 } 720 } 721