1 /* 2 * Copyright (C) 2018 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.NonNull; 20 import android.annotation.Nullable; 21 import android.app.Notification; 22 import android.os.SystemClock; 23 import android.service.notification.StatusBarNotification; 24 import android.util.ArrayMap; 25 26 import com.android.internal.statusbar.NotificationVisibility; 27 import com.android.systemui.Dependency; 28 import com.android.systemui.plugins.statusbar.StatusBarStateController; 29 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; 30 import com.android.systemui.statusbar.AlertingNotificationManager; 31 import com.android.systemui.statusbar.AmbientPulseManager; 32 import com.android.systemui.statusbar.AmbientPulseManager.OnAmbientChangedListener; 33 import com.android.systemui.statusbar.InflationTask; 34 import com.android.systemui.statusbar.notification.NotificationEntryListener; 35 import com.android.systemui.statusbar.notification.NotificationEntryManager; 36 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 37 import com.android.systemui.statusbar.notification.row.NotificationContentInflater.AsyncInflationTask; 38 import com.android.systemui.statusbar.notification.row.NotificationContentInflater.InflationFlag; 39 import com.android.systemui.statusbar.phone.NotificationGroupManager.NotificationGroup; 40 import com.android.systemui.statusbar.phone.NotificationGroupManager.OnGroupChangeListener; 41 import com.android.systemui.statusbar.policy.HeadsUpManager; 42 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; 43 44 import java.util.ArrayList; 45 import java.util.Objects; 46 47 import javax.inject.Inject; 48 import javax.inject.Singleton; 49 50 /** 51 * A helper class dealing with the alert interactions between {@link NotificationGroupManager}, 52 * {@link HeadsUpManager}, {@link AmbientPulseManager}. In particular, this class deals with keeping 53 * the correct notification in a group alerting based off the group suppression. 54 */ 55 @Singleton 56 public class NotificationGroupAlertTransferHelper implements OnHeadsUpChangedListener, 57 OnAmbientChangedListener, StateListener { 58 59 private static final long ALERT_TRANSFER_TIMEOUT = 300; 60 61 /** 62 * The list of entries containing group alert metadata for each group. Keyed by group key. 63 */ 64 private final ArrayMap<String, GroupAlertEntry> mGroupAlertEntries = new ArrayMap<>(); 65 66 /** 67 * The list of entries currently inflating that should alert after inflation. Keyed by 68 * notification key. 69 */ 70 private final ArrayMap<String, PendingAlertInfo> mPendingAlerts = new ArrayMap<>(); 71 72 private HeadsUpManager mHeadsUpManager; 73 private final AmbientPulseManager mAmbientPulseManager = 74 Dependency.get(AmbientPulseManager.class); 75 private final NotificationGroupManager mGroupManager = 76 Dependency.get(NotificationGroupManager.class); 77 78 private NotificationEntryManager mEntryManager; 79 80 private boolean mIsDozing; 81 82 @Inject NotificationGroupAlertTransferHelper()83 public NotificationGroupAlertTransferHelper() { 84 Dependency.get(StatusBarStateController.class).addCallback(this); 85 } 86 87 /** Causes the TransferHelper to register itself as a listener to the appropriate classes. */ bind(NotificationEntryManager entryManager, NotificationGroupManager groupManager)88 public void bind(NotificationEntryManager entryManager, 89 NotificationGroupManager groupManager) { 90 if (mEntryManager != null) { 91 throw new IllegalStateException("Already bound."); 92 } 93 94 // TODO(b/119637830): It would be good if GroupManager already had all pending notifications 95 // as normal children (i.e. add notifications to GroupManager before inflation) so that we 96 // don't have to have this dependency. We'd also have to worry less about the suppression 97 // not being up to date. 98 mEntryManager = entryManager; 99 100 mEntryManager.addNotificationEntryListener(mNotificationEntryListener); 101 groupManager.addOnGroupChangeListener(mOnGroupChangeListener); 102 } 103 104 /** 105 * Whether or not a notification has transferred its alert state to the notification and 106 * the notification should alert after inflating. 107 * 108 * @param entry notification to check 109 * @return true if the entry was transferred to and should inflate + alert 110 */ isAlertTransferPending(@onNull NotificationEntry entry)111 public boolean isAlertTransferPending(@NonNull NotificationEntry entry) { 112 PendingAlertInfo alertInfo = mPendingAlerts.get(entry.key); 113 return alertInfo != null && alertInfo.isStillValid(); 114 } 115 setHeadsUpManager(HeadsUpManager headsUpManager)116 public void setHeadsUpManager(HeadsUpManager headsUpManager) { 117 mHeadsUpManager = headsUpManager; 118 } 119 120 @Override onStateChanged(int newState)121 public void onStateChanged(int newState) {} 122 123 @Override onDozingChanged(boolean isDozing)124 public void onDozingChanged(boolean isDozing) { 125 if (mIsDozing != isDozing) { 126 for (GroupAlertEntry groupAlertEntry : mGroupAlertEntries.values()) { 127 groupAlertEntry.mLastAlertTransferTime = 0; 128 groupAlertEntry.mAlertSummaryOnNextAddition = false; 129 } 130 } 131 mIsDozing = isDozing; 132 } 133 134 private final OnGroupChangeListener mOnGroupChangeListener = new OnGroupChangeListener() { 135 @Override 136 public void onGroupCreated(NotificationGroup group, String groupKey) { 137 mGroupAlertEntries.put(groupKey, new GroupAlertEntry(group)); 138 } 139 140 @Override 141 public void onGroupRemoved(NotificationGroup group, String groupKey) { 142 mGroupAlertEntries.remove(groupKey); 143 } 144 145 @Override 146 public void onGroupSuppressionChanged(NotificationGroup group, boolean suppressed) { 147 AlertingNotificationManager alertManager = getActiveAlertManager(); 148 if (suppressed) { 149 if (alertManager.isAlerting(group.summary.key)) { 150 handleSuppressedSummaryAlerted(group.summary, alertManager); 151 } 152 } else { 153 // Group summary can be null if we are no longer suppressed because the summary was 154 // removed. In that case, we don't need to alert the summary. 155 if (group.summary == null) { 156 return; 157 } 158 GroupAlertEntry groupAlertEntry = mGroupAlertEntries.get(mGroupManager.getGroupKey( 159 group.summary.notification)); 160 // Group is no longer suppressed. We should check if we need to transfer the alert 161 // back to the summary now that it's no longer suppressed. 162 if (groupAlertEntry.mAlertSummaryOnNextAddition) { 163 if (!alertManager.isAlerting(group.summary.key)) { 164 alertNotificationWhenPossible(group.summary, alertManager); 165 } 166 groupAlertEntry.mAlertSummaryOnNextAddition = false; 167 } else { 168 checkShouldTransferBack(groupAlertEntry); 169 } 170 } 171 } 172 }; 173 174 @Override onAmbientStateChanged(NotificationEntry entry, boolean isAmbient)175 public void onAmbientStateChanged(NotificationEntry entry, boolean isAmbient) { 176 onAlertStateChanged(entry, isAmbient, mAmbientPulseManager); 177 } 178 179 @Override onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp)180 public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) { 181 onAlertStateChanged(entry, isHeadsUp, mHeadsUpManager); 182 } 183 onAlertStateChanged(NotificationEntry entry, boolean isAlerting, AlertingNotificationManager alertManager)184 private void onAlertStateChanged(NotificationEntry entry, boolean isAlerting, 185 AlertingNotificationManager alertManager) { 186 if (isAlerting && mGroupManager.isSummaryOfSuppressedGroup(entry.notification)) { 187 handleSuppressedSummaryAlerted(entry, alertManager); 188 } 189 } 190 191 private final NotificationEntryListener mNotificationEntryListener = 192 new NotificationEntryListener() { 193 // Called when a new notification has been posted but is not inflated yet. We use this to 194 // see as early as we can if we need to abort a transfer. 195 @Override 196 public void onPendingEntryAdded(NotificationEntry entry) { 197 String groupKey = mGroupManager.getGroupKey(entry.notification); 198 GroupAlertEntry groupAlertEntry = mGroupAlertEntries.get(groupKey); 199 if (groupAlertEntry != null) { 200 checkShouldTransferBack(groupAlertEntry); 201 } 202 } 203 204 // Called when the entry's reinflation has finished. If there is an alert pending, we 205 // then show the alert. 206 @Override 207 public void onEntryReinflated(NotificationEntry entry) { 208 PendingAlertInfo alertInfo = mPendingAlerts.remove(entry.key); 209 if (alertInfo != null) { 210 if (alertInfo.isStillValid()) { 211 alertNotificationWhenPossible(entry, getActiveAlertManager()); 212 } else { 213 // The transfer is no longer valid. Free the content. 214 entry.getRow().freeContentViewWhenSafe( 215 alertInfo.mAlertManager.getContentFlag()); 216 } 217 } 218 } 219 220 @Override 221 public void onEntryRemoved( 222 @Nullable NotificationEntry entry, 223 NotificationVisibility visibility, 224 boolean removedByUser) { 225 // Removes any alerts pending on this entry. Note that this will not stop any inflation 226 // tasks started by a transfer, so this should only be used as clean-up for when 227 // inflation is stopped and the pending alert no longer needs to happen. 228 mPendingAlerts.remove(entry.key); 229 } 230 }; 231 232 /** 233 * Gets the number of new notifications pending inflation that will be added to the group 234 * but currently aren't and should not alert. 235 * 236 * @param group group to check 237 * @return the number of new notifications that will be added to the group 238 */ getPendingChildrenNotAlerting(@onNull NotificationGroup group)239 private int getPendingChildrenNotAlerting(@NonNull NotificationGroup group) { 240 if (mEntryManager == null) { 241 return 0; 242 } 243 int number = 0; 244 Iterable<NotificationEntry> values = mEntryManager.getPendingNotificationsIterator(); 245 for (NotificationEntry entry : values) { 246 if (isPendingNotificationInGroup(entry, group) && onlySummaryAlerts(entry)) { 247 number++; 248 } 249 } 250 return number; 251 } 252 253 /** 254 * Checks if the pending inflations will add children to this group. 255 * 256 * @param group group to check 257 * @return true if a pending notification will add to this group 258 */ pendingInflationsWillAddChildren(@onNull NotificationGroup group)259 private boolean pendingInflationsWillAddChildren(@NonNull NotificationGroup group) { 260 if (mEntryManager == null) { 261 return false; 262 } 263 Iterable<NotificationEntry> values = mEntryManager.getPendingNotificationsIterator(); 264 for (NotificationEntry entry : values) { 265 if (isPendingNotificationInGroup(entry, group)) { 266 return true; 267 } 268 } 269 return false; 270 } 271 272 /** 273 * Checks if a new pending notification will be added to the group. 274 * 275 * @param entry pending notification 276 * @param group group to check 277 * @return true if the notification will add to the group, false o/w 278 */ isPendingNotificationInGroup(@onNull NotificationEntry entry, @NonNull NotificationGroup group)279 private boolean isPendingNotificationInGroup(@NonNull NotificationEntry entry, 280 @NonNull NotificationGroup group) { 281 String groupKey = mGroupManager.getGroupKey(group.summary.notification); 282 return mGroupManager.isGroupChild(entry.notification) 283 && Objects.equals(mGroupManager.getGroupKey(entry.notification), groupKey) 284 && !group.children.containsKey(entry.key); 285 } 286 287 /** 288 * Handles the scenario where a summary that has been suppressed is alerted. A suppressed 289 * summary should for all intents and purposes be invisible to the user and as a result should 290 * not alert. When this is the case, it is our responsibility to pass the alert to the 291 * appropriate child which will be the representative notification alerting for the group. 292 * 293 * @param summary the summary that is suppressed and alerting 294 * @param alertManager the alert manager that manages the alerting summary 295 */ handleSuppressedSummaryAlerted(@onNull NotificationEntry summary, @NonNull AlertingNotificationManager alertManager)296 private void handleSuppressedSummaryAlerted(@NonNull NotificationEntry summary, 297 @NonNull AlertingNotificationManager alertManager) { 298 StatusBarNotification sbn = summary.notification; 299 GroupAlertEntry groupAlertEntry = 300 mGroupAlertEntries.get(mGroupManager.getGroupKey(sbn)); 301 if (!mGroupManager.isSummaryOfSuppressedGroup(summary.notification) 302 || !alertManager.isAlerting(sbn.getKey()) 303 || groupAlertEntry == null) { 304 return; 305 } 306 307 if (pendingInflationsWillAddChildren(groupAlertEntry.mGroup)) { 308 // New children will actually be added to this group, let's not transfer the alert. 309 return; 310 } 311 312 NotificationEntry child = mGroupManager.getLogicalChildren(summary.notification).iterator().next(); 313 if (child != null) { 314 if (child.getRow().keepInParent() 315 || child.isRowRemoved() 316 || child.isRowDismissed()) { 317 // The notification is actually already removed. No need to alert it. 318 return; 319 } 320 if (!alertManager.isAlerting(child.key) && onlySummaryAlerts(summary)) { 321 groupAlertEntry.mLastAlertTransferTime = SystemClock.elapsedRealtime(); 322 } 323 transferAlertState(summary, child, alertManager); 324 } 325 } 326 327 /** 328 * Transfers the alert state one entry to another. We remove the alert from the first entry 329 * immediately to have the incorrect one up as short as possible. The second should alert 330 * when possible. 331 * 332 * @param fromEntry entry to transfer alert from 333 * @param toEntry entry to transfer to 334 * @param alertManager alert manager for the alert type 335 */ transferAlertState(@onNull NotificationEntry fromEntry, @NonNull NotificationEntry toEntry, @NonNull AlertingNotificationManager alertManager)336 private void transferAlertState(@NonNull NotificationEntry fromEntry, @NonNull NotificationEntry toEntry, 337 @NonNull AlertingNotificationManager alertManager) { 338 alertManager.removeNotification(fromEntry.key, true /* releaseImmediately */); 339 alertNotificationWhenPossible(toEntry, alertManager); 340 } 341 342 /** 343 * Determines if we need to transfer the alert back to the summary from the child and does 344 * so if needed. 345 * 346 * This can happen since notification groups are not delivered as a whole unit and it is 347 * possible we erroneously transfer the alert from the summary to the child even though 348 * more children are coming. Thus, if a child is added within a certain timeframe after we 349 * transfer, we back out and alert the summary again. 350 * 351 * @param groupAlertEntry group alert entry to check 352 */ checkShouldTransferBack(@onNull GroupAlertEntry groupAlertEntry)353 private void checkShouldTransferBack(@NonNull GroupAlertEntry groupAlertEntry) { 354 if (SystemClock.elapsedRealtime() - groupAlertEntry.mLastAlertTransferTime 355 < ALERT_TRANSFER_TIMEOUT) { 356 NotificationEntry summary = groupAlertEntry.mGroup.summary; 357 AlertingNotificationManager alertManager = getActiveAlertManager(); 358 359 if (!onlySummaryAlerts(summary)) { 360 return; 361 } 362 ArrayList<NotificationEntry> children = mGroupManager.getLogicalChildren(summary.notification); 363 int numChildren = children.size(); 364 int numPendingChildren = getPendingChildrenNotAlerting(groupAlertEntry.mGroup); 365 numChildren += numPendingChildren; 366 if (numChildren <= 1) { 367 return; 368 } 369 boolean releasedChild = false; 370 for (int i = 0; i < children.size(); i++) { 371 NotificationEntry entry = children.get(i); 372 if (onlySummaryAlerts(entry) && alertManager.isAlerting(entry.key)) { 373 releasedChild = true; 374 alertManager.removeNotification(entry.key, true /* releaseImmediately */); 375 } 376 if (mPendingAlerts.containsKey(entry.key)) { 377 // This is the child that would've been removed if it was inflated. 378 releasedChild = true; 379 mPendingAlerts.get(entry.key).mAbortOnInflation = true; 380 } 381 } 382 if (releasedChild && !alertManager.isAlerting(summary.key)) { 383 boolean notifyImmediately = (numChildren - numPendingChildren) > 1; 384 if (notifyImmediately) { 385 alertNotificationWhenPossible(summary, alertManager); 386 } else { 387 // Should wait until the pending child inflates before alerting. 388 groupAlertEntry.mAlertSummaryOnNextAddition = true; 389 } 390 groupAlertEntry.mLastAlertTransferTime = 0; 391 } 392 } 393 } 394 395 /** 396 * Tries to alert the notification. If its content view is not inflated, we inflate and continue 397 * when the entry finishes inflating the view. 398 * 399 * @param entry entry to show 400 * @param alertManager alert manager for the alert type 401 */ alertNotificationWhenPossible(@onNull NotificationEntry entry, @NonNull AlertingNotificationManager alertManager)402 private void alertNotificationWhenPossible(@NonNull NotificationEntry entry, 403 @NonNull AlertingNotificationManager alertManager) { 404 @InflationFlag int contentFlag = alertManager.getContentFlag(); 405 if (!entry.getRow().isInflationFlagSet(contentFlag)) { 406 mPendingAlerts.put(entry.key, new PendingAlertInfo(entry, alertManager)); 407 entry.getRow().updateInflationFlag(contentFlag, true /* shouldInflate */); 408 entry.getRow().inflateViews(); 409 return; 410 } 411 if (alertManager.isAlerting(entry.key)) { 412 alertManager.updateNotification(entry.key, true /* alert */); 413 } else { 414 alertManager.showNotification(entry); 415 } 416 } 417 getActiveAlertManager()418 private AlertingNotificationManager getActiveAlertManager() { 419 return mIsDozing ? mAmbientPulseManager : mHeadsUpManager; 420 } 421 onlySummaryAlerts(NotificationEntry entry)422 private boolean onlySummaryAlerts(NotificationEntry entry) { 423 return entry.notification.getNotification().getGroupAlertBehavior() 424 == Notification.GROUP_ALERT_SUMMARY; 425 } 426 427 /** 428 * Information about a pending alert used to determine if the alert is still needed when 429 * inflation completes. 430 */ 431 private class PendingAlertInfo { 432 /** 433 * The alert manager when the transfer is initiated. 434 */ 435 final AlertingNotificationManager mAlertManager; 436 437 /** 438 * The original notification when the transfer is initiated. This is used to determine if 439 * the transfer is still valid if the notification is updated. 440 */ 441 final StatusBarNotification mOriginalNotification; 442 final NotificationEntry mEntry; 443 444 /** 445 * The notification is still pending inflation but we've decided that we no longer need 446 * the content view (e.g. suppression might have changed and we decided we need to transfer 447 * back). However, there is no way to abort just this inflation if other inflation requests 448 * have started (see {@link AsyncInflationTask#supersedeTask(InflationTask)}). So instead 449 * we just flag it as aborted and free when it's inflated. 450 */ 451 boolean mAbortOnInflation; 452 PendingAlertInfo(NotificationEntry entry, AlertingNotificationManager alertManager)453 PendingAlertInfo(NotificationEntry entry, AlertingNotificationManager alertManager) { 454 mOriginalNotification = entry.notification; 455 mEntry = entry; 456 mAlertManager = alertManager; 457 } 458 459 /** 460 * Whether or not the pending alert is still valid and should still alert after inflation. 461 * 462 * @return true if the pending alert should still occur, false o/w 463 */ isStillValid()464 private boolean isStillValid() { 465 if (mAbortOnInflation) { 466 // Notification is aborted due to the transfer being explicitly cancelled 467 return false; 468 } 469 if (mAlertManager != getActiveAlertManager()) { 470 // Alert manager has changed 471 return false; 472 } 473 if (mEntry.notification.getGroupKey() != mOriginalNotification.getGroupKey()) { 474 // Groups have changed 475 return false; 476 } 477 if (mEntry.notification.getNotification().isGroupSummary() 478 != mOriginalNotification.getNotification().isGroupSummary()) { 479 // Notification has changed from group summary to not or vice versa 480 return false; 481 } 482 return true; 483 } 484 } 485 486 /** 487 * Contains alert metadata for the notification group used to determine when/how the alert 488 * should be transferred. 489 */ 490 private static class GroupAlertEntry { 491 /** 492 * The time when the last alert transfer from summary to child happened. 493 */ 494 long mLastAlertTransferTime; 495 boolean mAlertSummaryOnNextAddition; 496 final NotificationGroup mGroup; 497 GroupAlertEntry(NotificationGroup group)498 GroupAlertEntry(NotificationGroup group) { 499 this.mGroup = group; 500 } 501 } 502 } 503