1 /* 2 * Copyright (C) 2020 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.notification.collection.coordinator; 18 19 import static com.android.systemui.statusbar.notification.NotificationUtils.logKey; 20 import static com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer.NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED; 21 22 import static java.util.Objects.requireNonNull; 23 24 import android.annotation.IntDef; 25 import android.os.RemoteException; 26 import android.os.Trace; 27 import android.service.notification.StatusBarNotification; 28 import android.util.ArrayMap; 29 import android.util.ArraySet; 30 import android.util.Log; 31 32 import androidx.annotation.NonNull; 33 import androidx.annotation.Nullable; 34 35 import com.android.internal.annotations.VisibleForTesting; 36 import com.android.internal.statusbar.IStatusBarService; 37 import com.android.systemui.statusbar.notification.collection.BundleEntry; 38 import com.android.systemui.statusbar.notification.collection.GroupEntry; 39 import com.android.systemui.statusbar.notification.collection.ListEntry; 40 import com.android.systemui.statusbar.notification.collection.PipelineEntry; 41 import com.android.systemui.statusbar.notification.collection.NotifPipeline; 42 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 43 import com.android.systemui.statusbar.notification.collection.ShadeListBuilder; 44 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope; 45 import com.android.systemui.statusbar.notification.collection.inflation.BindEventManagerImpl; 46 import com.android.systemui.statusbar.notification.collection.inflation.NotifInflater; 47 import com.android.systemui.statusbar.notification.collection.inflation.NotifUiAdjustment; 48 import com.android.systemui.statusbar.notification.collection.inflation.NotifUiAdjustmentProvider; 49 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; 50 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; 51 import com.android.systemui.statusbar.notification.collection.render.NotifViewBarn; 52 import com.android.systemui.statusbar.notification.collection.render.NotifViewController; 53 import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager; 54 import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager.NotifInflationErrorListener; 55 import com.android.systemui.statusbar.notification.row.icon.AppIconProvider; 56 import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider; 57 import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; 58 import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; 59 import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; 60 61 import java.lang.annotation.Retention; 62 import java.lang.annotation.RetentionPolicy; 63 import java.util.Collection; 64 import java.util.HashSet; 65 import java.util.List; 66 import java.util.Map; 67 import java.util.Set; 68 69 import javax.inject.Inject; 70 71 /** 72 * Kicks off core notification inflation and view rebinding when a notification is added or updated. 73 * Aborts inflation when a notification is removed. 74 * 75 * If a notification was uninflated, this coordinator will filter the notification out from the 76 * {@link ShadeListBuilder} until it is inflated. 77 */ 78 @CoordinatorScope 79 public class PreparationCoordinator implements Coordinator { 80 private static final String TAG = "PreparationCoordinator"; 81 82 private final PreparationCoordinatorLogger mLogger; 83 private final NotifInflater mNotifInflater; 84 private final NotifInflationErrorManager mNotifErrorManager; 85 private final NotifViewBarn mViewBarn; 86 private final NotifUiAdjustmentProvider mAdjustmentProvider; 87 private final ArrayMap<NotificationEntry, Integer> mInflationStates = new ArrayMap<>(); 88 89 /** 90 * The map of notifications to the NotifUiAdjustment (i.e. parameters) that were calculated 91 * when the inflation started. If an update of any kind results in the adjustment changing, 92 * then the row must be reinflated. If the row is being inflated, then the inflation must be 93 * aborted and restarted. 94 */ 95 private final ArrayMap<NotificationEntry, NotifUiAdjustment> mInflationAdjustments = 96 new ArrayMap<>(); 97 98 /** 99 * The set of notifications that are currently inflating something. Note that this is 100 * separate from inflation state as a view could either be uninflated or inflated and still be 101 * inflating something. 102 */ 103 private final ArraySet<NotificationEntry> mInflatingNotifs = new ArraySet<>(); 104 105 private final IStatusBarService mStatusBarService; 106 107 /** 108 * The number of children in a group we actually keep inflated since we don't actually show 109 * all the children and don't need every child inflated at all times. 110 */ 111 private final int mChildBindCutoff; 112 113 /** How long we can delay a group while waiting for all children to inflate */ 114 private final long mMaxGroupInflationDelay; 115 private final BindEventManagerImpl mBindEventManager; 116 private final AppIconProvider mAppIconProvider; 117 private final NotificationIconStyleProvider mNotificationIconStyleProvider; 118 119 @Inject PreparationCoordinator( PreparationCoordinatorLogger logger, NotifInflater notifInflater, NotifInflationErrorManager errorManager, NotifViewBarn viewBarn, NotifUiAdjustmentProvider adjustmentProvider, IStatusBarService service, BindEventManagerImpl bindEventManager, AppIconProvider appIconProvider, NotificationIconStyleProvider notificationIconStyleProvider)120 public PreparationCoordinator( 121 PreparationCoordinatorLogger logger, 122 NotifInflater notifInflater, 123 NotifInflationErrorManager errorManager, 124 NotifViewBarn viewBarn, 125 NotifUiAdjustmentProvider adjustmentProvider, 126 IStatusBarService service, 127 BindEventManagerImpl bindEventManager, 128 AppIconProvider appIconProvider, 129 NotificationIconStyleProvider notificationIconStyleProvider) { 130 this( 131 logger, 132 notifInflater, 133 errorManager, 134 viewBarn, 135 adjustmentProvider, 136 service, 137 bindEventManager, 138 appIconProvider, 139 notificationIconStyleProvider, 140 CHILD_BIND_CUTOFF, 141 MAX_GROUP_INFLATION_DELAY); 142 } 143 144 @VisibleForTesting PreparationCoordinator( PreparationCoordinatorLogger logger, NotifInflater notifInflater, NotifInflationErrorManager errorManager, NotifViewBarn viewBarn, NotifUiAdjustmentProvider adjustmentProvider, IStatusBarService service, BindEventManagerImpl bindEventManager, AppIconProvider appIconProvider, NotificationIconStyleProvider notificationIconStyleProvider, int childBindCutoff, long maxGroupInflationDelay)145 PreparationCoordinator( 146 PreparationCoordinatorLogger logger, 147 NotifInflater notifInflater, 148 NotifInflationErrorManager errorManager, 149 NotifViewBarn viewBarn, 150 NotifUiAdjustmentProvider adjustmentProvider, 151 IStatusBarService service, 152 BindEventManagerImpl bindEventManager, 153 AppIconProvider appIconProvider, 154 NotificationIconStyleProvider notificationIconStyleProvider, 155 int childBindCutoff, 156 long maxGroupInflationDelay) { 157 mLogger = logger; 158 mNotifInflater = notifInflater; 159 mNotifErrorManager = errorManager; 160 mViewBarn = viewBarn; 161 mAdjustmentProvider = adjustmentProvider; 162 mStatusBarService = service; 163 mChildBindCutoff = childBindCutoff; 164 mMaxGroupInflationDelay = maxGroupInflationDelay; 165 mBindEventManager = bindEventManager; 166 mAppIconProvider = appIconProvider; 167 mNotificationIconStyleProvider = notificationIconStyleProvider; 168 } 169 170 @Override attach(NotifPipeline pipeline)171 public void attach(NotifPipeline pipeline) { 172 mNotifErrorManager.addInflationErrorListener(mInflationErrorListener); 173 mAdjustmentProvider.addDirtyListener( 174 () -> mNotifInflatingFilter.invalidateList("adjustmentProviderChanged")); 175 176 pipeline.addCollectionListener(mNotifCollectionListener); 177 if (android.app.Flags.notificationsRedesignAppIcons()) { 178 pipeline.addOnBeforeTransformGroupsListener(this::purgeCaches); 179 } 180 // Inflate after grouping/sorting since that affects what views to inflate. 181 pipeline.addOnBeforeFinalizeFilterListener(this::inflateAllRequiredViews); 182 pipeline.addFinalizeFilter(mNotifInflationErrorFilter); 183 pipeline.addFinalizeFilter(mNotifInflatingFilter); 184 } 185 186 private final NotifCollectionListener mNotifCollectionListener = new NotifCollectionListener() { 187 188 @Override 189 public void onEntryInit(NotificationEntry entry) { 190 mInflationStates.put(entry, STATE_UNINFLATED); 191 } 192 193 @Override 194 public void onEntryUpdated(NotificationEntry entry) { 195 abortInflation(entry, "entryUpdated"); 196 @InflationState int state = getInflationState(entry); 197 if (state == STATE_INFLATED) { 198 mInflationStates.put(entry, STATE_INFLATED_INVALID); 199 } else if (state == STATE_ERROR) { 200 // Updated so maybe it won't error out now. 201 mInflationStates.put(entry, STATE_UNINFLATED); 202 } 203 } 204 205 @Override 206 public void onEntryRemoved(NotificationEntry entry, int reason) { 207 abortInflation(entry, "entryRemoved reason=" + reason); 208 } 209 210 @Override 211 public void onEntryCleanUp(NotificationEntry entry) { 212 mInflationStates.remove(entry); 213 mViewBarn.removeViewForEntry(entry); 214 mInflationAdjustments.remove(entry); 215 } 216 }; 217 218 private final NotifFilter mNotifInflationErrorFilter = new NotifFilter( 219 TAG + "InflationError") { 220 /** 221 * Filters out notifications that threw an error when attempting to inflate. 222 */ 223 @Override 224 public boolean shouldFilterOut(NotificationEntry entry, long now) { 225 return getInflationState(entry) == STATE_ERROR; 226 } 227 }; 228 229 private final NotifFilter mNotifInflatingFilter = new NotifFilter(TAG + "Inflating") { 230 private final Map<GroupEntry, Boolean> mIsDelayedGroupCache = new ArrayMap<>(); 231 232 /** 233 * Filters out notifications that either (a) aren't inflated or (b) are part of a group 234 * that isn't completely inflated yet 235 */ 236 @Override 237 public boolean shouldFilterOut(NotificationEntry entry, long now) { 238 final PipelineEntry pipelineEntryParent = requireNonNull(entry.getParent()); 239 Boolean isMemberOfDelayedGroup = mIsDelayedGroupCache.get(pipelineEntryParent); 240 if (isMemberOfDelayedGroup == null && pipelineEntryParent instanceof GroupEntry) { 241 GroupEntry parent = (GroupEntry) pipelineEntryParent; 242 isMemberOfDelayedGroup = shouldWaitForGroupToInflate(parent, now); 243 mIsDelayedGroupCache.put(parent, isMemberOfDelayedGroup); 244 } 245 return !isInflated(entry) || (isMemberOfDelayedGroup != null && isMemberOfDelayedGroup); 246 } 247 248 @Override 249 public void onCleanup() { 250 mIsDelayedGroupCache.clear(); 251 } 252 }; 253 254 private final NotifInflationErrorListener mInflationErrorListener = 255 new NotifInflationErrorListener() { 256 @Override 257 public void onNotifInflationError(NotificationEntry entry, Exception e) { 258 mViewBarn.removeViewForEntry(entry); 259 mInflationStates.put(entry, STATE_ERROR); 260 try { 261 final StatusBarNotification sbn = entry.getSbn(); 262 // report notification inflation errors back up 263 // to notification delegates 264 mStatusBarService.onNotificationError( 265 sbn.getPackageName(), 266 sbn.getTag(), 267 sbn.getId(), 268 sbn.getUid(), 269 sbn.getInitialPid(), 270 e.getMessage(), 271 sbn.getUser().getIdentifier()); 272 } catch (RemoteException ex) { 273 // System server is dead, nothing to do about that 274 } 275 mNotifInflationErrorFilter.invalidateList("onNotifInflationError for " + logKey(entry)); 276 } 277 278 @Override 279 public void onNotifInflationErrorCleared(NotificationEntry entry) { 280 mNotifInflationErrorFilter.invalidateList( 281 "onNotifInflationErrorCleared for " + logKey(entry)); 282 } 283 }; 284 purgeCaches(Collection<PipelineEntry> entries)285 private void purgeCaches(Collection<PipelineEntry> entries) { 286 Set<String> wantedPackages = getPackages(entries); 287 mAppIconProvider.purgeCache(wantedPackages); 288 mNotificationIconStyleProvider.purgeCache(wantedPackages); 289 } 290 291 /** 292 * Get all app packages present in {@param entries}. 293 */ getPackages(Collection<PipelineEntry> entries)294 private static @NonNull Set<String> getPackages(Collection<PipelineEntry> entries) { 295 Set<String> packages = new HashSet<>(); 296 for (PipelineEntry entry : entries) { 297 NotificationEntry notificationEntry = entry.getRepresentativeEntry(); 298 if (notificationEntry == null) { 299 Log.wtf(TAG, "notification entry " + entry.getKey() 300 + " has no representative entry"); 301 continue; 302 } 303 packages.add(notificationEntry.getSbn().getPackageName()); 304 } 305 return packages; 306 } 307 inflateAllRequiredViews(List<PipelineEntry> entries)308 private void inflateAllRequiredViews(List<PipelineEntry> entries) { 309 for (int i = 0, size = entries.size(); i < size; i++) { 310 PipelineEntry entry = entries.get(i); 311 if (NotificationBundleUi.isEnabled() && entry instanceof BundleEntry bundleEntry) { 312 for (ListEntry listEntry : bundleEntry.getChildren()) { 313 if (listEntry instanceof GroupEntry groupEntry) { 314 inflateRequiredGroupViews(groupEntry); 315 } else { 316 NotificationEntry notifEntry = (NotificationEntry) listEntry; 317 inflateRequiredNotifViews(notifEntry); 318 } 319 } 320 } else if (entry instanceof GroupEntry) { 321 GroupEntry groupEntry = (GroupEntry) entry; 322 inflateRequiredGroupViews(groupEntry); 323 } else { 324 NotificationEntry notifEntry = (NotificationEntry) entry; 325 inflateRequiredNotifViews(notifEntry); 326 } 327 } 328 } 329 inflateRequiredGroupViews(GroupEntry groupEntry)330 private void inflateRequiredGroupViews(GroupEntry groupEntry) { 331 NotificationEntry summary = groupEntry.getSummary(); 332 if (summary != null && AsyncGroupHeaderViewInflation.isEnabled()) { 333 summary.markAsGroupSummary(); 334 } 335 List<NotificationEntry> children = groupEntry.getChildren(); 336 inflateRequiredNotifViews(summary); 337 for (int j = 0; j < children.size(); j++) { 338 NotificationEntry child = children.get(j); 339 if (AsyncHybridViewInflation.isEnabled()) child.markAsGroupChild(); 340 boolean childShouldBeBound = j < mChildBindCutoff; 341 if (childShouldBeBound) { 342 inflateRequiredNotifViews(child); 343 } else { 344 if (mInflatingNotifs.contains(child)) { 345 abortInflation(child, "Past last visible group child"); 346 } 347 if (isInflated(child)) { 348 // TODO: May want to put an animation hint here so view manager knows to treat 349 // this differently from a regular removal animation 350 freeNotifViews(child, "Past last visible group child"); 351 } 352 } 353 } 354 } 355 356 private void inflateRequiredNotifViews(NotificationEntry entry) { 357 NotifUiAdjustment newAdjustment = mAdjustmentProvider.calculateAdjustment(entry); 358 if (mInflatingNotifs.contains(entry)) { 359 // Already inflating this entry 360 String errorIfNoOldAdjustment = "Inflating notification has no adjustments"; 361 if (needToReinflate(entry, newAdjustment, errorIfNoOldAdjustment)) { 362 inflateEntry(entry, newAdjustment, "adjustment changed while inflating"); 363 } 364 return; 365 } 366 @InflationState int state = mInflationStates.get(entry); 367 switch (state) { 368 case STATE_UNINFLATED: 369 inflateEntry(entry, newAdjustment, "entryAdded"); 370 break; 371 case STATE_INFLATED_INVALID: 372 rebind(entry, newAdjustment, "entryUpdated"); 373 break; 374 case STATE_INFLATED: 375 String errorIfNoOldAdjustment = "Fully inflated notification has no adjustments"; 376 if (needToReinflate(entry, newAdjustment, errorIfNoOldAdjustment)) { 377 rebind(entry, newAdjustment, "adjustment changed after inflated"); 378 } 379 break; 380 case STATE_ERROR: 381 if (needToReinflate(entry, newAdjustment, null)) { 382 inflateEntry(entry, newAdjustment, "adjustment changed after error"); 383 } 384 break; 385 default: 386 // Nothing to do. 387 } 388 } 389 390 private boolean needToReinflate(@NonNull NotificationEntry entry, 391 @NonNull NotifUiAdjustment newAdjustment, @Nullable String oldAdjustmentMissingError) { 392 NotifUiAdjustment oldAdjustment = mInflationAdjustments.get(entry); 393 if (oldAdjustment == null) { 394 if (oldAdjustmentMissingError == null) { 395 return true; 396 } else { 397 throw new IllegalStateException(oldAdjustmentMissingError); 398 } 399 } 400 return NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment); 401 } 402 403 private void inflateEntry(NotificationEntry entry, 404 NotifUiAdjustment newAdjustment, 405 String reason) { 406 Trace.beginSection("PrepCoord.inflateEntry"); 407 abortInflation(entry, reason); 408 mInflationAdjustments.put(entry, newAdjustment); 409 mInflatingNotifs.add(entry); 410 NotifInflater.Params params = getInflaterParams(newAdjustment, reason); 411 mNotifInflater.inflateViews(entry, params, this::onInflationFinished); 412 Trace.endSection(); 413 } 414 415 private void rebind(NotificationEntry entry, 416 NotifUiAdjustment newAdjustment, 417 String reason) { 418 mInflationAdjustments.put(entry, newAdjustment); 419 mInflatingNotifs.add(entry); 420 NotifInflater.Params params = getInflaterParams(newAdjustment, reason); 421 mNotifInflater.rebindViews(entry, params, this::onInflationFinished); 422 } 423 424 NotifInflater.Params getInflaterParams(NotifUiAdjustment adjustment, String reason) { 425 return new NotifInflater.Params( 426 /* isMinimized = */ adjustment.isMinimized(), 427 /* reason = */ reason, 428 /* showSnooze = */ adjustment.isSnoozeEnabled(), 429 /* isChildInGroup = */ adjustment.isChildInGroup(), 430 /* isGroupSummary = */ adjustment.isGroupSummary(), 431 /* needsRedaction = */ adjustment.getRedactionType() 432 ); 433 } 434 435 private void abortInflation(NotificationEntry entry, String reason) { 436 final boolean taskAborted = mNotifInflater.abortInflation(entry); 437 final boolean wasInflating = mInflatingNotifs.remove(entry); 438 if (taskAborted || wasInflating) { 439 mLogger.logInflationAborted(entry, reason); 440 } 441 } 442 443 private void onInflationFinished(NotificationEntry entry, NotifViewController controller) { 444 mLogger.logNotifInflated(entry); 445 mInflatingNotifs.remove(entry); 446 mViewBarn.registerViewForEntry(entry, controller); 447 mInflationStates.put(entry, STATE_INFLATED); 448 mBindEventManager.notifyViewBound(entry); 449 mNotifInflatingFilter.invalidateList("onInflationFinished for " + logKey(entry)); 450 } 451 452 private void freeNotifViews(NotificationEntry entry, String reason) { 453 mLogger.logFreeNotifViews(entry, reason); 454 mViewBarn.removeViewForEntry(entry); 455 mNotifInflater.releaseViews(entry); 456 // TODO: clear the entry's row here, or even better, stop setting the row on the entry! 457 mInflationStates.put(entry, STATE_UNINFLATED); 458 } 459 460 private boolean isInflated(NotificationEntry entry) { 461 @InflationState int state = getInflationState(entry); 462 return (state == STATE_INFLATED) || (state == STATE_INFLATED_INVALID); 463 } 464 465 private @InflationState int getInflationState(NotificationEntry entry) { 466 Integer stateObj = mInflationStates.get(entry); 467 requireNonNull(stateObj, 468 "Asking state of a notification preparation coordinator doesn't know about"); 469 return stateObj; 470 } 471 472 private boolean shouldWaitForGroupToInflate(GroupEntry group, long now) { 473 if (group == GroupEntry.ROOT_ENTRY || group.wasAttachedInPreviousPass()) { 474 return false; 475 } 476 if (isBeyondGroupInitializationWindow(group, now)) { 477 mLogger.logGroupInflationTookTooLong(group); 478 return false; 479 } 480 // Only delay release if the summary is not inflated. 481 // TODO(253454977): Once we ensure that all other pipeline filtering and pruning has been 482 // done by this point, we can revert back to checking for mInflatingNotifs.contains(...) 483 if (group.getSummary() != null && !isInflated(group.getSummary())) { 484 mLogger.logDelayingGroupRelease(group, group.getSummary()); 485 return true; 486 } 487 for (NotificationEntry child : group.getChildren()) { 488 if (mInflatingNotifs.contains(child) && !child.wasAttachedInPreviousPass()) { 489 mLogger.logDelayingGroupRelease(group, child); 490 return true; 491 } 492 } 493 mLogger.logDoneWaitingForGroupInflation(group); 494 return false; 495 } 496 497 private boolean isBeyondGroupInitializationWindow(GroupEntry entry, long now) { 498 return now - entry.getCreationTime() > mMaxGroupInflationDelay; 499 } 500 501 @Retention(RetentionPolicy.SOURCE) 502 @IntDef(prefix = {"STATE_"}, 503 value = {STATE_UNINFLATED, STATE_INFLATED_INVALID, STATE_INFLATED, STATE_ERROR}) 504 @interface InflationState {} 505 506 /** The notification has no views attached. */ 507 private static final int STATE_UNINFLATED = 0; 508 509 /** The notification is inflated. */ 510 private static final int STATE_INFLATED = 1; 511 512 /** 513 * The notification is inflated, but its content may be out-of-date since the notification has 514 * been updated. 515 */ 516 private static final int STATE_INFLATED_INVALID = 2; 517 518 /** The notification errored out while inflating */ 519 private static final int STATE_ERROR = -1; 520 521 /** 522 * How big the buffer of extra views we keep around to be ready to show when we do need to 523 * dynamically inflate a row. 524 */ 525 private static final int EXTRA_VIEW_BUFFER_COUNT = 1; 526 527 private static final long MAX_GROUP_INFLATION_DELAY = 500; 528 529 private static final int CHILD_BIND_CUTOFF = 530 NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED + EXTRA_VIEW_BUFFER_COUNT; 531 } 532