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