NotificationGroupManager.java revision c0b14b0e895d65ab428d5c05778aae37ee946e19
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 17package com.android.systemui.statusbar.phone; 18 19import android.service.notification.StatusBarNotification; 20import android.support.annotation.Nullable; 21 22import com.android.systemui.statusbar.ExpandableNotificationRow; 23import com.android.systemui.statusbar.NotificationData; 24import com.android.systemui.statusbar.StatusBarState; 25import com.android.systemui.statusbar.policy.HeadsUpManager; 26 27import java.io.FileDescriptor; 28import java.io.PrintWriter; 29import java.util.ArrayList; 30import java.util.HashMap; 31import java.util.HashSet; 32import java.util.Iterator; 33import java.util.Map; 34import java.util.Objects; 35 36/** 37 * A class to handle notifications and their corresponding groups. 38 */ 39public 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 47 public void setOnGroupChangeListener(OnGroupChangeListener listener) { 48 mListener = listener; 49 } 50 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 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 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 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 */ 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 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 137 private void onEntryBecomingChild(NotificationData.Entry entry) { 138 if (entry.row.isHeadsUp()) { 139 onHeadsUpStateChanged(entry, true); 140 } 141 } 142 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 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 170 private boolean hasIsolatedChildren(NotificationGroup group) { 171 return getNumberOfIsolatedChildren(group.summary.notification.getGroupKey()) != 0; 172 } 173 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 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 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 212 public boolean isSummaryOfSuppressedGroup(StatusBarNotification sbn) { 213 return isGroupSuppressed(getGroupKey(sbn)) && sbn.getNotification().isGroupSummary(); 214 } 215 216 public boolean isOnlyChildInSuppressedGroup(StatusBarNotification sbn) { 217 return isGroupSuppressed(sbn.getGroupKey()) 218 && !sbn.getNotification().isGroupSummary() 219 && getTotalNumberOfChildren(sbn) == 1; 220 } 221 222 private int getTotalNumberOfChildren(StatusBarNotification sbn) { 223 return getNumberOfIsolatedChildren(sbn.getGroupKey()) 224 + mGroupMap.get(sbn.getGroupKey()).children.size(); 225 } 226 227 private boolean isGroupSuppressed(String groupKey) { 228 NotificationGroup group = mGroupMap.get(groupKey); 229 return group != null && group.suppressed; 230 } 231 232 public void setStatusBarState(int newState) { 233 if (mBarState == newState) { 234 return; 235 } 236 mBarState = newState; 237 if (mBarState == StatusBarState.KEYGUARD) { 238 collapseAllGroups(); 239 } 240 } 241 242 public void collapseAllGroups() { 243 // Because notifications can become isolated when the group becomes suppressed it can 244 // lead to concurrent modifications while looping. We need to make a copy. 245 ArrayList<NotificationGroup> groupCopy = new ArrayList<>(mGroupMap.values()); 246 int size = groupCopy.size(); 247 for (int i = 0; i < size; i++) { 248 NotificationGroup group = groupCopy.get(i); 249 if (group.expanded) { 250 setGroupExpanded(group, false); 251 } 252 updateSuppression(group); 253 } 254 } 255 256 /** 257 * @return whether a given notification is a child in a group which has a summary 258 */ 259 public boolean isChildInGroupWithSummary(StatusBarNotification sbn) { 260 if (!isGroupChild(sbn)) { 261 return false; 262 } 263 NotificationGroup group = mGroupMap.get(getGroupKey(sbn)); 264 if (group == null || group.summary == null || group.suppressed) { 265 return false; 266 } 267 if (group.children.isEmpty()) { 268 // If the suppression of a group changes because the last child was removed, this can 269 // still be called temporarily because the child hasn't been fully removed yet. Let's 270 // make sure we still return false in that case. 271 return false; 272 } 273 return true; 274 } 275 276 /** 277 * @return whether a given notification is a summary in a group which has children 278 */ 279 public boolean isSummaryOfGroup(StatusBarNotification sbn) { 280 if (!isGroupSummary(sbn)) { 281 return false; 282 } 283 NotificationGroup group = mGroupMap.get(getGroupKey(sbn)); 284 if (group == null) { 285 return false; 286 } 287 return !group.children.isEmpty(); 288 } 289 290 /** 291 * Get the summary of a specified status bar notification. For isolated notification this return 292 * itself. 293 */ 294 public ExpandableNotificationRow getGroupSummary(StatusBarNotification sbn) { 295 return getGroupSummary(getGroupKey(sbn)); 296 } 297 298 /** 299 * Similar to {@link #getGroupSummary(StatusBarNotification)} but doesn't get the visual summary 300 * but the logical summary, i.e when a child is isolated, it still returns the summary as if 301 * it wasn't isolated. 302 */ 303 public ExpandableNotificationRow getLogicalGroupSummary( 304 StatusBarNotification sbn) { 305 return getGroupSummary(sbn.getGroupKey()); 306 } 307 308 @Nullable 309 private ExpandableNotificationRow getGroupSummary(String groupKey) { 310 NotificationGroup group = mGroupMap.get(groupKey); 311 return group == null ? null 312 : group.summary == null ? null 313 : group.summary.row; 314 } 315 316 public void toggleGroupExpansion(StatusBarNotification sbn) { 317 NotificationGroup group = mGroupMap.get(getGroupKey(sbn)); 318 if (group == null) { 319 return; 320 } 321 setGroupExpanded(group, !group.expanded); 322 } 323 324 private boolean isIsolated(StatusBarNotification sbn) { 325 return mIsolatedEntries.containsKey(sbn.getKey()); 326 } 327 328 private boolean isGroupSummary(StatusBarNotification sbn) { 329 if (isIsolated(sbn)) { 330 return true; 331 } 332 return sbn.getNotification().isGroupSummary(); 333 } 334 335 private boolean isGroupChild(StatusBarNotification sbn) { 336 if (isIsolated(sbn)) { 337 return false; 338 } 339 return sbn.isGroup() && !sbn.getNotification().isGroupSummary(); 340 } 341 342 private String getGroupKey(StatusBarNotification sbn) { 343 if (isIsolated(sbn)) { 344 return sbn.getKey(); 345 } 346 return sbn.getGroupKey(); 347 } 348 349 @Override 350 public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) { 351 } 352 353 @Override 354 public void onHeadsUpPinned(ExpandableNotificationRow headsUp) { 355 } 356 357 @Override 358 public void onHeadsUpUnPinned(ExpandableNotificationRow headsUp) { 359 } 360 361 @Override 362 public void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp) { 363 final StatusBarNotification sbn = entry.notification; 364 if (entry.row.isHeadsUp()) { 365 if (shouldIsolate(sbn)) { 366 // We will be isolated now, so lets update the groups 367 onEntryRemovedInternal(entry, entry.notification); 368 369 mIsolatedEntries.put(sbn.getKey(), sbn); 370 371 onEntryAdded(entry); 372 // We also need to update the suppression of the old group, because this call comes 373 // even before the groupManager knows about the notification at all. 374 // When the notification gets added afterwards it is already isolated and therefore 375 // it doesn't lead to an update. 376 updateSuppression(mGroupMap.get(entry.notification.getGroupKey())); 377 mListener.onGroupsChanged(); 378 } else { 379 handleSuppressedSummaryHeadsUpped(entry); 380 } 381 } else { 382 if (mIsolatedEntries.containsKey(sbn.getKey())) { 383 // not isolated anymore, we need to update the groups 384 onEntryRemovedInternal(entry, entry.notification); 385 mIsolatedEntries.remove(sbn.getKey()); 386 onEntryAdded(entry); 387 mListener.onGroupsChanged(); 388 } 389 } 390 } 391 392 private void handleSuppressedSummaryHeadsUpped(NotificationData.Entry entry) { 393 StatusBarNotification sbn = entry.notification; 394 if (!isGroupSuppressed(sbn.getGroupKey()) 395 || !sbn.getNotification().isGroupSummary() 396 || !entry.row.isHeadsUp()) { 397 return; 398 } 399 // The parent of a suppressed group got huned, lets hun the child! 400 NotificationGroup notificationGroup = mGroupMap.get(sbn.getGroupKey()); 401 if (notificationGroup != null) { 402 Iterator<NotificationData.Entry> iterator = notificationGroup.children.iterator(); 403 NotificationData.Entry child = iterator.hasNext() ? iterator.next() : null; 404 if (child == null) { 405 child = getIsolatedChild(sbn.getGroupKey()); 406 } 407 if (child != null) { 408 if (mHeadsUpManager.isHeadsUp(child.key)) { 409 mHeadsUpManager.updateNotification(child, true); 410 } else { 411 mHeadsUpManager.showNotification(child); 412 } 413 } 414 } 415 mHeadsUpManager.releaseImmediately(entry.key); 416 } 417 418 private boolean shouldIsolate(StatusBarNotification sbn) { 419 NotificationGroup notificationGroup = mGroupMap.get(sbn.getGroupKey()); 420 return (sbn.isGroup() && !sbn.getNotification().isGroupSummary()) 421 && (sbn.getNotification().fullScreenIntent != null 422 || notificationGroup == null 423 || !notificationGroup.expanded 424 || isGroupNotFullyVisible(notificationGroup)); 425 } 426 427 private boolean isGroupNotFullyVisible(NotificationGroup notificationGroup) { 428 return notificationGroup.summary == null 429 || notificationGroup.summary.row.getClipTopOptimization() > 0 430 || notificationGroup.summary.row.getClipTopAmount() > 0 431 || notificationGroup.summary.row.getTranslationY() < 0; 432 } 433 434 public void setHeadsUpManager(HeadsUpManager headsUpManager) { 435 mHeadsUpManager = headsUpManager; 436 } 437 438 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 439 pw.println("GroupManager state:"); 440 pw.println(" number of groups: " + mGroupMap.size()); 441 for (Map.Entry<String, NotificationGroup> entry : mGroupMap.entrySet()) { 442 pw.println("\n key: " + entry.getKey()); pw.println(entry.getValue()); 443 } 444 pw.println("\n isolated entries: " + mIsolatedEntries.size()); 445 for (Map.Entry<String, StatusBarNotification> entry : mIsolatedEntries.entrySet()) { 446 pw.print(" "); pw.print(entry.getKey()); 447 pw.print(", "); pw.println(entry.getValue()); 448 } 449 } 450 451 public static class NotificationGroup { 452 public final HashSet<NotificationData.Entry> children = new HashSet<>(); 453 public NotificationData.Entry summary; 454 public boolean expanded; 455 /** 456 * Is this notification group suppressed, i.e its summary is hidden 457 */ 458 public boolean suppressed; 459 460 @Override 461 public String toString() { 462 String result = " summary:\n " + summary.notification; 463 result += "\n children size: " + children.size(); 464 for (NotificationData.Entry child : children) { 465 result += "\n " + child.notification; 466 } 467 return result; 468 } 469 } 470 471 public interface OnGroupChangeListener { 472 /** 473 * The expansion of a group has changed. 474 * 475 * @param changedRow the row for which the expansion has changed, which is also the summary 476 * @param expanded a boolean indicating the new expanded state 477 */ 478 void onGroupExpansionChanged(ExpandableNotificationRow changedRow, boolean expanded); 479 480 /** 481 * A group of children just received a summary notification and should therefore become 482 * children of it. 483 * 484 * @param group the group created 485 */ 486 void onGroupCreatedFromChildren(NotificationGroup group); 487 488 /** 489 * The groups have changed. This can happen if the isolation of a child has changes or if a 490 * group became suppressed / unsuppressed 491 */ 492 void onGroupsChanged(); 493 } 494} 495