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