1 /* 2 * Copyright (C) 2019 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; 18 19 import static com.android.internal.util.Preconditions.checkArgument; 20 import static com.android.internal.util.Preconditions.checkState; 21 import static com.android.systemui.statusbar.notification.collection.GroupEntry.ROOT_ENTRY; 22 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_BUILD_STARTED; 23 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_FINALIZE_FILTERING; 24 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_FINALIZING; 25 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_GROUPING; 26 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_GROUP_STABILIZING; 27 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_IDLE; 28 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_PRE_GROUP_FILTERING; 29 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_RESETTING; 30 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_SORTING; 31 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_TRANSFORMING; 32 33 import static java.util.Objects.requireNonNull; 34 35 import android.annotation.MainThread; 36 import android.annotation.Nullable; 37 import android.os.Trace; 38 import android.service.notification.StatusBarNotification; 39 import android.util.ArrayMap; 40 import android.util.ArraySet; 41 import android.util.Log; 42 43 import androidx.annotation.NonNull; 44 import androidx.annotation.VisibleForTesting; 45 46 import com.android.systemui.Dumpable; 47 import com.android.systemui.dagger.SysUISingleton; 48 import com.android.systemui.dump.DumpManager; 49 import com.android.systemui.statusbar.NotificationInteractionTracker; 50 import com.android.systemui.statusbar.notification.NotifPipelineFlags; 51 import com.android.systemui.statusbar.notification.collection.coordinator.BundleCoordinator; 52 import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection; 53 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener; 54 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener; 55 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener; 56 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener; 57 import com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState; 58 import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort; 59 import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort.StableOrder; 60 import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderHelper; 61 import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderLogger; 62 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.DefaultNotifBundler; 63 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.DefaultNotifStabilityManager; 64 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator; 65 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifBundler; 66 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator; 67 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; 68 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter; 69 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner; 70 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager; 71 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable; 72 import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener; 73 import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; 74 import com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt; 75 import com.android.systemui.util.Assert; 76 import com.android.systemui.util.NamedListenerSet; 77 import com.android.systemui.util.time.SystemClock; 78 79 import java.io.PrintWriter; 80 import java.util.ArrayList; 81 import java.util.Collection; 82 import java.util.Collections; 83 import java.util.Comparator; 84 import java.util.HashMap; 85 import java.util.Iterator; 86 import java.util.List; 87 import java.util.Map; 88 import java.util.Objects; 89 import java.util.Set; 90 91 import javax.inject.Inject; 92 93 /** 94 * The second half of {@link NotifPipeline}. Sits downstream of the NotifCollection and transforms 95 * its "notification set" into the "shade list", the filtered, grouped, and sorted list of 96 * notifications that are currently present in the notification shade. 97 */ 98 @MainThread 99 @SysUISingleton 100 public class ShadeListBuilder implements Dumpable, PipelineDumpable { 101 private final SystemClock mSystemClock; 102 private final ShadeListBuilderLogger mLogger; 103 private final NotificationInteractionTracker mInteractionTracker; 104 private final DumpManager mDumpManager; 105 // used exclusivly by ShadeListBuilder#notifySectionEntriesUpdated 106 // TODO replace temp with collection pool for readability 107 private final ArrayList<PipelineEntry> mTempSectionMembers = new ArrayList<>(); 108 private NotifPipelineFlags mFlags; 109 private final boolean mAlwaysLogList; 110 111 private List<PipelineEntry> mNotifList = new ArrayList<>(); 112 private List<PipelineEntry> mNewNotifList = new ArrayList<>(); 113 114 private final SemiStableSort mSemiStableSort = new SemiStableSort(); 115 private final StableOrder<PipelineEntry> mStableOrder = this::getStableOrderRank; 116 private final PipelineState mPipelineState = new PipelineState(); 117 private final Map<String, GroupEntry> mGroups = new ArrayMap<>(); 118 private Collection<NotificationEntry> mAllEntries = Collections.emptyList(); 119 @Nullable 120 private Collection<NotificationEntry> mPendingEntries = null; 121 private int mIterationCount = 0; 122 123 private final List<NotifFilter> mNotifPreGroupFilters = new ArrayList<>(); 124 private final List<NotifPromoter> mNotifPromoters = new ArrayList<>(); 125 private final List<NotifFilter> mNotifFinalizeFilters = new ArrayList<>(); 126 private final List<NotifComparator> mNotifComparators = new ArrayList<>(); 127 private final List<NotifSection> mNotifSections = new ArrayList<>(); 128 private NotifStabilityManager mNotifStabilityManager; 129 private NotifBundler mNotifBundler; 130 private Map<String, BundleEntry> mIdToBundleEntry = new HashMap<>(); 131 private final NamedListenerSet<OnBeforeTransformGroupsListener> 132 mOnBeforeTransformGroupsListeners = new NamedListenerSet<>(); 133 private final NamedListenerSet<OnBeforeSortListener> 134 mOnBeforeSortListeners = new NamedListenerSet<>(); 135 private final NamedListenerSet<OnBeforeFinalizeFilterListener> 136 mOnBeforeFinalizeFilterListeners = new NamedListenerSet<>(); 137 private final NamedListenerSet<OnBeforeRenderListListener> 138 mOnBeforeRenderListListeners = new NamedListenerSet<>(); 139 @Nullable private OnRenderListListener mOnRenderListListener; 140 141 private List<PipelineEntry> mReadOnlyNotifList = Collections.unmodifiableList(mNotifList); 142 private List<PipelineEntry> mReadOnlyNewNotifList = Collections.unmodifiableList(mNewNotifList); 143 private final NotifPipelineChoreographer mChoreographer; 144 145 private int mConsecutiveReentrantRebuilds = 0; 146 @VisibleForTesting public static final int MAX_CONSECUTIVE_REENTRANT_REBUILDS = 3; 147 148 @Inject ShadeListBuilder( DumpManager dumpManager, NotifPipelineChoreographer pipelineChoreographer, NotifPipelineFlags flags, NotificationInteractionTracker interactionTracker, ShadeListBuilderLogger logger, SystemClock systemClock )149 public ShadeListBuilder( 150 DumpManager dumpManager, 151 NotifPipelineChoreographer pipelineChoreographer, 152 NotifPipelineFlags flags, 153 NotificationInteractionTracker interactionTracker, 154 ShadeListBuilderLogger logger, 155 SystemClock systemClock 156 ) { 157 mSystemClock = systemClock; 158 mLogger = logger; 159 mFlags = flags; 160 mAlwaysLogList = flags.isDevLoggingEnabled(); 161 mInteractionTracker = interactionTracker; 162 mChoreographer = pipelineChoreographer; 163 mDumpManager = dumpManager; 164 setSectioners(Collections.emptyList()); 165 } 166 167 /** 168 * Attach the list builder to the NotifCollection. After this is called, it will start building 169 * the notif list in response to changes to the colletion. 170 */ attach(NotifCollection collection)171 public void attach(NotifCollection collection) { 172 Assert.isMainThread(); 173 mDumpManager.registerDumpable(TAG, this); 174 collection.addCollectionListener(mInteractionTracker); 175 collection.setBuildListener(mReadyForBuildListener); 176 mChoreographer.addOnEvalListener(this::buildList); 177 } 178 179 /** 180 * Registers the listener that's responsible for rendering the notif list to the screen. Called 181 * At the very end of pipeline execution, after all other listeners and pluggables have fired. 182 */ setOnRenderListListener(OnRenderListListener onRenderListListener)183 public void setOnRenderListListener(OnRenderListListener onRenderListListener) { 184 Assert.isMainThread(); 185 186 mPipelineState.requireState(STATE_IDLE); 187 mOnRenderListListener = onRenderListListener; 188 } 189 addOnBeforeTransformGroupsListener(OnBeforeTransformGroupsListener listener)190 void addOnBeforeTransformGroupsListener(OnBeforeTransformGroupsListener listener) { 191 Assert.isMainThread(); 192 193 mPipelineState.requireState(STATE_IDLE); 194 mOnBeforeTransformGroupsListeners.addIfAbsent(listener); 195 } 196 addOnBeforeSortListener(OnBeforeSortListener listener)197 void addOnBeforeSortListener(OnBeforeSortListener listener) { 198 Assert.isMainThread(); 199 200 mPipelineState.requireState(STATE_IDLE); 201 mOnBeforeSortListeners.addIfAbsent(listener); 202 } 203 addOnBeforeFinalizeFilterListener(OnBeforeFinalizeFilterListener listener)204 void addOnBeforeFinalizeFilterListener(OnBeforeFinalizeFilterListener listener) { 205 Assert.isMainThread(); 206 207 mPipelineState.requireState(STATE_IDLE); 208 mOnBeforeFinalizeFilterListeners.addIfAbsent(listener); 209 } 210 addOnBeforeRenderListListener(OnBeforeRenderListListener listener)211 void addOnBeforeRenderListListener(OnBeforeRenderListListener listener) { 212 Assert.isMainThread(); 213 214 mPipelineState.requireState(STATE_IDLE); 215 mOnBeforeRenderListListeners.addIfAbsent(listener); 216 } 217 addPreRenderInvalidator(Invalidator invalidator)218 void addPreRenderInvalidator(Invalidator invalidator) { 219 Assert.isMainThread(); 220 221 mPipelineState.requireState(STATE_IDLE); 222 invalidator.setInvalidationListener(this::onPreRenderInvalidated); 223 } 224 addPreGroupFilter(NotifFilter filter)225 void addPreGroupFilter(NotifFilter filter) { 226 Assert.isMainThread(); 227 mPipelineState.requireState(STATE_IDLE); 228 229 mNotifPreGroupFilters.add(filter); 230 filter.setInvalidationListener(this::onPreGroupFilterInvalidated); 231 } 232 addFinalizeFilter(NotifFilter filter)233 void addFinalizeFilter(NotifFilter filter) { 234 Assert.isMainThread(); 235 mPipelineState.requireState(STATE_IDLE); 236 237 mNotifFinalizeFilters.add(filter); 238 filter.setInvalidationListener(this::onFinalizeFilterInvalidated); 239 } 240 addPromoter(NotifPromoter promoter)241 void addPromoter(NotifPromoter promoter) { 242 Assert.isMainThread(); 243 mPipelineState.requireState(STATE_IDLE); 244 245 mNotifPromoters.add(promoter); 246 promoter.setInvalidationListener(this::onPromoterInvalidated); 247 } 248 setSectioners(List<NotifSectioner> sectioners)249 void setSectioners(List<NotifSectioner> sectioners) { 250 Assert.isMainThread(); 251 mPipelineState.requireState(STATE_IDLE); 252 253 mNotifSections.clear(); 254 for (NotifSectioner sectioner : sectioners) { 255 final NotifSection section = new NotifSection(sectioner, mNotifSections.size()); 256 final NotifComparator sectionComparator = section.getComparator(); 257 mNotifSections.add(section); 258 sectioner.setInvalidationListener(this::onNotifSectionInvalidated); 259 if (sectionComparator != null) { 260 sectionComparator.setInvalidationListener(this::onNotifComparatorInvalidated); 261 } 262 } 263 264 mNotifSections.add(new NotifSection(DEFAULT_SECTIONER, mNotifSections.size())); 265 266 // validate sections 267 final ArraySet<Integer> seenBuckets = new ArraySet<>(); 268 int lastBucket = mNotifSections.size() > 0 269 ? mNotifSections.get(0).getBucket() 270 : 0; 271 for (NotifSection section : mNotifSections) { 272 if (lastBucket != section.getBucket() && seenBuckets.contains(section.getBucket())) { 273 throw new IllegalStateException("setSectioners with non contiguous sections " 274 + section.getLabel() + " has an already seen bucket"); 275 } 276 lastBucket = section.getBucket(); 277 seenBuckets.add(lastBucket); 278 } 279 } 280 setBundler(NotifBundler bundler)281 void setBundler(NotifBundler bundler) { 282 Assert.isMainThread(); 283 mPipelineState.requireState(STATE_IDLE); 284 285 mNotifBundler = bundler; 286 if (mNotifBundler == null) { 287 throw new IllegalStateException(TAG + ".setBundler: null"); 288 } 289 290 mIdToBundleEntry.clear(); 291 for (String id: mNotifBundler.getBundleIds()) { 292 if (BundleCoordinator.debugBundleUi) { 293 Log.i(TAG, "create BundleEntry with id: " + id); 294 } 295 mIdToBundleEntry.put(id, new BundleEntry(id)); 296 } 297 } 298 setNotifStabilityManager(@onNull NotifStabilityManager notifStabilityManager)299 void setNotifStabilityManager(@NonNull NotifStabilityManager notifStabilityManager) { 300 Assert.isMainThread(); 301 mPipelineState.requireState(STATE_IDLE); 302 303 if (mNotifStabilityManager != null) { 304 throw new IllegalStateException( 305 "Attempting to set the NotifStabilityManager more than once. There should " 306 + "only be one visual stability manager. Manager is being set by " 307 + mNotifStabilityManager.getName() + " and " 308 + notifStabilityManager.getName()); 309 } 310 311 mNotifStabilityManager = notifStabilityManager; 312 mNotifStabilityManager.setInvalidationListener(this::onReorderingAllowedInvalidated); 313 } 314 315 @NonNull getStabilityManager()316 private NotifStabilityManager getStabilityManager() { 317 if (mNotifStabilityManager == null) { 318 return DefaultNotifStabilityManager.INSTANCE; 319 } 320 return mNotifStabilityManager; 321 } 322 323 @NonNull getNotifBundler()324 private NotifBundler getNotifBundler() { 325 if (mNotifBundler == null) { 326 return DefaultNotifBundler.INSTANCE; 327 } 328 return mNotifBundler; 329 } 330 setComparators(List<NotifComparator> comparators)331 void setComparators(List<NotifComparator> comparators) { 332 Assert.isMainThread(); 333 mPipelineState.requireState(STATE_IDLE); 334 335 mNotifComparators.clear(); 336 for (NotifComparator comparator : comparators) { 337 mNotifComparators.add(comparator); 338 comparator.setInvalidationListener(this::onNotifComparatorInvalidated); 339 } 340 } 341 getShadeList()342 List<PipelineEntry> getShadeList() { 343 Assert.isMainThread(); 344 // NOTE: Accessing this method when the pipeline is running is generally going to provide 345 // incorrect results, and indicates a poorly behaved component of the pipeline. 346 mPipelineState.requireState(STATE_IDLE); 347 return mReadOnlyNotifList; 348 } 349 350 private final CollectionReadyForBuildListener mReadyForBuildListener = 351 new CollectionReadyForBuildListener() { 352 @Override 353 public void onBuildList(Collection<NotificationEntry> entries, String reason) { 354 Assert.isMainThread(); 355 mPendingEntries = new ArrayList<>(entries); 356 mLogger.logOnBuildList(reason); 357 rebuildListIfBefore(STATE_BUILD_STARTED); 358 } 359 }; 360 onPreRenderInvalidated(Invalidator invalidator, @Nullable String reason)361 private void onPreRenderInvalidated(Invalidator invalidator, @Nullable String reason) { 362 Assert.isMainThread(); 363 364 mLogger.logPreRenderInvalidated(invalidator, mPipelineState.getState(), reason); 365 366 rebuildListIfBefore(STATE_FINALIZING); 367 } 368 onPreGroupFilterInvalidated(NotifFilter filter, @Nullable String reason)369 private void onPreGroupFilterInvalidated(NotifFilter filter, @Nullable String reason) { 370 Assert.isMainThread(); 371 372 mLogger.logPreGroupFilterInvalidated(filter, mPipelineState.getState(), reason); 373 374 rebuildListIfBefore(STATE_PRE_GROUP_FILTERING); 375 } 376 onReorderingAllowedInvalidated(NotifStabilityManager stabilityManager, @Nullable String reason)377 private void onReorderingAllowedInvalidated(NotifStabilityManager stabilityManager, 378 @Nullable String reason) { 379 Assert.isMainThread(); 380 381 mLogger.logReorderingAllowedInvalidated( 382 stabilityManager, 383 mPipelineState.getState(), 384 reason); 385 386 rebuildListIfBefore(STATE_GROUPING); 387 } 388 onPromoterInvalidated(NotifPromoter promoter, @Nullable String reason)389 private void onPromoterInvalidated(NotifPromoter promoter, @Nullable String reason) { 390 Assert.isMainThread(); 391 392 mLogger.logPromoterInvalidated(promoter, mPipelineState.getState(), reason); 393 394 rebuildListIfBefore(STATE_TRANSFORMING); 395 } 396 onNotifSectionInvalidated(NotifSectioner section, @Nullable String reason)397 private void onNotifSectionInvalidated(NotifSectioner section, @Nullable String reason) { 398 Assert.isMainThread(); 399 400 mLogger.logNotifSectionInvalidated(section, mPipelineState.getState(), reason); 401 402 rebuildListIfBefore(STATE_SORTING); 403 } 404 onFinalizeFilterInvalidated(NotifFilter filter, @Nullable String reason)405 private void onFinalizeFilterInvalidated(NotifFilter filter, @Nullable String reason) { 406 Assert.isMainThread(); 407 408 mLogger.logFinalizeFilterInvalidated(filter, mPipelineState.getState(), reason); 409 410 rebuildListIfBefore(STATE_FINALIZE_FILTERING); 411 } 412 onNotifComparatorInvalidated(NotifComparator comparator, @Nullable String reason)413 private void onNotifComparatorInvalidated(NotifComparator comparator, @Nullable String reason) { 414 Assert.isMainThread(); 415 416 mLogger.logNotifComparatorInvalidated(comparator, mPipelineState.getState(), reason); 417 418 rebuildListIfBefore(STATE_SORTING); 419 } 420 421 /** 422 * The core algorithm of the pipeline. See the top comment in {@link NotifPipeline} for 423 * details on our contracts with other code. 424 * 425 * Once the build starts we are very careful to protect against reentrant code. Anything that 426 * tries to invalidate itself after the pipeline has passed it by will return in an exception. 427 * In general, we should be extremely sensitive to client code doing things in the wrong order; 428 * if we detect that behavior, we should crash instantly. 429 */ buildList()430 private void buildList() { 431 Trace.beginSection("ShadeListBuilder.buildList"); 432 mPipelineState.requireIsBefore(STATE_BUILD_STARTED); 433 434 if (mPendingEntries != null) { 435 mAllEntries = mPendingEntries; 436 mPendingEntries = null; 437 } 438 439 if (!mNotifStabilityManager.isPipelineRunAllowed()) { 440 mLogger.logPipelineRunSuppressed(); 441 Trace.endSection(); 442 return; 443 } 444 445 mPipelineState.setState(STATE_BUILD_STARTED); 446 447 // Step 1: Reset notification states 448 mPipelineState.incrementTo(STATE_RESETTING); 449 resetNotifs(); 450 onBeginRun(); 451 452 // Step 2: Filter out any notifications that shouldn't be shown right now 453 mPipelineState.incrementTo(STATE_PRE_GROUP_FILTERING); 454 filterNotifs(mAllEntries, mNotifList, mNotifPreGroupFilters); 455 456 // Step 3: Group notifications with the same group key and set summaries 457 mPipelineState.incrementTo(STATE_GROUPING); 458 groupNotifs(mNotifList, mNewNotifList); 459 applyNewNotifList(); 460 pruneIncompleteGroups(mNotifList); 461 462 // Step 4: Group transforming 463 // Move some notifs out of their groups and up to top-level (mostly used for heads-upping) 464 dispatchOnBeforeTransformGroups(mReadOnlyNotifList); 465 mPipelineState.incrementTo(STATE_TRANSFORMING); 466 promoteNotifs(mNotifList); 467 pruneIncompleteGroups(mNotifList); 468 469 // Step 4.5: Reassign/revert any groups to maintain visual stability 470 mPipelineState.incrementTo(STATE_GROUP_STABILIZING); 471 stabilizeGroupingNotifs(mNotifList); 472 473 // Step 5: Section & Sort 474 // Assign each top-level entry a section, and copy to all of its children 475 dispatchOnBeforeSort(mReadOnlyNotifList); 476 mPipelineState.incrementTo(STATE_SORTING); 477 assignSections(); 478 notifySectionEntriesUpdated(); 479 // Sort the list by section and then within section by our list of custom comparators 480 sortListAndGroups(); 481 482 // Step 6: Filter out entries after pre-group filtering, grouping, promoting, and sorting 483 // Now filters can see grouping, sectioning, and order information to determine whether 484 // to filter or not. 485 dispatchOnBeforeFinalizeFilter(mReadOnlyNotifList); 486 mPipelineState.incrementTo(STATE_FINALIZE_FILTERING); 487 filterNotifs(mNotifList, mNewNotifList, mNotifFinalizeFilters); 488 applyNewNotifList(); 489 pruneIncompleteGroups(mNotifList); 490 491 // Step 7: Lock in our group structure and log anything that's changed since the last run 492 mPipelineState.incrementTo(STATE_FINALIZING); 493 logChanges(); 494 freeEmptyGroups(); 495 cleanupPluggables(); 496 497 // Step 8: Dispatch the new list, first to any listeners and then to the view layer 498 dispatchOnBeforeRenderList(mReadOnlyNotifList); 499 Trace.beginSection("ShadeListBuilder.onRenderList"); 500 if (mOnRenderListListener != null) { 501 mOnRenderListListener.onRenderList(mReadOnlyNotifList); 502 } 503 Trace.endSection(); 504 505 Trace.beginSection("ShadeListBuilder.logEndBuildList"); 506 // Step 9: We're done! 507 mLogger.logEndBuildList( 508 mIterationCount, 509 mReadOnlyNotifList.size(), 510 countChildren(mReadOnlyNotifList), 511 /* enforcedVisualStability */ !mNotifStabilityManager.isEveryChangeAllowed()); 512 if (mAlwaysLogList || mIterationCount % 10 == 0) { 513 Trace.beginSection("ShadeListBuilder.logFinalList"); 514 mLogger.logFinalList(mNotifList); 515 Trace.endSection(); 516 } 517 Trace.endSection(); 518 mPipelineState.setState(STATE_IDLE); 519 mIterationCount++; 520 Trace.endSection(); 521 } 522 notifySectionEntriesUpdated()523 private void notifySectionEntriesUpdated() { 524 Trace.beginSection("ShadeListBuilder.notifySectionEntriesUpdated"); 525 mTempSectionMembers.clear(); 526 for (NotifSection section : mNotifSections) { 527 for (PipelineEntry entry : mNotifList) { 528 if (section == entry.getSection()) { 529 mTempSectionMembers.add(entry); 530 } 531 } 532 Trace.beginSection(section.getLabel()); 533 section.getSectioner().onEntriesUpdated(mTempSectionMembers); 534 Trace.endSection(); 535 mTempSectionMembers.clear(); 536 } 537 Trace.endSection(); 538 } 539 540 /** 541 * Points mNotifList to the list stored in mNewNotifList. 542 * Reuses the (emptied) mNotifList as mNewNotifList. 543 * 544 * Accordingly, updates the ReadOnlyNotifList pointers. 545 */ applyNewNotifList()546 private void applyNewNotifList() { 547 mNotifList.clear(); 548 List<PipelineEntry> emptyList = mNotifList; 549 mNotifList = mNewNotifList; 550 mNewNotifList = emptyList; 551 552 List<PipelineEntry> readOnlyNotifList = mReadOnlyNotifList; 553 mReadOnlyNotifList = mReadOnlyNewNotifList; 554 mReadOnlyNewNotifList = readOnlyNotifList; 555 } 556 resetNotifs()557 private void resetNotifs() { 558 for (GroupEntry group : mGroups.values()) { 559 group.beginNewAttachState(); 560 group.clearChildren(); 561 group.setSummary(null); 562 } 563 564 for (NotificationEntry entry : mAllEntries) { 565 entry.beginNewAttachState(); 566 } 567 568 for (BundleEntry be : mIdToBundleEntry.values()) { 569 be.beginNewAttachState(); 570 be.clearChildren(); 571 // BundleEntry has not representative summary so we do not need to clear it here. 572 } 573 mNotifList.clear(); 574 } 575 filterNotifs( Collection<? extends PipelineEntry> entries, List<PipelineEntry> out, List<NotifFilter> filters)576 private void filterNotifs( 577 Collection<? extends PipelineEntry> entries, 578 List<PipelineEntry> out, 579 List<NotifFilter> filters) { 580 Trace.beginSection("ShadeListBuilder.filterNotifs"); 581 final long now = UseElapsedRealtimeForCreationTime.getCurrentTime(mSystemClock); 582 for (PipelineEntry entry : entries) { 583 if (entry instanceof GroupEntry) { 584 final GroupEntry groupEntry = (GroupEntry) entry; 585 586 // apply filter on its summary 587 final NotificationEntry summary = groupEntry.getRepresentativeEntry(); 588 if (applyFilters(summary, now, filters)) { 589 groupEntry.setSummary(null); 590 annulAddition(summary); 591 } 592 593 // apply filter on its children 594 final List<NotificationEntry> children = groupEntry.getRawChildren(); 595 for (int j = children.size() - 1; j >= 0; j--) { 596 final NotificationEntry child = children.get(j); 597 if (applyFilters(child, now, filters)) { 598 children.remove(child); 599 annulAddition(child); 600 } 601 } 602 603 out.add(groupEntry); 604 } else { 605 if (applyFilters((NotificationEntry) entry, now, filters)) { 606 annulAddition(entry); 607 } else { 608 out.add(entry); 609 } 610 } 611 } 612 Trace.endSection(); 613 } 614 groupNotifs(List<PipelineEntry> entries, List<PipelineEntry> out)615 private void groupNotifs(List<PipelineEntry> entries, List<PipelineEntry> out) { 616 Trace.beginSection("ShadeListBuilder.groupNotifs"); 617 for (PipelineEntry PipelineEntry : entries) { 618 // since grouping hasn't happened yet, all notifs are NotificationEntries 619 NotificationEntry entry = (NotificationEntry) PipelineEntry; 620 if (entry.getSbn().isGroup()) { 621 final String topLevelKey = entry.getSbn().getGroupKey(); 622 623 GroupEntry group = mGroups.get(topLevelKey); 624 if (group == null) { 625 group = new GroupEntry(topLevelKey, 626 UseElapsedRealtimeForCreationTime.getCurrentTime(mSystemClock)); 627 mGroups.put(topLevelKey, group); 628 } 629 if (group.getParent() == null) { 630 group.setParent(ROOT_ENTRY); 631 out.add(group); 632 } 633 634 entry.setParent(group); 635 636 if (entry.getSbn().getNotification().isGroupSummary()) { 637 final NotificationEntry existingSummary = group.getSummary(); 638 639 if (existingSummary == null) { 640 group.setSummary(entry); 641 } else { 642 mLogger.logDuplicateSummary(mIterationCount, group, existingSummary, entry); 643 644 // Use whichever one was posted most recently 645 if (entry.getSbn().getPostTime() 646 > existingSummary.getSbn().getPostTime()) { 647 group.setSummary(entry); 648 annulAddition(existingSummary, out); 649 } else { 650 annulAddition(entry, out); 651 } 652 } 653 } else { 654 group.addChild(entry); 655 } 656 657 } else { 658 659 final String topLevelKey = entry.getKey(); 660 if (mGroups.containsKey(topLevelKey)) { 661 mLogger.logDuplicateTopLevelKey(mIterationCount, topLevelKey); 662 } else { 663 entry.setParent(ROOT_ENTRY); 664 out.add(entry); 665 } 666 } 667 } 668 Trace.endSection(); 669 } 670 stabilizeGroupingNotifs(List<PipelineEntry> topLevelList)671 private void stabilizeGroupingNotifs(List<PipelineEntry> topLevelList) { 672 if (getStabilityManager().isEveryChangeAllowed()) { 673 return; 674 } 675 Trace.beginSection("ShadeListBuilder.stabilizeGroupingNotifs"); 676 677 for (int i = 0; i < topLevelList.size(); i++) { 678 final PipelineEntry tle = topLevelList.get(i); 679 if (tle instanceof GroupEntry) { 680 // maybe put children back into their old group (including moving back to top-level) 681 GroupEntry groupEntry = (GroupEntry) tle; 682 List<NotificationEntry> children = groupEntry.getRawChildren(); 683 for (int j = 0; j < groupEntry.getChildren().size(); j++) { 684 if (maybeSuppressGroupChange(children.get(j), topLevelList)) { 685 // child was put back into its previous group, so we remove it from this 686 // group 687 children.remove(j); 688 j--; 689 } 690 } 691 } else if (tle instanceof NotificationEntry) { 692 // maybe put top-level-entries back into their previous groups 693 if (maybeSuppressGroupChange(tle.getRepresentativeEntry(), topLevelList)) { 694 // entry was put back into its previous group, so we remove it from the list of 695 // top-level-entries 696 topLevelList.remove(i); 697 i--; 698 } 699 } // Promoters ignore bundles so we don't have to demote any here. 700 } 701 Trace.endSection(); 702 } 703 704 /** 705 * Returns true if the group change was suppressed, else false 706 */ maybeSuppressGroupChange(NotificationEntry entry, List<PipelineEntry> out)707 private boolean maybeSuppressGroupChange(NotificationEntry entry, List<PipelineEntry> out) { 708 final PipelineEntry prevParent = entry.getPreviousAttachState().getParent(); 709 if (prevParent == null) { 710 // New entries are always allowed. 711 return false; 712 } 713 final PipelineEntry assignedParent = entry.getParent(); 714 if (prevParent == assignedParent) { 715 // Nothing to change. 716 return false; 717 } 718 if (prevParent != ROOT_ENTRY && prevParent.getParent() == null) { 719 // Previous parent was a group, which has been removed (hence, its parent is null). 720 // Always allow this group change, otherwise the child will remain attached to the 721 // removed group and be removed from the shade until visual stability ends. 722 return false; 723 } 724 // TODO: Rather than perform "half" of the move here and require the caller remove the child 725 // from the assignedParent, ideally we would have an atomic "move" operation. 726 if (!getStabilityManager().isGroupChangeAllowed(entry.getRepresentativeEntry())) { 727 entry.getAttachState().getSuppressedChanges().setParent(assignedParent); 728 entry.setParent(prevParent); 729 if (prevParent == ROOT_ENTRY) { 730 out.add(entry); 731 } else if (prevParent instanceof GroupEntry) { 732 ((GroupEntry) prevParent).addChild(entry); 733 if (!mGroups.containsKey(prevParent.getKey())) { 734 mGroups.put(prevParent.getKey(), (GroupEntry) prevParent); 735 } 736 } 737 // TODO(b/394483200): Revisit group stability for BundleEntry 738 return true; 739 } 740 return false; 741 } 742 promoteNotifs(List<PipelineEntry> list)743 private void promoteNotifs(List<PipelineEntry> list) { 744 Trace.beginSection("ShadeListBuilder.promoteNotifs"); 745 for (int i = 0; i < list.size(); i++) { 746 final PipelineEntry tle = list.get(i); 747 748 if (tle instanceof GroupEntry) { 749 final GroupEntry group = (GroupEntry) tle; 750 751 group.getRawChildren().removeIf(child -> { 752 final boolean shouldPromote = applyTopLevelPromoters(child); 753 754 if (shouldPromote) { 755 child.setParent(ROOT_ENTRY); 756 list.add(child); 757 } 758 759 return shouldPromote; 760 }); 761 } 762 } 763 Trace.endSection(); 764 } 765 pruneIncompleteGroups(List<PipelineEntry> shadeList)766 private void pruneIncompleteGroups(List<PipelineEntry> shadeList) { 767 Trace.beginSection("ShadeListBuilder.pruneIncompleteGroups"); 768 // Any group which lost a child on this run to stability is exempt from being pruned or 769 // having its summary promoted, regardless of how many children it has 770 Set<String> groupsWithChildrenLostToStability = 771 getGroupsWithChildrenLostToStability(shadeList); 772 // Groups with children lost to stability are exempt from summary promotion. 773 ArraySet<String> groupsExemptFromSummaryPromotion = 774 new ArraySet<>(groupsWithChildrenLostToStability); 775 // Any group which lost a child to filtering or promotion is exempt from having its summary 776 // promoted when it has no attached children. 777 addGroupsWithChildrenLostToFiltering(groupsExemptFromSummaryPromotion); 778 addGroupsWithChildrenLostToPromotion(shadeList, groupsExemptFromSummaryPromotion); 779 780 // Iterate backwards, so that we can remove elements without affecting indices of 781 // yet-to-be-accessed entries. 782 for (int i = shadeList.size() - 1; i >= 0; i--) { 783 final PipelineEntry tle = shadeList.get(i); 784 785 if (tle instanceof GroupEntry) { 786 final GroupEntry group = (GroupEntry) tle; 787 final List<NotificationEntry> children = group.getRawChildren(); 788 final boolean hasSummary = group.getSummary() != null; 789 790 if (hasSummary && children.size() == 0) { 791 if (groupsExemptFromSummaryPromotion.contains(group.getKey())) { 792 // This group lost a child on this run to promotion or stability, so it is 793 // exempt from having its summary promoted to the top level, so prune it. 794 // It has no children, so it will just vanish. 795 pruneGroupAtIndexAndPromoteAnyChildren(shadeList, group, i); 796 } else { 797 // For any other summary with no children, promote the summary. 798 pruneGroupAtIndexAndPromoteSummary(shadeList, group, i); 799 } 800 } else if (!hasSummary) { 801 // If the group doesn't provide a summary, ignore it and add 802 // any children it may have directly to top-level. 803 pruneGroupAtIndexAndPromoteAnyChildren(shadeList, group, i); 804 } else if (children.size() < MIN_CHILDREN_FOR_GROUP) { 805 // This group has a summary and insufficient, but nonzero children. 806 checkState(hasSummary, "group must have summary at this point"); 807 checkState(!children.isEmpty(), "empty group should have been promoted"); 808 809 if (groupsWithChildrenLostToStability.contains(group.getKey())) { 810 // This group lost a child on this run to stability, so it is exempt from 811 // the "min children" requirement; keep it around in case more children are 812 // added before changes are allowed again. 813 group.getAttachState().getSuppressedChanges().setWasPruneSuppressed(true); 814 continue; 815 } 816 if (group.wasAttachedInPreviousPass() 817 && !getStabilityManager().isGroupPruneAllowed(group)) { 818 checkState(!children.isEmpty(), "empty group should have been pruned"); 819 // This group was previously attached and group changes aren't 820 // allowed; keep it around until group changes are allowed again. 821 group.getAttachState().getSuppressedChanges().setWasPruneSuppressed(true); 822 continue; 823 } 824 825 // The group is too small, ignore it and add 826 // its children (if any) directly to top-level. 827 pruneGroupAtIndexAndPromoteAnyChildren(shadeList, group, i); 828 } 829 } 830 } 831 Trace.endSection(); 832 } 833 pruneGroupAtIndexAndPromoteSummary(List<PipelineEntry> shadeList, GroupEntry group, int index)834 private void pruneGroupAtIndexAndPromoteSummary(List<PipelineEntry> shadeList, 835 GroupEntry group, int index) { 836 // Validate that the group has no children 837 checkArgument(group.getChildren().isEmpty(), "group should have no children"); 838 839 NotificationEntry summary = group.getSummary(); 840 summary.setParent(ROOT_ENTRY); 841 // The list may be sorted; replace the group with the summary, in its place 842 PipelineEntry oldEntry = shadeList.set(index, summary); 843 844 // Validate that the replaced entry was the group entry 845 checkState(oldEntry == group); 846 847 group.setSummary(null); 848 annulAddition(group, shadeList); 849 summary.getAttachState().setGroupPruneReason( 850 "SUMMARY with no children @ " + mPipelineState.getStateName()); 851 } 852 pruneGroupAtIndexAndPromoteAnyChildren(List<PipelineEntry> shadeList, GroupEntry group, int index)853 private void pruneGroupAtIndexAndPromoteAnyChildren(List<PipelineEntry> shadeList, 854 GroupEntry group, int index) { 855 // REMOVE the GroupEntry at this index 856 PipelineEntry oldEntry = shadeList.remove(index); 857 858 // Validate that the replaced entry was the group entry 859 checkState(oldEntry == group); 860 861 List<NotificationEntry> children = group.getRawChildren(); 862 boolean hasSummary = group.getSummary() != null; 863 864 // Remove the group summary, if present, and leave detached. 865 if (hasSummary) { 866 final NotificationEntry summary = group.getSummary(); 867 group.setSummary(null); 868 annulAddition(summary, shadeList); 869 summary.getAttachState().setGroupPruneReason( 870 "SUMMARY with too few children @ " + mPipelineState.getStateName()); 871 } 872 873 // Promote any children 874 if (!children.isEmpty()) { 875 // create the reason we will report on the child for why its group was pruned. 876 String childReason = hasSummary 877 ? ("CHILD with " + (children.size() - 1) + " siblings @ " 878 + mPipelineState.getStateName()) 879 : ("CHILD with no summary @ " + mPipelineState.getStateName()); 880 881 // Remove children from the group and add them to the shadeList. 882 for (int j = 0; j < children.size(); j++) { 883 final NotificationEntry child = children.get(j); 884 child.setParent(ROOT_ENTRY); 885 child.getAttachState().setGroupPruneReason(requireNonNull(childReason)); 886 } 887 // The list may be sorted, so add the children in order where the group was. 888 shadeList.addAll(index, children); 889 children.clear(); 890 } 891 892 annulAddition(group, shadeList); 893 } 894 895 /** 896 * Collect the keys of any groups which have already lost a child to stability this run. 897 * 898 * If stability is being enforced, then {@link #stabilizeGroupingNotifs(List)} might have 899 * detached some children from their groups and left them at the top level because the child was 900 * previously attached at the top level. Doing so would set the 901 * {@link SuppressedAttachState#getParent() suppressed parent} for the current attach state. 902 * 903 * If we've already removed a child from this group, we don't want to remove any more children 904 * from the group (even if that would leave only a single notification in the group) because 905 * that could cascade over multiple runs and allow a large group of notifications all show up as 906 * top level (ungrouped) notifications. 907 */ 908 @NonNull getGroupsWithChildrenLostToStability(List<PipelineEntry> shadeList)909 private Set<String> getGroupsWithChildrenLostToStability(List<PipelineEntry> shadeList) { 910 if (getStabilityManager().isEveryChangeAllowed()) { 911 return Collections.emptySet(); 912 } 913 ArraySet<String> groupsWithChildrenLostToStability = new ArraySet<>(); 914 for (int i = 0; i < shadeList.size(); i++) { 915 final PipelineEntry tle = shadeList.get(i); 916 final PipelineEntry suppressedParent = 917 tle.getAttachState().getSuppressedChanges().getParent(); 918 if (suppressedParent != null) { 919 // This top-level-entry was supposed to be attached to this group, 920 // so mark the group as having lost a child to stability. 921 groupsWithChildrenLostToStability.add(suppressedParent.getKey()); 922 } 923 } 924 return groupsWithChildrenLostToStability; 925 } 926 927 /** 928 * Collect the keys of any groups which have already lost a child to a {@link NotifPromoter} 929 * this run. 930 * 931 * These groups will be exempt from appearing without any children. 932 */ addGroupsWithChildrenLostToPromotion(List<PipelineEntry> shadeList, Set<String> out)933 private void addGroupsWithChildrenLostToPromotion(List<PipelineEntry> shadeList, 934 Set<String> out) { 935 for (int i = 0; i < shadeList.size(); i++) { 936 final PipelineEntry tle = shadeList.get(i); 937 if (tle.getAttachState().getPromoter() != null) { 938 // This top-level-entry was part of a group, but was promoted out of it. 939 final String groupKey = tle.getRepresentativeEntry().getSbn().getGroupKey(); 940 out.add(groupKey); 941 } 942 } 943 } 944 945 /** 946 * Collect the keys of any groups which have already lost a child to a {@link NotifFilter} 947 * this run. 948 * 949 * These groups will be exempt from appearing without any children. 950 */ addGroupsWithChildrenLostToFiltering(Set<String> out)951 private void addGroupsWithChildrenLostToFiltering(Set<String> out) { 952 for (PipelineEntry tle : mAllEntries) { 953 StatusBarNotification sbn = tle.getRepresentativeEntry().getSbn(); 954 if (sbn.isGroup() 955 && !sbn.getNotification().isGroupSummary() 956 && tle.getAttachState().getExcludingFilter() != null) { 957 out.add(sbn.getGroupKey()); 958 } 959 } 960 } 961 962 /** 963 * If a PipelineEntry was added to the shade list and then later removed (e.g. because it was a 964 * group that was broken up), this method will erase any bookkeeping traces of that addition 965 * and/or check that they were already erased. 966 * 967 * Before calling this method, the entry must already have been removed from its parent. If 968 * it's a group, its summary must be null and its children must be empty. 969 */ annulAddition(PipelineEntry entry, List<PipelineEntry> shadeList)970 private void annulAddition(PipelineEntry entry, List<PipelineEntry> shadeList) { 971 972 // This function does very little, but if any of its assumptions are violated (and it has a 973 // lot of them), it will put the system into an inconsistent state. So we check all of them 974 // here. 975 976 if (entry.getParent() == null) { 977 throw new IllegalStateException( 978 "Cannot nullify addition of " + entry.getKey() + ": no parent."); 979 } 980 981 if (entry.getParent() == ROOT_ENTRY) { 982 if (shadeList.contains(entry)) { 983 throw new IllegalStateException("Cannot nullify addition of " + entry.getKey() 984 + ": it's still in the shade list."); 985 } 986 } 987 988 if (entry instanceof GroupEntry) { 989 GroupEntry ge = (GroupEntry) entry; 990 if (ge.getSummary() != null) { 991 throw new IllegalStateException( 992 "Cannot nullify group " + ge.getKey() + ": summary is not null"); 993 } 994 if (!ge.getChildren().isEmpty()) { 995 throw new IllegalStateException( 996 "Cannot nullify group " + ge.getKey() + ": still has children"); 997 } 998 } else if (entry instanceof NotificationEntry 999 && entry.getParent() instanceof GroupEntry) { 1000 GroupEntry parentGroupEntry = (GroupEntry) entry.getParent(); 1001 if (entry == parentGroupEntry.getSummary() 1002 || parentGroupEntry.getChildren().contains(entry)) { 1003 throw new IllegalStateException("Cannot nullify addition of child " 1004 + entry.getKey() + ": it's still attached to its parent."); 1005 } 1006 } 1007 1008 annulAddition(entry); 1009 1010 } 1011 1012 /** 1013 * Erases bookkeeping traces stored on an entry when it is removed from the notif list. 1014 * This can happen if the entry is removed from a group that was broken up or if the entry was 1015 * filtered out during any of the filtering steps. 1016 */ annulAddition(PipelineEntry entry)1017 private void annulAddition(PipelineEntry entry) { 1018 entry.getAttachState().detach(); 1019 } 1020 assignSections()1021 private void assignSections() { 1022 Trace.beginSection("ShadeListBuilder.assignSections"); 1023 // Assign sections to top-level elements and their children 1024 for (PipelineEntry entry : mNotifList) { 1025 NotifSection section = applySections(entry); 1026 if (entry instanceof GroupEntry) { 1027 GroupEntry parent = (GroupEntry) entry; 1028 for (NotificationEntry child : parent.getChildren()) { 1029 setEntrySection(child, section); 1030 } 1031 } 1032 } 1033 Trace.endSection(); 1034 } 1035 sortListAndGroups()1036 private void sortListAndGroups() { 1037 Trace.beginSection("ShadeListBuilder.sortListAndGroups"); 1038 sortWithSemiStableSort(); 1039 Trace.endSection(); 1040 } 1041 sortWithSemiStableSort()1042 private void sortWithSemiStableSort() { 1043 // Sort each group's children 1044 boolean allSorted = true; 1045 for (PipelineEntry entry : mNotifList) { 1046 if (entry instanceof GroupEntry) { 1047 GroupEntry parent = (GroupEntry) entry; 1048 allSorted &= sortGroupChildren(parent.getRawChildren()); 1049 } 1050 } 1051 // Sort each section within the top level list 1052 mNotifList.sort(mTopLevelComparator); 1053 if (!getStabilityManager().isEveryChangeAllowed()) { 1054 for (List<PipelineEntry> subList : getSectionSubLists(mNotifList)) { 1055 allSorted &= mSemiStableSort.stabilizeTo(subList, mStableOrder, mNewNotifList); 1056 } 1057 applyNewNotifList(); 1058 } 1059 assignIndexes(mNotifList); 1060 if (!allSorted) { 1061 // Report suppressed order changes 1062 getStabilityManager().onEntryReorderSuppressed(); 1063 } 1064 } 1065 getSectionSubLists(List<PipelineEntry> entries)1066 private Iterable<List<PipelineEntry>> getSectionSubLists(List<PipelineEntry> entries) { 1067 return ShadeListBuilderHelper.INSTANCE.getSectionSubLists(entries); 1068 } 1069 sortGroupChildren(List<NotificationEntry> entries)1070 private boolean sortGroupChildren(List<NotificationEntry> entries) { 1071 if (getStabilityManager().isEveryChangeAllowed()) { 1072 entries.sort(mGroupChildrenComparator); 1073 return true; 1074 } else { 1075 return mSemiStableSort.sort(entries, mStableOrder, mGroupChildrenComparator); 1076 } 1077 } 1078 1079 /** Determine whether the items in the list are sorted according to the comparator */ 1080 @VisibleForTesting isSorted(List<T> items, Comparator<? super T> comparator)1081 public static <T> boolean isSorted(List<T> items, Comparator<? super T> comparator) { 1082 if (items.size() <= 1) { 1083 return true; 1084 } 1085 Iterator<T> iterator = items.iterator(); 1086 T previous = iterator.next(); 1087 T current; 1088 while (iterator.hasNext()) { 1089 current = iterator.next(); 1090 if (comparator.compare(previous, current) > 0) { 1091 return false; 1092 } 1093 previous = current; 1094 } 1095 return true; 1096 } 1097 1098 /** 1099 * Assign the index of each notification relative to the total order 1100 */ assignIndexes(List<PipelineEntry> notifList)1101 private void assignIndexes(List<PipelineEntry> notifList) { 1102 if (notifList.size() == 0) return; 1103 NotifSection currentSection = requireNonNull(notifList.get(0).getSection()); 1104 int sectionMemberIndex = 0; 1105 for (int i = 0; i < notifList.size(); i++) { 1106 final PipelineEntry entry = notifList.get(i); 1107 NotifSection section = requireNonNull(entry.getSection()); 1108 if (section.getIndex() != currentSection.getIndex()) { 1109 sectionMemberIndex = 0; 1110 currentSection = section; 1111 } 1112 entry.getAttachState().setStableIndex(sectionMemberIndex++); 1113 if (entry instanceof GroupEntry) { 1114 final GroupEntry parent = (GroupEntry) entry; 1115 final NotificationEntry summary = parent.getSummary(); 1116 if (summary != null) { 1117 summary.getAttachState().setStableIndex(sectionMemberIndex++); 1118 } 1119 for (NotificationEntry child : parent.getChildren()) { 1120 child.getAttachState().setStableIndex(sectionMemberIndex++); 1121 } 1122 } 1123 } 1124 } 1125 freeEmptyGroups()1126 private void freeEmptyGroups() { 1127 Trace.beginSection("ShadeListBuilder.freeEmptyGroups"); 1128 mGroups.values().removeIf(ge -> ge.getSummary() == null && ge.getChildren().isEmpty()); 1129 Trace.endSection(); 1130 } 1131 logChanges()1132 private void logChanges() { 1133 Trace.beginSection("ShadeListBuilder.logChanges"); 1134 for (NotificationEntry entry : mAllEntries) { 1135 logAttachStateChanges(entry); 1136 } 1137 for (GroupEntry group : mGroups.values()) { 1138 logAttachStateChanges(group); 1139 } 1140 Trace.endSection(); 1141 } 1142 logAttachStateChanges(PipelineEntry entry)1143 private void logAttachStateChanges(PipelineEntry entry) { 1144 1145 final ListAttachState curr = entry.getAttachState(); 1146 final ListAttachState prev = entry.getPreviousAttachState(); 1147 1148 if (!Objects.equals(curr, prev)) { 1149 mLogger.logEntryAttachStateChanged( 1150 mIterationCount, 1151 entry, 1152 prev.getParent(), 1153 curr.getParent()); 1154 1155 if (curr.getParent() != prev.getParent()) { 1156 mLogger.logParentChanged(mIterationCount, prev.getParent(), curr.getParent()); 1157 } 1158 1159 PipelineEntry currSuppressedParent = curr.getSuppressedChanges().getParent(); 1160 PipelineEntry prevSuppressedParent = prev.getSuppressedChanges().getParent(); 1161 if (currSuppressedParent != null && (prevSuppressedParent == null 1162 || !prevSuppressedParent.getKey().equals(currSuppressedParent.getKey()))) { 1163 mLogger.logParentChangeSuppressedStarted( 1164 mIterationCount, 1165 currSuppressedParent, 1166 curr.getParent()); 1167 } 1168 if (prevSuppressedParent != null && currSuppressedParent == null) { 1169 mLogger.logParentChangeSuppressedStopped( 1170 mIterationCount, 1171 prevSuppressedParent, 1172 prev.getParent()); 1173 } 1174 1175 if (curr.getSuppressedChanges().getSection() != null) { 1176 mLogger.logSectionChangeSuppressed( 1177 mIterationCount, 1178 curr.getSuppressedChanges().getSection(), 1179 curr.getSection()); 1180 } 1181 1182 if (curr.getSuppressedChanges().getWasPruneSuppressed()) { 1183 mLogger.logGroupPruningSuppressed( 1184 mIterationCount, 1185 curr.getParent()); 1186 } 1187 1188 if (!Objects.equals(curr.getGroupPruneReason(), prev.getGroupPruneReason())) { 1189 mLogger.logPrunedReasonChanged( 1190 mIterationCount, 1191 prev.getGroupPruneReason(), 1192 curr.getGroupPruneReason()); 1193 } 1194 1195 if (curr.getExcludingFilter() != prev.getExcludingFilter()) { 1196 mLogger.logFilterChanged( 1197 mIterationCount, 1198 prev.getExcludingFilter(), 1199 curr.getExcludingFilter()); 1200 } 1201 1202 // When something gets detached, its promoter and section are always set to null, so 1203 // don't bother logging those changes. 1204 final boolean wasDetached = curr.getParent() == null && prev.getParent() != null; 1205 1206 if (!wasDetached && curr.getPromoter() != prev.getPromoter()) { 1207 mLogger.logPromoterChanged( 1208 mIterationCount, 1209 prev.getPromoter(), 1210 curr.getPromoter()); 1211 } 1212 1213 if (!wasDetached && curr.getSection() != prev.getSection()) { 1214 mLogger.logSectionChanged( 1215 mIterationCount, 1216 prev.getSection(), 1217 curr.getSection()); 1218 } 1219 } 1220 } 1221 onBeginRun()1222 private void onBeginRun() { 1223 getStabilityManager().onBeginRun(); 1224 } 1225 cleanupPluggables()1226 private void cleanupPluggables() { 1227 Trace.beginSection("ShadeListBuilder.cleanupPluggables"); 1228 callOnCleanup(mNotifPreGroupFilters); 1229 callOnCleanup(mNotifPromoters); 1230 callOnCleanup(mNotifFinalizeFilters); 1231 callOnCleanup(mNotifComparators); 1232 1233 for (int i = 0; i < mNotifSections.size(); i++) { 1234 final NotifSection notifSection = mNotifSections.get(i); 1235 notifSection.getSectioner().onCleanup(); 1236 final NotifComparator comparator = notifSection.getComparator(); 1237 if (comparator != null) { 1238 comparator.onCleanup(); 1239 } 1240 } 1241 1242 callOnCleanup(List.of(getStabilityManager())); 1243 Trace.endSection(); 1244 } 1245 callOnCleanup(List<? extends Pluggable<?>> pluggables)1246 private void callOnCleanup(List<? extends Pluggable<?>> pluggables) { 1247 for (int i = 0; i < pluggables.size(); i++) { 1248 pluggables.get(i).onCleanup(); 1249 } 1250 } 1251 1252 @Nullable getSectionComparator( @onNull PipelineEntry o1, @NonNull PipelineEntry o2)1253 private NotifComparator getSectionComparator( 1254 @NonNull PipelineEntry o1, @NonNull PipelineEntry o2) { 1255 final NotifSection section = o1.getSection(); 1256 if (section != o2.getSection()) { 1257 throw new RuntimeException("Entry ordering should only be done within sections"); 1258 } 1259 if (section != null) { 1260 return section.getComparator(); 1261 } 1262 return null; 1263 } 1264 1265 private final Comparator<PipelineEntry> mTopLevelComparator = (o1, o2) -> { 1266 int cmp = Integer.compare( 1267 o1.getSectionIndex(), 1268 o2.getSectionIndex()); 1269 if (cmp != 0) return cmp; 1270 1271 NotifComparator sectionComparator = getSectionComparator(o1, o2); 1272 if (sectionComparator != null) { 1273 cmp = sectionComparator.compare(o1, o2); 1274 if (cmp != 0) return cmp; 1275 } 1276 1277 for (int i = 0; i < mNotifComparators.size(); i++) { 1278 cmp = mNotifComparators.get(i).compare(o1, o2); 1279 if (cmp != 0) return cmp; 1280 } 1281 1282 cmp = Integer.compare( 1283 o1.getRepresentativeEntry().getRanking().getRank(), 1284 o2.getRepresentativeEntry().getRanking().getRank()); 1285 if (cmp != 0) return cmp; 1286 1287 cmp = -1 * Long.compare( 1288 o1.getRepresentativeEntry().getSbn().getNotification().getWhen(), 1289 o2.getRepresentativeEntry().getSbn().getNotification().getWhen()); 1290 1291 return cmp; 1292 }; 1293 1294 1295 private final Comparator<NotificationEntry> mGroupChildrenComparator = (o1, o2) -> { 1296 int cmp = Integer.compare( 1297 o1.getRepresentativeEntry().getRanking().getRank(), 1298 o2.getRepresentativeEntry().getRanking().getRank()); 1299 if (cmp != 0) return cmp; 1300 1301 cmp = -1 * Long.compare( 1302 o1.getRepresentativeEntry().getSbn().getNotification().getWhen(), 1303 o2.getRepresentativeEntry().getSbn().getNotification().getWhen()); 1304 return cmp; 1305 }; 1306 1307 @Nullable getStableOrderRank(PipelineEntry entry)1308 private Integer getStableOrderRank(PipelineEntry entry) { 1309 if (getStabilityManager().isEntryReorderingAllowed(entry)) { 1310 // let the stability manager constrain or allow reordering 1311 return null; 1312 } 1313 if (entry.getAttachState().getSectionIndex() 1314 != entry.getPreviousAttachState().getSectionIndex()) { 1315 // stable index is only valid within the same section; otherwise we allow reordering 1316 return null; 1317 } 1318 final int stableIndex = entry.getPreviousAttachState().getStableIndex(); 1319 return stableIndex == -1 ? null : stableIndex; 1320 } 1321 applyFilters(NotificationEntry entry, long now, List<NotifFilter> filters)1322 private boolean applyFilters(NotificationEntry entry, long now, List<NotifFilter> filters) { 1323 final NotifFilter filter = findRejectingFilter(entry, now, filters); 1324 entry.getAttachState().setExcludingFilter(filter); 1325 if (filter != null) { 1326 // notification is removed from the list, so we reset its initialization time 1327 if (NotificationBundleUi.isEnabled()) { 1328 if (entry.getRow() != null) { 1329 entry.getRow().resetInitializationTime(); 1330 } 1331 } else { 1332 entry.resetInitializationTime(); 1333 } 1334 } 1335 return filter != null; 1336 } 1337 findRejectingFilter(NotificationEntry entry, long now, List<NotifFilter> filters)1338 @Nullable private static NotifFilter findRejectingFilter(NotificationEntry entry, long now, 1339 List<NotifFilter> filters) { 1340 final int size = filters.size(); 1341 1342 for (int i = 0; i < size; i++) { 1343 NotifFilter filter = filters.get(i); 1344 if (filter.shouldFilterOut(entry, now)) { 1345 return filter; 1346 } 1347 } 1348 return null; 1349 } 1350 applyTopLevelPromoters(NotificationEntry entry)1351 private boolean applyTopLevelPromoters(NotificationEntry entry) { 1352 NotifPromoter promoter = findPromoter(entry); 1353 entry.getAttachState().setPromoter(promoter); 1354 return promoter != null; 1355 } 1356 findPromoter(NotificationEntry entry)1357 @Nullable private NotifPromoter findPromoter(NotificationEntry entry) { 1358 for (int i = 0; i < mNotifPromoters.size(); i++) { 1359 NotifPromoter promoter = mNotifPromoters.get(i); 1360 if (promoter.shouldPromoteToTopLevel(entry)) { 1361 return promoter; 1362 } 1363 } 1364 return null; 1365 } 1366 applySections(PipelineEntry entry)1367 private NotifSection applySections(PipelineEntry entry) { 1368 final NotifSection newSection = findSection(entry); 1369 final ListAttachState prevAttachState = entry.getPreviousAttachState(); 1370 1371 NotifSection finalSection = newSection; 1372 1373 // have we seen this entry before and are we changing its section? 1374 if (entry.wasAttachedInPreviousPass() && newSection != prevAttachState.getSection()) { 1375 1376 // are section changes allowed? 1377 if (!getStabilityManager().isSectionChangeAllowed(entry.getRepresentativeEntry())) { 1378 // record the section that we wanted to change to 1379 entry.getAttachState().getSuppressedChanges().setSection(newSection); 1380 1381 // keep the previous section 1382 finalSection = prevAttachState.getSection(); 1383 } 1384 } 1385 1386 setEntrySection(entry, finalSection); 1387 return finalSection; 1388 } 1389 setEntrySection(PipelineEntry entry, NotifSection finalSection)1390 private void setEntrySection(PipelineEntry entry, NotifSection finalSection) { 1391 entry.getAttachState().setSection(finalSection); 1392 NotificationEntry representativeEntry = entry.getRepresentativeEntry(); 1393 if (representativeEntry != null) { 1394 representativeEntry.getAttachState().setSection(finalSection); 1395 if (finalSection != null) { 1396 representativeEntry.setBucket(finalSection.getBucket()); 1397 } 1398 } 1399 } 1400 1401 @NonNull findSection(PipelineEntry entry)1402 private NotifSection findSection(PipelineEntry entry) { 1403 for (int i = 0; i < mNotifSections.size(); i++) { 1404 NotifSection section = mNotifSections.get(i); 1405 if (section.getSectioner().isInSection(entry)) { 1406 return section; 1407 } 1408 } 1409 throw new RuntimeException("Missing default sectioner!"); 1410 } 1411 rebuildListIfBefore(@ipelineState.StateName int rebuildState)1412 private void rebuildListIfBefore(@PipelineState.StateName int rebuildState) { 1413 final @PipelineState.StateName int currentState = mPipelineState.getState(); 1414 1415 // If the pipeline is idle, requesting an invalidation is always okay, and starts a new run. 1416 if (currentState == STATE_IDLE) { 1417 scheduleRebuild(/* reentrant = */ false, rebuildState); 1418 return; 1419 } 1420 1421 // If the pipeline is running, it is okay to request an invalidation of a *later* stage. 1422 // Since the current pipeline run hasn't run it yet, no new pipeline run is needed. 1423 if (rebuildState > currentState) { 1424 return; 1425 } 1426 1427 // If the pipeline is running, it is bad to request an invalidation of *earlier* stages or 1428 // the *current* stage; this will run the pipeline more often than needed, and may even 1429 // cause an infinite loop of pipeline runs. 1430 // 1431 // Unfortunately, there are some unfixed bugs that cause reentrant pipeline runs, so we keep 1432 // a counter and allow a few reentrant runs in a row between any two non-reentrant runs. 1433 // 1434 // It is technically possible for a *pair* of invalidations, one reentrant and one not, to 1435 // trigger *each other*, alternating responsibility for pipeline runs in an infinite loop 1436 // but constantly resetting the reentrant run counter. Hopefully that doesn't happen. 1437 scheduleRebuild(/* reentrant = */ true, rebuildState); 1438 } 1439 scheduleRebuild(boolean reentrant)1440 private void scheduleRebuild(boolean reentrant) { 1441 scheduleRebuild(reentrant, STATE_IDLE); 1442 } 1443 scheduleRebuild(boolean reentrant, @PipelineState.StateName int rebuildState)1444 private void scheduleRebuild(boolean reentrant, @PipelineState.StateName int rebuildState) { 1445 if (!reentrant) { 1446 mConsecutiveReentrantRebuilds = 0; 1447 mChoreographer.schedule(); 1448 return; 1449 } 1450 1451 final @PipelineState.StateName int currentState = mPipelineState.getState(); 1452 1453 final String rebuildStateName = PipelineState.getStateName(rebuildState); 1454 final String currentStateName = PipelineState.getStateName(currentState); 1455 final IllegalStateException exception = new IllegalStateException( 1456 "Reentrant notification pipeline rebuild of state " + rebuildStateName 1457 + " while pipeline in state " + currentStateName + "."); 1458 1459 mConsecutiveReentrantRebuilds++; 1460 1461 if (mConsecutiveReentrantRebuilds > MAX_CONSECUTIVE_REENTRANT_REBUILDS) { 1462 Log.e(TAG, "Crashing after more than " + MAX_CONSECUTIVE_REENTRANT_REBUILDS 1463 + " consecutive reentrant notification pipeline rebuilds.", exception); 1464 throw exception; 1465 } 1466 1467 Log.wtf(TAG, "Allowing " + mConsecutiveReentrantRebuilds 1468 + " consecutive reentrant notification pipeline rebuild(s).", exception); 1469 mChoreographer.schedule(); 1470 } 1471 countChildren(List<PipelineEntry> entries)1472 private static int countChildren(List<PipelineEntry> entries) { 1473 int count = 0; 1474 for (int i = 0; i < entries.size(); i++) { 1475 final PipelineEntry entry = entries.get(i); 1476 if (entry instanceof GroupEntry) { 1477 count += ((GroupEntry) entry).getChildren().size(); 1478 } 1479 } 1480 return count; 1481 } 1482 dispatchOnBeforeTransformGroups(List<PipelineEntry> entries)1483 private void dispatchOnBeforeTransformGroups(List<PipelineEntry> entries) { 1484 Trace.beginSection("ShadeListBuilder.dispatchOnBeforeTransformGroups"); 1485 mOnBeforeTransformGroupsListeners.forEachTraced(listener -> { 1486 listener.onBeforeTransformGroups(entries); 1487 }); 1488 Trace.endSection(); 1489 } 1490 dispatchOnBeforeSort(List<PipelineEntry> entries)1491 private void dispatchOnBeforeSort(List<PipelineEntry> entries) { 1492 Trace.beginSection("ShadeListBuilder.dispatchOnBeforeSort"); 1493 mOnBeforeSortListeners.forEachTraced(listener -> { 1494 listener.onBeforeSort(entries); 1495 }); 1496 Trace.endSection(); 1497 } 1498 dispatchOnBeforeFinalizeFilter(List<PipelineEntry> entries)1499 private void dispatchOnBeforeFinalizeFilter(List<PipelineEntry> entries) { 1500 Trace.beginSection("ShadeListBuilder.dispatchOnBeforeFinalizeFilter"); 1501 mOnBeforeFinalizeFilterListeners.forEachTraced(listener -> { 1502 listener.onBeforeFinalizeFilter(entries); 1503 }); 1504 Trace.endSection(); 1505 } 1506 dispatchOnBeforeRenderList(List<PipelineEntry> entries)1507 private void dispatchOnBeforeRenderList(List<PipelineEntry> entries) { 1508 Trace.beginSection("ShadeListBuilder.dispatchOnBeforeRenderList"); 1509 mOnBeforeRenderListListeners.forEachTraced(listener -> { 1510 listener.onBeforeRenderList(entries); 1511 }); 1512 Trace.endSection(); 1513 } 1514 1515 @Override dump(PrintWriter pw, @NonNull String[] args)1516 public void dump(PrintWriter pw, @NonNull String[] args) { 1517 pw.println("\t" + TAG + " shade notifications:"); 1518 if (getShadeList().size() == 0) { 1519 pw.println("\t\t None"); 1520 } 1521 1522 pw.println(ListDumper.dumpTree( 1523 getShadeList(), 1524 mInteractionTracker, 1525 true, 1526 "\t\t")); 1527 } 1528 1529 @Override dumpPipeline(@onNull PipelineDumper d)1530 public void dumpPipeline(@NonNull PipelineDumper d) { 1531 d.dump("choreographer", mChoreographer); 1532 d.dump("notifPreGroupFilters", mNotifPreGroupFilters); 1533 d.dump("onBeforeTransformGroupsListeners", mOnBeforeTransformGroupsListeners); 1534 d.dump("notifPromoters", mNotifPromoters); 1535 d.dump("onBeforeSortListeners", mOnBeforeSortListeners); 1536 d.dump("notifSections", mNotifSections); 1537 d.dump("notifComparators", mNotifComparators); 1538 d.dump("onBeforeFinalizeFilterListeners", mOnBeforeFinalizeFilterListeners); 1539 d.dump("notifFinalizeFilters", mNotifFinalizeFilters); 1540 d.dump("onBeforeRenderListListeners", mOnBeforeRenderListListeners); 1541 d.dump("onRenderListListener", mOnRenderListListener); 1542 } 1543 1544 /** See {@link #setOnRenderListListener(OnRenderListListener)} */ 1545 public interface OnRenderListListener { 1546 /** 1547 * Called with the final filtered, grouped, and sorted list. 1548 * 1549 * @param entries A read-only view into the current notif list. Note that this list is 1550 * backed by the live list and will change in response to new pipeline runs. 1551 */ onRenderList(@onNull List<PipelineEntry> entries)1552 void onRenderList(@NonNull List<PipelineEntry> entries); 1553 } 1554 1555 private static final NotifSectioner DEFAULT_SECTIONER = new NotifSectioner("UnknownSection", 1556 NotificationPriorityBucketKt.BUCKET_UNKNOWN) { 1557 @Override 1558 public boolean isInSection(PipelineEntry entry) { 1559 return true; 1560 } 1561 }; 1562 1563 private static final int MIN_CHILDREN_FOR_GROUP = 2; 1564 1565 private static final String TAG = "ShadeListBuilder"; 1566 } 1567