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 27 import java.io.FileDescriptor; 28 import java.io.PrintWriter; 29 import java.util.ArrayList; 30 import java.util.HashMap; 31 import java.util.HashSet; 32 import java.util.Iterator; 33 import java.util.Map; 34 import java.util.Objects; 35 36 /** 37 * A class to handle notifications and their corresponding groups. 38 */ 39 public class NotificationGroupManager implements HeadsUpManager.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 setOnGroupChangeListener(OnGroupChangeListener listener)47 public void setOnGroupChangeListener(OnGroupChangeListener listener) { 48 mListener = listener; 49 } 50 isGroupExpanded(StatusBarNotification sbn)51 public boolean isGroupExpanded(StatusBarNotification sbn) { 52 NotificationGroup group = mGroupMap.get(getGroupKey(sbn)); 53 if (group == null) { 54 return false; 55 } 56 return group.expanded; 57 } 58 setGroupExpanded(StatusBarNotification sbn, boolean expanded)59 public void setGroupExpanded(StatusBarNotification sbn, boolean expanded) { 60 NotificationGroup group = mGroupMap.get(getGroupKey(sbn)); 61 if (group == null) { 62 return; 63 } 64 setGroupExpanded(group, expanded); 65 } 66 setGroupExpanded(NotificationGroup group, boolean expanded)67 private void setGroupExpanded(NotificationGroup group, boolean expanded) { 68 group.expanded = expanded; 69 if (group.summary != null) { 70 mListener.onGroupExpansionChanged(group.summary.row, expanded); 71 } 72 } 73 onEntryRemoved(NotificationData.Entry removed)74 public void onEntryRemoved(NotificationData.Entry removed) { 75 onEntryRemovedInternal(removed, removed.notification); 76 mIsolatedEntries.remove(removed.key); 77 } 78 79 /** 80 * An entry was removed. 81 * 82 * @param removed the removed entry 83 * @param sbn the notification the entry has, which doesn't need to be the same as it's internal 84 * notification 85 */ onEntryRemovedInternal(NotificationData.Entry removed, final StatusBarNotification sbn)86 private void onEntryRemovedInternal(NotificationData.Entry removed, 87 final StatusBarNotification sbn) { 88 String groupKey = getGroupKey(sbn); 89 final NotificationGroup group = mGroupMap.get(groupKey); 90 if (group == null) { 91 // When an app posts 2 different notifications as summary of the same group, then a 92 // cancellation of the first notification removes this group. 93 // This situation is not supported and we will not allow such notifications anymore in 94 // the close future. See b/23676310 for reference. 95 return; 96 } 97 if (isGroupChild(sbn)) { 98 group.children.remove(removed); 99 } else { 100 group.summary = null; 101 } 102 updateSuppression(group); 103 if (group.children.isEmpty()) { 104 if (group.summary == null) { 105 mGroupMap.remove(groupKey); 106 } 107 } 108 } 109 onEntryAdded(final NotificationData.Entry added)110 public void onEntryAdded(final NotificationData.Entry added) { 111 final StatusBarNotification sbn = added.notification; 112 boolean isGroupChild = isGroupChild(sbn); 113 String groupKey = getGroupKey(sbn); 114 NotificationGroup group = mGroupMap.get(groupKey); 115 if (group == null) { 116 group = new NotificationGroup(); 117 mGroupMap.put(groupKey, group); 118 } 119 if (isGroupChild) { 120 group.children.add(added); 121 updateSuppression(group); 122 } else { 123 group.summary = added; 124 group.expanded = added.row.areChildrenExpanded(); 125 updateSuppression(group); 126 if (!group.children.isEmpty()) { 127 HashSet<NotificationData.Entry> childrenCopy = 128 (HashSet<NotificationData.Entry>) group.children.clone(); 129 for (NotificationData.Entry child : childrenCopy) { 130 onEntryBecomingChild(child); 131 } 132 mListener.onGroupCreatedFromChildren(group); 133 } 134 } 135 } 136 onEntryBecomingChild(NotificationData.Entry entry)137 private void onEntryBecomingChild(NotificationData.Entry entry) { 138 if (entry.row.isHeadsUp()) { 139 onHeadsUpStateChanged(entry, true); 140 } 141 } 142 onEntryBundlingUpdated(final NotificationData.Entry updated, final String overrideGroupKey)143 public void onEntryBundlingUpdated(final NotificationData.Entry updated, 144 final String overrideGroupKey) { 145 final StatusBarNotification oldSbn = updated.notification.clone(); 146 if (!Objects.equals(oldSbn.getOverrideGroupKey(), overrideGroupKey)) { 147 updated.notification.setOverrideGroupKey(overrideGroupKey); 148 onEntryUpdated(updated, oldSbn); 149 } 150 } 151 updateSuppression(NotificationGroup group)152 private void updateSuppression(NotificationGroup group) { 153 if (group == null) { 154 return; 155 } 156 boolean prevSuppressed = group.suppressed; 157 group.suppressed = group.summary != null && !group.expanded 158 && (group.children.size() == 1 159 || (group.children.size() == 0 160 && group.summary.notification.getNotification().isGroupSummary() 161 && hasIsolatedChildren(group))); 162 if (prevSuppressed != group.suppressed) { 163 if (group.suppressed) { 164 handleSuppressedSummaryHeadsUpped(group.summary); 165 } 166 mListener.onGroupsChanged(); 167 } 168 } 169 hasIsolatedChildren(NotificationGroup group)170 private boolean hasIsolatedChildren(NotificationGroup group) { 171 return getNumberOfIsolatedChildren(group.summary.notification.getGroupKey()) != 0; 172 } 173 getNumberOfIsolatedChildren(String groupKey)174 private int getNumberOfIsolatedChildren(String groupKey) { 175 int count = 0; 176 for (StatusBarNotification sbn : mIsolatedEntries.values()) { 177 if (sbn.getGroupKey().equals(groupKey) && isIsolated(sbn)) { 178 count++; 179 } 180 } 181 return count; 182 } 183 getIsolatedChild(String groupKey)184 private NotificationData.Entry getIsolatedChild(String groupKey) { 185 for (StatusBarNotification sbn : mIsolatedEntries.values()) { 186 if (sbn.getGroupKey().equals(groupKey) && isIsolated(sbn)) { 187 return mGroupMap.get(sbn.getKey()).summary; 188 } 189 } 190 return null; 191 } 192 onEntryUpdated(NotificationData.Entry entry, StatusBarNotification oldNotification)193 public void onEntryUpdated(NotificationData.Entry entry, 194 StatusBarNotification oldNotification) { 195 if (mGroupMap.get(getGroupKey(oldNotification)) != null) { 196 onEntryRemovedInternal(entry, oldNotification); 197 } 198 onEntryAdded(entry); 199 if (isIsolated(entry.notification)) { 200 mIsolatedEntries.put(entry.key, entry.notification); 201 String oldKey = oldNotification.getGroupKey(); 202 String newKey = entry.notification.getGroupKey(); 203 if (!oldKey.equals(newKey)) { 204 updateSuppression(mGroupMap.get(oldKey)); 205 updateSuppression(mGroupMap.get(newKey)); 206 } 207 } else if (!isGroupChild(oldNotification) && isGroupChild(entry.notification)) { 208 onEntryBecomingChild(entry); 209 } 210 } 211 isSummaryOfSuppressedGroup(StatusBarNotification sbn)212 public boolean isSummaryOfSuppressedGroup(StatusBarNotification sbn) { 213 return isGroupSuppressed(getGroupKey(sbn)) && sbn.getNotification().isGroupSummary(); 214 } 215 isOnlyChild(StatusBarNotification sbn)216 private boolean isOnlyChild(StatusBarNotification sbn) { 217 return !sbn.getNotification().isGroupSummary() 218 && getTotalNumberOfChildren(sbn) == 1; 219 } 220 isOnlyChildInGroup(StatusBarNotification sbn)221 public boolean isOnlyChildInGroup(StatusBarNotification sbn) { 222 if (!isOnlyChild(sbn)) { 223 return false; 224 } 225 ExpandableNotificationRow logicalGroupSummary = getLogicalGroupSummary(sbn); 226 return logicalGroupSummary != null 227 && !logicalGroupSummary.getStatusBarNotification().equals(sbn); 228 } 229 getTotalNumberOfChildren(StatusBarNotification sbn)230 private int getTotalNumberOfChildren(StatusBarNotification sbn) { 231 int isolatedChildren = getNumberOfIsolatedChildren(sbn.getGroupKey()); 232 NotificationGroup group = mGroupMap.get(sbn.getGroupKey()); 233 int realChildren = group != null ? group.children.size() : 0; 234 return isolatedChildren + realChildren; 235 } 236 isGroupSuppressed(String groupKey)237 private boolean isGroupSuppressed(String groupKey) { 238 NotificationGroup group = mGroupMap.get(groupKey); 239 return group != null && group.suppressed; 240 } 241 setStatusBarState(int newState)242 public void setStatusBarState(int newState) { 243 if (mBarState == newState) { 244 return; 245 } 246 mBarState = newState; 247 if (mBarState == StatusBarState.KEYGUARD) { 248 collapseAllGroups(); 249 } 250 } 251 collapseAllGroups()252 public void collapseAllGroups() { 253 // Because notifications can become isolated when the group becomes suppressed it can 254 // lead to concurrent modifications while looping. We need to make a copy. 255 ArrayList<NotificationGroup> groupCopy = new ArrayList<>(mGroupMap.values()); 256 int size = groupCopy.size(); 257 for (int i = 0; i < size; i++) { 258 NotificationGroup group = groupCopy.get(i); 259 if (group.expanded) { 260 setGroupExpanded(group, false); 261 } 262 updateSuppression(group); 263 } 264 } 265 266 /** 267 * @return whether a given notification is a child in a group which has a summary 268 */ isChildInGroupWithSummary(StatusBarNotification sbn)269 public boolean isChildInGroupWithSummary(StatusBarNotification sbn) { 270 if (!isGroupChild(sbn)) { 271 return false; 272 } 273 NotificationGroup group = mGroupMap.get(getGroupKey(sbn)); 274 if (group == null || group.summary == null || group.suppressed) { 275 return false; 276 } 277 if (group.children.isEmpty()) { 278 // If the suppression of a group changes because the last child was removed, this can 279 // still be called temporarily because the child hasn't been fully removed yet. Let's 280 // make sure we still return false in that case. 281 return false; 282 } 283 return true; 284 } 285 286 /** 287 * @return whether a given notification is a summary in a group which has children 288 */ isSummaryOfGroup(StatusBarNotification sbn)289 public boolean isSummaryOfGroup(StatusBarNotification sbn) { 290 if (!isGroupSummary(sbn)) { 291 return false; 292 } 293 NotificationGroup group = mGroupMap.get(getGroupKey(sbn)); 294 if (group == null) { 295 return false; 296 } 297 return !group.children.isEmpty(); 298 } 299 300 /** 301 * Get the summary of a specified status bar notification. For isolated notification this return 302 * itself. 303 */ getGroupSummary(StatusBarNotification sbn)304 public ExpandableNotificationRow getGroupSummary(StatusBarNotification sbn) { 305 return getGroupSummary(getGroupKey(sbn)); 306 } 307 308 /** 309 * Similar to {@link #getGroupSummary(StatusBarNotification)} but doesn't get the visual summary 310 * but the logical summary, i.e when a child is isolated, it still returns the summary as if 311 * it wasn't isolated. 312 */ getLogicalGroupSummary( StatusBarNotification sbn)313 public ExpandableNotificationRow getLogicalGroupSummary( 314 StatusBarNotification sbn) { 315 return getGroupSummary(sbn.getGroupKey()); 316 } 317 318 @Nullable getGroupSummary(String groupKey)319 private ExpandableNotificationRow getGroupSummary(String groupKey) { 320 NotificationGroup group = mGroupMap.get(groupKey); 321 return group == null ? null 322 : group.summary == null ? null 323 : group.summary.row; 324 } 325 326 /** @return group expansion state after toggling. */ toggleGroupExpansion(StatusBarNotification sbn)327 public boolean toggleGroupExpansion(StatusBarNotification sbn) { 328 NotificationGroup group = mGroupMap.get(getGroupKey(sbn)); 329 if (group == null) { 330 return false; 331 } 332 setGroupExpanded(group, !group.expanded); 333 return group.expanded; 334 } 335 isIsolated(StatusBarNotification sbn)336 private boolean isIsolated(StatusBarNotification sbn) { 337 return mIsolatedEntries.containsKey(sbn.getKey()); 338 } 339 isGroupSummary(StatusBarNotification sbn)340 private boolean isGroupSummary(StatusBarNotification sbn) { 341 if (isIsolated(sbn)) { 342 return true; 343 } 344 return sbn.getNotification().isGroupSummary(); 345 } 346 isGroupChild(StatusBarNotification sbn)347 private boolean isGroupChild(StatusBarNotification sbn) { 348 if (isIsolated(sbn)) { 349 return false; 350 } 351 return sbn.isGroup() && !sbn.getNotification().isGroupSummary(); 352 } 353 getGroupKey(StatusBarNotification sbn)354 private String getGroupKey(StatusBarNotification sbn) { 355 if (isIsolated(sbn)) { 356 return sbn.getKey(); 357 } 358 return sbn.getGroupKey(); 359 } 360 361 @Override onHeadsUpPinnedModeChanged(boolean inPinnedMode)362 public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) { 363 } 364 365 @Override onHeadsUpPinned(ExpandableNotificationRow headsUp)366 public void onHeadsUpPinned(ExpandableNotificationRow headsUp) { 367 } 368 369 @Override onHeadsUpUnPinned(ExpandableNotificationRow headsUp)370 public void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) { 371 } 372 373 @Override onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp)374 public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) { 375 final StatusBarNotification sbn = entry.notification; 376 if (entry.row.isHeadsUp()) { 377 if (shouldIsolate(sbn)) { 378 // We will be isolated now, so lets update the groups 379 onEntryRemovedInternal(entry, entry.notification); 380 381 mIsolatedEntries.put(sbn.getKey(), sbn); 382 383 onEntryAdded(entry); 384 // We also need to update the suppression of the old group, because this call comes 385 // even before the groupManager knows about the notification at all. 386 // When the notification gets added afterwards it is already isolated and therefore 387 // it doesn't lead to an update. 388 updateSuppression(mGroupMap.get(entry.notification.getGroupKey())); 389 mListener.onGroupsChanged(); 390 } else { 391 handleSuppressedSummaryHeadsUpped(entry); 392 } 393 } else { 394 if (mIsolatedEntries.containsKey(sbn.getKey())) { 395 // not isolated anymore, we need to update the groups 396 onEntryRemovedInternal(entry, entry.notification); 397 mIsolatedEntries.remove(sbn.getKey()); 398 onEntryAdded(entry); 399 mListener.onGroupsChanged(); 400 } 401 } 402 } 403 handleSuppressedSummaryHeadsUpped(NotificationData.Entry entry)404 private void handleSuppressedSummaryHeadsUpped(NotificationData.Entry entry) { 405 StatusBarNotification sbn = entry.notification; 406 if (!isGroupSuppressed(sbn.getGroupKey()) 407 || !sbn.getNotification().isGroupSummary() 408 || !entry.row.isHeadsUp()) { 409 return; 410 } 411 // The parent of a suppressed group got huned, lets hun the child! 412 NotificationGroup notificationGroup = mGroupMap.get(sbn.getGroupKey()); 413 if (notificationGroup != null) { 414 Iterator<NotificationData.Entry> iterator = notificationGroup.children.iterator(); 415 NotificationData.Entry child = iterator.hasNext() ? iterator.next() : null; 416 if (child == null) { 417 child = getIsolatedChild(sbn.getGroupKey()); 418 } 419 if (child != null) { 420 if (mHeadsUpManager.isHeadsUp(child.key)) { 421 mHeadsUpManager.updateNotification(child, true); 422 } else { 423 mHeadsUpManager.showNotification(child); 424 } 425 } 426 } 427 mHeadsUpManager.releaseImmediately(entry.key); 428 } 429 shouldIsolate(StatusBarNotification sbn)430 private boolean shouldIsolate(StatusBarNotification sbn) { 431 NotificationGroup notificationGroup = mGroupMap.get(sbn.getGroupKey()); 432 return (sbn.isGroup() && !sbn.getNotification().isGroupSummary()) 433 && (sbn.getNotification().fullScreenIntent != null 434 || notificationGroup == null 435 || !notificationGroup.expanded 436 || isGroupNotFullyVisible(notificationGroup)); 437 } 438 isGroupNotFullyVisible(NotificationGroup notificationGroup)439 private boolean isGroupNotFullyVisible(NotificationGroup notificationGroup) { 440 return notificationGroup.summary == null 441 || notificationGroup.summary.row.getClipTopAmount() > 0 442 || notificationGroup.summary.row.getTranslationY() < 0; 443 } 444 setHeadsUpManager(HeadsUpManager headsUpManager)445 public void setHeadsUpManager(HeadsUpManager headsUpManager) { 446 mHeadsUpManager = headsUpManager; 447 } 448 dump(FileDescriptor fd, PrintWriter pw, String[] args)449 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 450 pw.println("GroupManager state:"); 451 pw.println(" number of groups: " + mGroupMap.size()); 452 for (Map.Entry<String, NotificationGroup> entry : mGroupMap.entrySet()) { 453 pw.println("\n key: " + entry.getKey()); pw.println(entry.getValue()); 454 } 455 pw.println("\n isolated entries: " + mIsolatedEntries.size()); 456 for (Map.Entry<String, StatusBarNotification> entry : mIsolatedEntries.entrySet()) { 457 pw.print(" "); pw.print(entry.getKey()); 458 pw.print(", "); pw.println(entry.getValue()); 459 } 460 } 461 462 public static class NotificationGroup { 463 public final HashSet<NotificationData.Entry> children = new HashSet<>(); 464 public NotificationData.Entry summary; 465 public boolean expanded; 466 /** 467 * Is this notification group suppressed, i.e its summary is hidden 468 */ 469 public boolean suppressed; 470 471 @Override toString()472 public String toString() { 473 String result = " summary:\n " 474 + (summary != null ? summary.notification : "null"); 475 result += "\n children size: " + children.size(); 476 for (NotificationData.Entry child : children) { 477 result += "\n " + child.notification; 478 } 479 return result; 480 } 481 } 482 483 public interface OnGroupChangeListener { 484 /** 485 * The expansion of a group has changed. 486 * 487 * @param changedRow the row for which the expansion has changed, which is also the summary 488 * @param expanded a boolean indicating the new expanded state 489 */ onGroupExpansionChanged(ExpandableNotificationRow changedRow, boolean expanded)490 void onGroupExpansionChanged(ExpandableNotificationRow changedRow, boolean expanded); 491 492 /** 493 * A group of children just received a summary notification and should therefore become 494 * children of it. 495 * 496 * @param group the group created 497 */ onGroupCreatedFromChildren(NotificationGroup group)498 void onGroupCreatedFromChildren(NotificationGroup group); 499 500 /** 501 * The groups have changed. This can happen if the isolation of a child has changes or if a 502 * group became suppressed / unsuppressed 503 */ onGroupsChanged()504 void onGroupsChanged(); 505 } 506 } 507