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.systemui.statusbar.notification.collection.GroupEntry.ROOT_ENTRY; 20 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_BUILD_STARTED; 21 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_FINALIZE_FILTERING; 22 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_FINALIZING; 23 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_GROUPING; 24 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_GROUP_STABILIZING; 25 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_IDLE; 26 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_PRE_GROUP_FILTERING; 27 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_RESETTING; 28 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_SORTING; 29 import static com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState.STATE_TRANSFORMING; 30 31 import android.annotation.MainThread; 32 import android.annotation.Nullable; 33 import android.os.Trace; 34 import android.util.ArrayMap; 35 36 import androidx.annotation.NonNull; 37 38 import com.android.systemui.Dumpable; 39 import com.android.systemui.dagger.SysUISingleton; 40 import com.android.systemui.dump.DumpManager; 41 import com.android.systemui.statusbar.NotificationInteractionTracker; 42 import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection; 43 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener; 44 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener; 45 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener; 46 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener; 47 import com.android.systemui.statusbar.notification.collection.listbuilder.PipelineState; 48 import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderLogger; 49 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator; 50 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator; 51 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; 52 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter; 53 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner; 54 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager; 55 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable; 56 import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener; 57 import com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt; 58 import com.android.systemui.util.Assert; 59 import com.android.systemui.util.time.SystemClock; 60 61 import java.io.FileDescriptor; 62 import java.io.PrintWriter; 63 import java.util.ArrayList; 64 import java.util.Collection; 65 import java.util.Collections; 66 import java.util.Comparator; 67 import java.util.List; 68 import java.util.Map; 69 import java.util.Objects; 70 71 import javax.inject.Inject; 72 73 /** 74 * The second half of {@link NotifPipeline}. Sits downstream of the NotifCollection and transforms 75 * its "notification set" into the "shade list", the filtered, grouped, and sorted list of 76 * notifications that are currently present in the notification shade. 77 */ 78 @MainThread 79 @SysUISingleton 80 public class ShadeListBuilder implements Dumpable { 81 private final SystemClock mSystemClock; 82 private final ShadeListBuilderLogger mLogger; 83 private final NotificationInteractionTracker mInteractionTracker; 84 // used exclusivly by ShadeListBuilder#notifySectionEntriesUpdated 85 private final ArrayList<ListEntry> mTempSectionMembers = new ArrayList<>(); 86 87 private List<ListEntry> mNotifList = new ArrayList<>(); 88 private List<ListEntry> mNewNotifList = new ArrayList<>(); 89 90 private final PipelineState mPipelineState = new PipelineState(); 91 private final Map<String, GroupEntry> mGroups = new ArrayMap<>(); 92 private Collection<NotificationEntry> mAllEntries = Collections.emptyList(); 93 private int mIterationCount = 0; 94 95 private final List<NotifFilter> mNotifPreGroupFilters = new ArrayList<>(); 96 private final List<NotifPromoter> mNotifPromoters = new ArrayList<>(); 97 private final List<NotifFilter> mNotifFinalizeFilters = new ArrayList<>(); 98 private final List<NotifComparator> mNotifComparators = new ArrayList<>(); 99 private final List<NotifSection> mNotifSections = new ArrayList<>(); 100 @Nullable private NotifStabilityManager mNotifStabilityManager; 101 102 private final List<OnBeforeTransformGroupsListener> mOnBeforeTransformGroupsListeners = 103 new ArrayList<>(); 104 private final List<OnBeforeSortListener> mOnBeforeSortListeners = 105 new ArrayList<>(); 106 private final List<OnBeforeFinalizeFilterListener> mOnBeforeFinalizeFilterListeners = 107 new ArrayList<>(); 108 private final List<OnBeforeRenderListListener> mOnBeforeRenderListListeners = 109 new ArrayList<>(); 110 @Nullable private OnRenderListListener mOnRenderListListener; 111 112 private List<ListEntry> mReadOnlyNotifList = Collections.unmodifiableList(mNotifList); 113 private List<ListEntry> mReadOnlyNewNotifList = Collections.unmodifiableList(mNewNotifList); 114 115 @Inject ShadeListBuilder( SystemClock systemClock, ShadeListBuilderLogger logger, DumpManager dumpManager, NotificationInteractionTracker interactionTracker )116 public ShadeListBuilder( 117 SystemClock systemClock, 118 ShadeListBuilderLogger logger, 119 DumpManager dumpManager, 120 NotificationInteractionTracker interactionTracker 121 ) { 122 Assert.isMainThread(); 123 mSystemClock = systemClock; 124 mLogger = logger; 125 mInteractionTracker = interactionTracker; 126 dumpManager.registerDumpable(TAG, this); 127 128 setSectioners(Collections.emptyList()); 129 } 130 131 /** 132 * Attach the list builder to the NotifCollection. After this is called, it will start building 133 * the notif list in response to changes to the colletion. 134 */ attach(NotifCollection collection)135 public void attach(NotifCollection collection) { 136 Assert.isMainThread(); 137 collection.setBuildListener(mReadyForBuildListener); 138 } 139 140 /** 141 * Registers the listener that's responsible for rendering the notif list to the screen. Called 142 * At the very end of pipeline execution, after all other listeners and pluggables have fired. 143 */ setOnRenderListListener(OnRenderListListener onRenderListListener)144 public void setOnRenderListListener(OnRenderListListener onRenderListListener) { 145 Assert.isMainThread(); 146 147 mPipelineState.requireState(STATE_IDLE); 148 mOnRenderListListener = onRenderListListener; 149 } 150 addOnBeforeTransformGroupsListener(OnBeforeTransformGroupsListener listener)151 void addOnBeforeTransformGroupsListener(OnBeforeTransformGroupsListener listener) { 152 Assert.isMainThread(); 153 154 mPipelineState.requireState(STATE_IDLE); 155 mOnBeforeTransformGroupsListeners.add(listener); 156 } 157 addOnBeforeSortListener(OnBeforeSortListener listener)158 void addOnBeforeSortListener(OnBeforeSortListener listener) { 159 Assert.isMainThread(); 160 161 mPipelineState.requireState(STATE_IDLE); 162 mOnBeforeSortListeners.add(listener); 163 } 164 addOnBeforeFinalizeFilterListener(OnBeforeFinalizeFilterListener listener)165 void addOnBeforeFinalizeFilterListener(OnBeforeFinalizeFilterListener listener) { 166 Assert.isMainThread(); 167 168 mPipelineState.requireState(STATE_IDLE); 169 mOnBeforeFinalizeFilterListeners.add(listener); 170 } 171 addOnBeforeRenderListListener(OnBeforeRenderListListener listener)172 void addOnBeforeRenderListListener(OnBeforeRenderListListener listener) { 173 Assert.isMainThread(); 174 175 mPipelineState.requireState(STATE_IDLE); 176 mOnBeforeRenderListListeners.add(listener); 177 } 178 addPreRenderInvalidator(Invalidator invalidator)179 void addPreRenderInvalidator(Invalidator invalidator) { 180 Assert.isMainThread(); 181 182 mPipelineState.requireState(STATE_IDLE); 183 invalidator.setInvalidationListener(this::onPreRenderInvalidated); 184 } 185 addPreGroupFilter(NotifFilter filter)186 void addPreGroupFilter(NotifFilter filter) { 187 Assert.isMainThread(); 188 mPipelineState.requireState(STATE_IDLE); 189 190 mNotifPreGroupFilters.add(filter); 191 filter.setInvalidationListener(this::onPreGroupFilterInvalidated); 192 } 193 addFinalizeFilter(NotifFilter filter)194 void addFinalizeFilter(NotifFilter filter) { 195 Assert.isMainThread(); 196 mPipelineState.requireState(STATE_IDLE); 197 198 mNotifFinalizeFilters.add(filter); 199 filter.setInvalidationListener(this::onFinalizeFilterInvalidated); 200 } 201 addPromoter(NotifPromoter promoter)202 void addPromoter(NotifPromoter promoter) { 203 Assert.isMainThread(); 204 mPipelineState.requireState(STATE_IDLE); 205 206 mNotifPromoters.add(promoter); 207 promoter.setInvalidationListener(this::onPromoterInvalidated); 208 } 209 setSectioners(List<NotifSectioner> sectioners)210 void setSectioners(List<NotifSectioner> sectioners) { 211 Assert.isMainThread(); 212 mPipelineState.requireState(STATE_IDLE); 213 214 mNotifSections.clear(); 215 for (NotifSectioner sectioner : sectioners) { 216 mNotifSections.add(new NotifSection(sectioner, mNotifSections.size())); 217 sectioner.setInvalidationListener(this::onNotifSectionInvalidated); 218 } 219 220 mNotifSections.add(new NotifSection(DEFAULT_SECTIONER, mNotifSections.size())); 221 } 222 setNotifStabilityManager(NotifStabilityManager notifStabilityManager)223 void setNotifStabilityManager(NotifStabilityManager notifStabilityManager) { 224 Assert.isMainThread(); 225 mPipelineState.requireState(STATE_IDLE); 226 227 if (mNotifStabilityManager != null) { 228 throw new IllegalStateException( 229 "Attempting to set the NotifStabilityManager more than once. There should " 230 + "only be one visual stability manager. Manager is being set by " 231 + mNotifStabilityManager.getName() + " and " 232 + notifStabilityManager.getName()); 233 } 234 235 mNotifStabilityManager = notifStabilityManager; 236 mNotifStabilityManager.setInvalidationListener(this::onReorderingAllowedInvalidated); 237 } 238 setComparators(List<NotifComparator> comparators)239 void setComparators(List<NotifComparator> comparators) { 240 Assert.isMainThread(); 241 mPipelineState.requireState(STATE_IDLE); 242 243 mNotifComparators.clear(); 244 for (NotifComparator comparator : comparators) { 245 mNotifComparators.add(comparator); 246 comparator.setInvalidationListener(this::onNotifComparatorInvalidated); 247 } 248 } 249 getShadeList()250 List<ListEntry> getShadeList() { 251 Assert.isMainThread(); 252 return mReadOnlyNotifList; 253 } 254 255 private final CollectionReadyForBuildListener mReadyForBuildListener = 256 new CollectionReadyForBuildListener() { 257 @Override 258 public void onBuildList(Collection<NotificationEntry> entries) { 259 Assert.isMainThread(); 260 mPipelineState.requireIsBefore(STATE_BUILD_STARTED); 261 262 mLogger.logOnBuildList(); 263 mAllEntries = entries; 264 buildList(); 265 } 266 }; 267 onPreRenderInvalidated(Invalidator invalidator)268 private void onPreRenderInvalidated(Invalidator invalidator) { 269 Assert.isMainThread(); 270 271 mLogger.logPreRenderInvalidated(invalidator.getName(), mPipelineState.getState()); 272 273 rebuildListIfBefore(STATE_FINALIZING); 274 } 275 onPreGroupFilterInvalidated(NotifFilter filter)276 private void onPreGroupFilterInvalidated(NotifFilter filter) { 277 Assert.isMainThread(); 278 279 mLogger.logPreGroupFilterInvalidated(filter.getName(), mPipelineState.getState()); 280 281 rebuildListIfBefore(STATE_PRE_GROUP_FILTERING); 282 } 283 onReorderingAllowedInvalidated(NotifStabilityManager stabilityManager)284 private void onReorderingAllowedInvalidated(NotifStabilityManager stabilityManager) { 285 Assert.isMainThread(); 286 287 mLogger.logReorderingAllowedInvalidated( 288 stabilityManager.getName(), 289 mPipelineState.getState()); 290 291 rebuildListIfBefore(STATE_GROUPING); 292 } 293 onPromoterInvalidated(NotifPromoter promoter)294 private void onPromoterInvalidated(NotifPromoter promoter) { 295 Assert.isMainThread(); 296 297 mLogger.logPromoterInvalidated(promoter.getName(), mPipelineState.getState()); 298 299 rebuildListIfBefore(STATE_TRANSFORMING); 300 } 301 onNotifSectionInvalidated(NotifSectioner section)302 private void onNotifSectionInvalidated(NotifSectioner section) { 303 Assert.isMainThread(); 304 305 mLogger.logNotifSectionInvalidated(section.getName(), mPipelineState.getState()); 306 307 rebuildListIfBefore(STATE_SORTING); 308 } 309 onFinalizeFilterInvalidated(NotifFilter filter)310 private void onFinalizeFilterInvalidated(NotifFilter filter) { 311 Assert.isMainThread(); 312 313 mLogger.logFinalizeFilterInvalidated(filter.getName(), mPipelineState.getState()); 314 315 rebuildListIfBefore(STATE_FINALIZE_FILTERING); 316 } 317 onNotifComparatorInvalidated(NotifComparator comparator)318 private void onNotifComparatorInvalidated(NotifComparator comparator) { 319 Assert.isMainThread(); 320 321 mLogger.logNotifComparatorInvalidated(comparator.getName(), mPipelineState.getState()); 322 323 rebuildListIfBefore(STATE_SORTING); 324 } 325 326 /** 327 * The core algorithm of the pipeline. See the top comment in {@link NotifPipeline} for 328 * details on our contracts with other code. 329 * 330 * Once the build starts we are very careful to protect against reentrant code. Anything that 331 * tries to invalidate itself after the pipeline has passed it by will return in an exception. 332 * In general, we should be extremely sensitive to client code doing things in the wrong order; 333 * if we detect that behavior, we should crash instantly. 334 */ buildList()335 private void buildList() { 336 Trace.beginSection("ShadeListBuilder.buildList"); 337 mPipelineState.requireIsBefore(STATE_BUILD_STARTED); 338 mPipelineState.setState(STATE_BUILD_STARTED); 339 340 // Step 1: Reset notification states 341 mPipelineState.incrementTo(STATE_RESETTING); 342 resetNotifs(); 343 onBeginRun(); 344 345 // Step 2: Filter out any notifications that shouldn't be shown right now 346 mPipelineState.incrementTo(STATE_PRE_GROUP_FILTERING); 347 filterNotifs(mAllEntries, mNotifList, mNotifPreGroupFilters); 348 349 // Step 3: Group notifications with the same group key and set summaries 350 mPipelineState.incrementTo(STATE_GROUPING); 351 groupNotifs(mNotifList, mNewNotifList); 352 applyNewNotifList(); 353 pruneIncompleteGroups(mNotifList); 354 355 // Step 4: Group transforming 356 // Move some notifs out of their groups and up to top-level (mostly used for heads-upping) 357 dispatchOnBeforeTransformGroups(mReadOnlyNotifList); 358 mPipelineState.incrementTo(STATE_TRANSFORMING); 359 promoteNotifs(mNotifList); 360 pruneIncompleteGroups(mNotifList); 361 362 // Step 4.5: Reassign/revert any groups to maintain visual stability 363 mPipelineState.incrementTo(STATE_GROUP_STABILIZING); 364 stabilizeGroupingNotifs(mNotifList); 365 366 367 // Step 5: Filter out entries after pre-group filtering, grouping and promoting 368 // Now filters can see grouping information to determine whether to filter or not. 369 dispatchOnBeforeFinalizeFilter(mReadOnlyNotifList); 370 mPipelineState.incrementTo(STATE_FINALIZE_FILTERING); 371 filterNotifs(mNotifList, mNewNotifList, mNotifFinalizeFilters); 372 applyNewNotifList(); 373 pruneIncompleteGroups(mNotifList); 374 375 // Step 6: Sort 376 // Assign each top-level entry a section, then sort the list by section and then within 377 // section by our list of custom comparators 378 dispatchOnBeforeSort(mReadOnlyNotifList); 379 mPipelineState.incrementTo(STATE_SORTING); 380 sortListAndNotifySections(); 381 382 // Step 7: Lock in our group structure and log anything that's changed since the last run 383 mPipelineState.incrementTo(STATE_FINALIZING); 384 logChanges(); 385 freeEmptyGroups(); 386 cleanupPluggables(); 387 388 // Step 8: Dispatch the new list, first to any listeners and then to the view layer 389 dispatchOnBeforeRenderList(mReadOnlyNotifList); 390 Trace.beginSection("ShadeListBuilder.onRenderList"); 391 if (mOnRenderListListener != null) { 392 mOnRenderListListener.onRenderList(mReadOnlyNotifList); 393 } 394 Trace.endSection(); 395 396 // Step 9: We're done! 397 mLogger.logEndBuildList( 398 mIterationCount, 399 mReadOnlyNotifList.size(), 400 countChildren(mReadOnlyNotifList)); 401 if (mIterationCount % 10 == 0) { 402 mLogger.logFinalList(mNotifList); 403 } 404 mPipelineState.setState(STATE_IDLE); 405 mIterationCount++; 406 Trace.endSection(); 407 } 408 notifySectionEntriesUpdated()409 private void notifySectionEntriesUpdated() { 410 Trace.beginSection("ShadeListBuilder.notifySectionEntriesUpdated"); 411 NotifSection currentSection = null; 412 mTempSectionMembers.clear(); 413 for (int i = 0; i < mNotifList.size(); i++) { 414 ListEntry currentEntry = mNotifList.get(i); 415 if (currentSection != currentEntry.getSection()) { 416 if (currentSection != null) { 417 currentSection.getSectioner().onEntriesUpdated(mTempSectionMembers); 418 mTempSectionMembers.clear(); 419 } 420 currentSection = currentEntry.getSection(); 421 } 422 mTempSectionMembers.add(currentEntry); 423 } 424 Trace.endSection(); 425 } 426 427 /** 428 * Points mNotifList to the list stored in mNewNotifList. 429 * Reuses the (emptied) mNotifList as mNewNotifList. 430 * 431 * Accordingly, updates the ReadOnlyNotifList pointers. 432 */ applyNewNotifList()433 private void applyNewNotifList() { 434 mNotifList.clear(); 435 List<ListEntry> emptyList = mNotifList; 436 mNotifList = mNewNotifList; 437 mNewNotifList = emptyList; 438 439 List<ListEntry> readOnlyNotifList = mReadOnlyNotifList; 440 mReadOnlyNotifList = mReadOnlyNewNotifList; 441 mReadOnlyNewNotifList = readOnlyNotifList; 442 } 443 resetNotifs()444 private void resetNotifs() { 445 for (GroupEntry group : mGroups.values()) { 446 group.beginNewAttachState(); 447 group.clearChildren(); 448 group.setSummary(null); 449 } 450 451 for (NotificationEntry entry : mAllEntries) { 452 entry.beginNewAttachState(); 453 454 if (entry.mFirstAddedIteration == -1) { 455 entry.mFirstAddedIteration = mIterationCount; 456 } 457 } 458 459 mNotifList.clear(); 460 } 461 filterNotifs( Collection<? extends ListEntry> entries, List<ListEntry> out, List<NotifFilter> filters)462 private void filterNotifs( 463 Collection<? extends ListEntry> entries, 464 List<ListEntry> out, 465 List<NotifFilter> filters) { 466 Trace.beginSection("ShadeListBuilder.filterNotifs"); 467 final long now = mSystemClock.uptimeMillis(); 468 for (ListEntry entry : entries) { 469 if (entry instanceof GroupEntry) { 470 final GroupEntry groupEntry = (GroupEntry) entry; 471 472 // apply filter on its summary 473 final NotificationEntry summary = groupEntry.getRepresentativeEntry(); 474 if (applyFilters(summary, now, filters)) { 475 groupEntry.setSummary(null); 476 annulAddition(summary); 477 } 478 479 // apply filter on its children 480 final List<NotificationEntry> children = groupEntry.getRawChildren(); 481 for (int j = children.size() - 1; j >= 0; j--) { 482 final NotificationEntry child = children.get(j); 483 if (applyFilters(child, now, filters)) { 484 children.remove(child); 485 annulAddition(child); 486 } 487 } 488 489 out.add(groupEntry); 490 } else { 491 if (applyFilters((NotificationEntry) entry, now, filters)) { 492 annulAddition(entry); 493 } else { 494 out.add(entry); 495 } 496 } 497 } 498 Trace.endSection(); 499 } 500 groupNotifs(List<ListEntry> entries, List<ListEntry> out)501 private void groupNotifs(List<ListEntry> entries, List<ListEntry> out) { 502 Trace.beginSection("ShadeListBuilder.groupNotifs"); 503 for (ListEntry listEntry : entries) { 504 // since grouping hasn't happened yet, all notifs are NotificationEntries 505 NotificationEntry entry = (NotificationEntry) listEntry; 506 if (entry.getSbn().isGroup()) { 507 final String topLevelKey = entry.getSbn().getGroupKey(); 508 509 GroupEntry group = mGroups.get(topLevelKey); 510 if (group == null) { 511 group = new GroupEntry(topLevelKey, mSystemClock.uptimeMillis()); 512 group.mFirstAddedIteration = mIterationCount; 513 mGroups.put(topLevelKey, group); 514 } 515 if (group.getParent() == null) { 516 group.setParent(ROOT_ENTRY); 517 out.add(group); 518 } 519 520 entry.setParent(group); 521 522 if (entry.getSbn().getNotification().isGroupSummary()) { 523 final NotificationEntry existingSummary = group.getSummary(); 524 525 if (existingSummary == null) { 526 group.setSummary(entry); 527 } else { 528 mLogger.logDuplicateSummary( 529 mIterationCount, 530 group.getKey(), 531 existingSummary.getKey(), 532 entry.getKey()); 533 534 // Use whichever one was posted most recently 535 if (entry.getSbn().getPostTime() 536 > existingSummary.getSbn().getPostTime()) { 537 group.setSummary(entry); 538 annulAddition(existingSummary, out); 539 } else { 540 annulAddition(entry, out); 541 } 542 } 543 } else { 544 group.addChild(entry); 545 } 546 547 } else { 548 549 final String topLevelKey = entry.getKey(); 550 if (mGroups.containsKey(topLevelKey)) { 551 mLogger.logDuplicateTopLevelKey(mIterationCount, topLevelKey); 552 } else { 553 entry.setParent(ROOT_ENTRY); 554 out.add(entry); 555 } 556 } 557 } 558 Trace.endSection(); 559 } 560 stabilizeGroupingNotifs(List<ListEntry> topLevelList)561 private void stabilizeGroupingNotifs(List<ListEntry> topLevelList) { 562 if (mNotifStabilityManager == null) { 563 return; 564 } 565 Trace.beginSection("ShadeListBuilder.stabilizeGroupingNotifs"); 566 567 for (int i = 0; i < topLevelList.size(); i++) { 568 final ListEntry tle = topLevelList.get(i); 569 if (tle instanceof GroupEntry) { 570 // maybe put children back into their old group (including moving back to top-level) 571 GroupEntry groupEntry = (GroupEntry) tle; 572 List<NotificationEntry> children = groupEntry.getRawChildren(); 573 for (int j = 0; j < groupEntry.getChildren().size(); j++) { 574 if (maybeSuppressGroupChange(children.get(j), topLevelList)) { 575 // child was put back into its previous group, so we remove it from this 576 // group 577 children.remove(j); 578 j--; 579 } 580 } 581 } else { 582 // maybe put top-level-entries back into their previous groups 583 if (maybeSuppressGroupChange(tle.getRepresentativeEntry(), topLevelList)) { 584 // entry was put back into its previous group, so we remove it from the list of 585 // top-level-entries 586 topLevelList.remove(i); 587 i--; 588 } 589 } 590 } 591 Trace.endSection(); 592 } 593 594 /** 595 * Returns true if the group change was suppressed, else false 596 */ maybeSuppressGroupChange(NotificationEntry entry, List<ListEntry> out)597 private boolean maybeSuppressGroupChange(NotificationEntry entry, List<ListEntry> out) { 598 if (!entry.wasAttachedInPreviousPass()) { 599 return false; // new entries are allowed 600 } 601 602 final GroupEntry prevParent = entry.getPreviousAttachState().getParent(); 603 final GroupEntry assignedParent = entry.getParent(); 604 if (prevParent != assignedParent 605 && !mNotifStabilityManager.isGroupChangeAllowed(entry.getRepresentativeEntry())) { 606 entry.getAttachState().getSuppressedChanges().setParent(assignedParent); 607 entry.setParent(prevParent); 608 if (prevParent == ROOT_ENTRY) { 609 out.add(entry); 610 } else if (prevParent != null) { 611 prevParent.addChild(entry); 612 if (!mGroups.containsKey(prevParent.getKey())) { 613 mGroups.put(prevParent.getKey(), prevParent); 614 } 615 } 616 617 return true; 618 } 619 620 return false; 621 } 622 promoteNotifs(List<ListEntry> list)623 private void promoteNotifs(List<ListEntry> list) { 624 Trace.beginSection("ShadeListBuilder.promoteNotifs"); 625 for (int i = 0; i < list.size(); i++) { 626 final ListEntry tle = list.get(i); 627 628 if (tle instanceof GroupEntry) { 629 final GroupEntry group = (GroupEntry) tle; 630 631 group.getRawChildren().removeIf(child -> { 632 final boolean shouldPromote = applyTopLevelPromoters(child); 633 634 if (shouldPromote) { 635 child.setParent(ROOT_ENTRY); 636 list.add(child); 637 } 638 639 return shouldPromote; 640 }); 641 } 642 } 643 Trace.endSection(); 644 } 645 pruneIncompleteGroups(List<ListEntry> shadeList)646 private void pruneIncompleteGroups(List<ListEntry> shadeList) { 647 Trace.beginSection("ShadeListBuilder.pruneIncompleteGroups"); 648 for (int i = 0; i < shadeList.size(); i++) { 649 final ListEntry tle = shadeList.get(i); 650 651 if (tle instanceof GroupEntry) { 652 final GroupEntry group = (GroupEntry) tle; 653 final List<NotificationEntry> children = group.getRawChildren(); 654 655 if (group.getSummary() != null && children.size() == 0) { 656 shadeList.remove(i); 657 i--; 658 659 NotificationEntry summary = group.getSummary(); 660 summary.setParent(ROOT_ENTRY); 661 shadeList.add(summary); 662 663 group.setSummary(null); 664 annulAddition(group, shadeList); 665 666 } else if (group.getSummary() == null 667 || children.size() < MIN_CHILDREN_FOR_GROUP) { 668 669 if (group.getSummary() != null 670 && group.wasAttachedInPreviousPass() 671 && mNotifStabilityManager != null 672 && !mNotifStabilityManager.isGroupChangeAllowed(group.getSummary())) { 673 // if this group was previously attached and group changes aren't 674 // allowed, keep it around until group changes are allowed again 675 group.getAttachState().getSuppressedChanges().setWasPruneSuppressed(true); 676 continue; 677 } 678 679 // If the group doesn't provide a summary or is too small, ignore it and add 680 // its children (if any) directly to top-level. 681 682 shadeList.remove(i); 683 i--; 684 685 if (group.getSummary() != null) { 686 final NotificationEntry summary = group.getSummary(); 687 group.setSummary(null); 688 annulAddition(summary, shadeList); 689 } 690 691 for (int j = 0; j < children.size(); j++) { 692 final NotificationEntry child = children.get(j); 693 child.setParent(ROOT_ENTRY); 694 shadeList.add(child); 695 } 696 children.clear(); 697 698 annulAddition(group, shadeList); 699 } 700 } 701 } 702 Trace.endSection(); 703 } 704 705 /** 706 * If a ListEntry was added to the shade list and then later removed (e.g. because it was a 707 * group that was broken up), this method will erase any bookkeeping traces of that addition 708 * and/or check that they were already erased. 709 * 710 * Before calling this method, the entry must already have been removed from its parent. If 711 * it's a group, its summary must be null and its children must be empty. 712 */ annulAddition(ListEntry entry, List<ListEntry> shadeList)713 private void annulAddition(ListEntry entry, List<ListEntry> shadeList) { 714 715 // This function does very little, but if any of its assumptions are violated (and it has a 716 // lot of them), it will put the system into an inconsistent state. So we check all of them 717 // here. 718 719 if (entry.getParent() == null || entry.mFirstAddedIteration == -1) { 720 throw new IllegalStateException( 721 "Cannot nullify addition of " + entry.getKey() + ": no such addition. (" 722 + entry.getParent() + " " + entry.mFirstAddedIteration + ")"); 723 } 724 725 if (entry.getParent() == ROOT_ENTRY) { 726 if (shadeList.contains(entry)) { 727 throw new IllegalStateException("Cannot nullify addition of " + entry.getKey() 728 + ": it's still in the shade list."); 729 } 730 } 731 732 if (entry instanceof GroupEntry) { 733 GroupEntry ge = (GroupEntry) entry; 734 if (ge.getSummary() != null) { 735 throw new IllegalStateException( 736 "Cannot nullify group " + ge.getKey() + ": summary is not null"); 737 } 738 if (!ge.getChildren().isEmpty()) { 739 throw new IllegalStateException( 740 "Cannot nullify group " + ge.getKey() + ": still has children"); 741 } 742 } else if (entry instanceof NotificationEntry) { 743 if (entry == entry.getParent().getSummary() 744 || entry.getParent().getChildren().contains(entry)) { 745 throw new IllegalStateException("Cannot nullify addition of child " 746 + entry.getKey() + ": it's still attached to its parent."); 747 } 748 } 749 750 annulAddition(entry); 751 752 } 753 754 /** 755 * Erases bookkeeping traces stored on an entry when it is removed from the notif list. 756 * This can happen if the entry is removed from a group that was broken up or if the entry was 757 * filtered out during any of the filtering steps. 758 */ annulAddition(ListEntry entry)759 private void annulAddition(ListEntry entry) { 760 entry.setParent(null); 761 entry.getAttachState().setSection(null); 762 entry.getAttachState().setPromoter(null); 763 if (entry.mFirstAddedIteration == mIterationCount) { 764 entry.mFirstAddedIteration = -1; 765 } 766 } 767 sortListAndNotifySections()768 private void sortListAndNotifySections() { 769 Trace.beginSection("ShadeListBuilder.sortListAndNotifySections"); 770 // Assign sections to top-level elements and sort their children 771 for (ListEntry entry : mNotifList) { 772 NotifSection section = applySections(entry); 773 if (entry instanceof GroupEntry) { 774 GroupEntry parent = (GroupEntry) entry; 775 for (NotificationEntry child : parent.getChildren()) { 776 setEntrySection(child, section); 777 } 778 parent.sortChildren(sChildComparator); 779 } 780 } 781 782 // Finally, sort all top-level elements 783 mNotifList.sort(mTopLevelComparator); 784 785 // notify sections since the list is sorted now 786 notifySectionEntriesUpdated(); 787 Trace.endSection(); 788 } 789 freeEmptyGroups()790 private void freeEmptyGroups() { 791 mGroups.values().removeIf(ge -> ge.getSummary() == null && ge.getChildren().isEmpty()); 792 } 793 logChanges()794 private void logChanges() { 795 for (NotificationEntry entry : mAllEntries) { 796 logAttachStateChanges(entry); 797 } 798 for (GroupEntry group : mGroups.values()) { 799 logAttachStateChanges(group); 800 } 801 } 802 logAttachStateChanges(ListEntry entry)803 private void logAttachStateChanges(ListEntry entry) { 804 805 final ListAttachState curr = entry.getAttachState(); 806 final ListAttachState prev = entry.getPreviousAttachState(); 807 808 if (!Objects.equals(curr, prev)) { 809 mLogger.logEntryAttachStateChanged( 810 mIterationCount, 811 entry.getKey(), 812 prev.getParent(), 813 curr.getParent()); 814 815 if (curr.getParent() != prev.getParent()) { 816 mLogger.logParentChanged(mIterationCount, prev.getParent(), curr.getParent()); 817 } 818 819 if (curr.getSuppressedChanges().getParent() != null) { 820 mLogger.logParentChangeSuppressed( 821 mIterationCount, 822 curr.getSuppressedChanges().getParent(), 823 curr.getParent()); 824 } 825 826 if (curr.getSuppressedChanges().getWasPruneSuppressed()) { 827 mLogger.logGroupPruningSuppressed( 828 mIterationCount, 829 curr.getParent()); 830 } 831 832 if (curr.getExcludingFilter() != prev.getExcludingFilter()) { 833 mLogger.logFilterChanged( 834 mIterationCount, 835 prev.getExcludingFilter(), 836 curr.getExcludingFilter()); 837 } 838 839 // When something gets detached, its promoter and section are always set to null, so 840 // don't bother logging those changes. 841 final boolean wasDetached = curr.getParent() == null && prev.getParent() != null; 842 843 if (!wasDetached && curr.getPromoter() != prev.getPromoter()) { 844 mLogger.logPromoterChanged( 845 mIterationCount, 846 prev.getPromoter(), 847 curr.getPromoter()); 848 } 849 850 if (!wasDetached && curr.getSection() != prev.getSection()) { 851 mLogger.logSectionChanged( 852 mIterationCount, 853 prev.getSection(), 854 curr.getSection()); 855 } 856 857 if (curr.getSuppressedChanges().getSection() != null) { 858 mLogger.logSectionChangeSuppressed( 859 mIterationCount, 860 curr.getSuppressedChanges().getSection(), 861 curr.getSection()); 862 } 863 } 864 } 865 onBeginRun()866 private void onBeginRun() { 867 if (mNotifStabilityManager != null) { 868 mNotifStabilityManager.onBeginRun(); 869 } 870 } 871 cleanupPluggables()872 private void cleanupPluggables() { 873 callOnCleanup(mNotifPreGroupFilters); 874 callOnCleanup(mNotifPromoters); 875 callOnCleanup(mNotifFinalizeFilters); 876 callOnCleanup(mNotifComparators); 877 878 for (int i = 0; i < mNotifSections.size(); i++) { 879 mNotifSections.get(i).getSectioner().onCleanup(); 880 } 881 882 if (mNotifStabilityManager != null) { 883 callOnCleanup(List.of(mNotifStabilityManager)); 884 } 885 } 886 callOnCleanup(List<? extends Pluggable<?>> pluggables)887 private void callOnCleanup(List<? extends Pluggable<?>> pluggables) { 888 for (int i = 0; i < pluggables.size(); i++) { 889 pluggables.get(i).onCleanup(); 890 } 891 } 892 893 private final Comparator<ListEntry> mTopLevelComparator = (o1, o2) -> { 894 895 int cmp = Integer.compare( 896 o1.getSectionIndex(), 897 o2.getSectionIndex()); 898 899 if (cmp == 0) { 900 for (int i = 0; i < mNotifComparators.size(); i++) { 901 cmp = mNotifComparators.get(i).compare(o1, o2); 902 if (cmp != 0) { 903 break; 904 } 905 } 906 } 907 908 final NotificationEntry rep1 = o1.getRepresentativeEntry(); 909 final NotificationEntry rep2 = o2.getRepresentativeEntry(); 910 911 if (cmp == 0) { 912 cmp = rep1.getRanking().getRank() - rep2.getRanking().getRank(); 913 } 914 915 if (cmp == 0) { 916 cmp = Long.compare( 917 rep2.getSbn().getNotification().when, 918 rep1.getSbn().getNotification().when); 919 } 920 921 return cmp; 922 }; 923 924 private static final Comparator<NotificationEntry> sChildComparator = (o1, o2) -> { 925 int cmp = o1.getRanking().getRank() - o2.getRanking().getRank(); 926 927 if (cmp == 0) { 928 cmp = Long.compare( 929 o2.getSbn().getNotification().when, 930 o1.getSbn().getNotification().when); 931 } 932 933 return cmp; 934 }; 935 applyFilters(NotificationEntry entry, long now, List<NotifFilter> filters)936 private boolean applyFilters(NotificationEntry entry, long now, List<NotifFilter> filters) { 937 final NotifFilter filter = findRejectingFilter(entry, now, filters); 938 entry.getAttachState().setExcludingFilter(filter); 939 if (filter != null) { 940 // notification is removed from the list, so we reset its initialization time 941 entry.resetInitializationTime(); 942 } 943 return filter != null; 944 } 945 findRejectingFilter(NotificationEntry entry, long now, List<NotifFilter> filters)946 @Nullable private static NotifFilter findRejectingFilter(NotificationEntry entry, long now, 947 List<NotifFilter> filters) { 948 final int size = filters.size(); 949 950 for (int i = 0; i < size; i++) { 951 NotifFilter filter = filters.get(i); 952 if (filter.shouldFilterOut(entry, now)) { 953 return filter; 954 } 955 } 956 return null; 957 } 958 applyTopLevelPromoters(NotificationEntry entry)959 private boolean applyTopLevelPromoters(NotificationEntry entry) { 960 NotifPromoter promoter = findPromoter(entry); 961 entry.getAttachState().setPromoter(promoter); 962 return promoter != null; 963 } 964 findPromoter(NotificationEntry entry)965 @Nullable private NotifPromoter findPromoter(NotificationEntry entry) { 966 for (int i = 0; i < mNotifPromoters.size(); i++) { 967 NotifPromoter promoter = mNotifPromoters.get(i); 968 if (promoter.shouldPromoteToTopLevel(entry)) { 969 return promoter; 970 } 971 } 972 return null; 973 } 974 applySections(ListEntry entry)975 private NotifSection applySections(ListEntry entry) { 976 final NotifSection newSection = findSection(entry); 977 final ListAttachState prevAttachState = entry.getPreviousAttachState(); 978 979 NotifSection finalSection = newSection; 980 981 // have we seen this entry before and are we changing its section? 982 if (mNotifStabilityManager != null 983 && entry.wasAttachedInPreviousPass() 984 && newSection != prevAttachState.getSection()) { 985 986 // are section changes allowed? 987 if (!mNotifStabilityManager.isSectionChangeAllowed(entry.getRepresentativeEntry())) { 988 // record the section that we wanted to change to 989 entry.getAttachState().getSuppressedChanges().setSection(newSection); 990 991 // keep the previous section 992 finalSection = prevAttachState.getSection(); 993 } 994 } 995 996 setEntrySection(entry, finalSection); 997 return finalSection; 998 } 999 setEntrySection(ListEntry entry, NotifSection finalSection)1000 private void setEntrySection(ListEntry entry, NotifSection finalSection) { 1001 entry.getAttachState().setSection(finalSection); 1002 NotificationEntry representativeEntry = entry.getRepresentativeEntry(); 1003 if (representativeEntry != null && finalSection != null) { 1004 representativeEntry.setBucket(finalSection.getBucket()); 1005 } 1006 } 1007 1008 @NonNull findSection(ListEntry entry)1009 private NotifSection findSection(ListEntry entry) { 1010 for (int i = 0; i < mNotifSections.size(); i++) { 1011 NotifSection section = mNotifSections.get(i); 1012 if (section.getSectioner().isInSection(entry)) { 1013 return section; 1014 } 1015 } 1016 throw new RuntimeException("Missing default sectioner!"); 1017 } 1018 rebuildListIfBefore(@ipelineState.StateName int state)1019 private void rebuildListIfBefore(@PipelineState.StateName int state) { 1020 mPipelineState.requireIsBefore(state); 1021 if (mPipelineState.is(STATE_IDLE)) { 1022 buildList(); 1023 } 1024 } 1025 countChildren(List<ListEntry> entries)1026 private static int countChildren(List<ListEntry> entries) { 1027 int count = 0; 1028 for (int i = 0; i < entries.size(); i++) { 1029 final ListEntry entry = entries.get(i); 1030 if (entry instanceof GroupEntry) { 1031 count += ((GroupEntry) entry).getChildren().size(); 1032 } 1033 } 1034 return count; 1035 } 1036 dispatchOnBeforeTransformGroups(List<ListEntry> entries)1037 private void dispatchOnBeforeTransformGroups(List<ListEntry> entries) { 1038 Trace.beginSection("ShadeListBuilder.dispatchOnBeforeTransformGroups"); 1039 for (int i = 0; i < mOnBeforeTransformGroupsListeners.size(); i++) { 1040 mOnBeforeTransformGroupsListeners.get(i).onBeforeTransformGroups(entries); 1041 } 1042 Trace.endSection(); 1043 } 1044 dispatchOnBeforeSort(List<ListEntry> entries)1045 private void dispatchOnBeforeSort(List<ListEntry> entries) { 1046 Trace.beginSection("ShadeListBuilder.dispatchOnBeforeSort"); 1047 for (int i = 0; i < mOnBeforeSortListeners.size(); i++) { 1048 mOnBeforeSortListeners.get(i).onBeforeSort(entries); 1049 } 1050 Trace.endSection(); 1051 } 1052 dispatchOnBeforeFinalizeFilter(List<ListEntry> entries)1053 private void dispatchOnBeforeFinalizeFilter(List<ListEntry> entries) { 1054 Trace.beginSection("ShadeListBuilder.dispatchOnBeforeFinalizeFilter"); 1055 for (int i = 0; i < mOnBeforeFinalizeFilterListeners.size(); i++) { 1056 mOnBeforeFinalizeFilterListeners.get(i).onBeforeFinalizeFilter(entries); 1057 } 1058 Trace.endSection(); 1059 } 1060 dispatchOnBeforeRenderList(List<ListEntry> entries)1061 private void dispatchOnBeforeRenderList(List<ListEntry> entries) { 1062 Trace.beginSection("ShadeListBuilder.dispatchOnBeforeRenderList"); 1063 for (int i = 0; i < mOnBeforeRenderListListeners.size(); i++) { 1064 mOnBeforeRenderListListeners.get(i).onBeforeRenderList(entries); 1065 } 1066 Trace.endSection(); 1067 } 1068 1069 @Override dump(@onNull FileDescriptor fd, PrintWriter pw, @NonNull String[] args)1070 public void dump(@NonNull FileDescriptor fd, PrintWriter pw, @NonNull String[] args) { 1071 pw.println("\t" + TAG + " shade notifications:"); 1072 if (getShadeList().size() == 0) { 1073 pw.println("\t\t None"); 1074 } 1075 1076 pw.println(ListDumper.dumpTree( 1077 getShadeList(), 1078 mInteractionTracker, 1079 true, 1080 "\t\t")); 1081 } 1082 1083 /** See {@link #setOnRenderListListener(OnRenderListListener)} */ 1084 public interface OnRenderListListener { 1085 /** 1086 * Called with the final filtered, grouped, and sorted list. 1087 * 1088 * @param entries A read-only view into the current notif list. Note that this list is 1089 * backed by the live list and will change in response to new pipeline runs. 1090 */ onRenderList(@onNull List<ListEntry> entries)1091 void onRenderList(@NonNull List<ListEntry> entries); 1092 } 1093 1094 private static final NotifSectioner DEFAULT_SECTIONER = new NotifSectioner("UnknownSection", 1095 NotificationPriorityBucketKt.BUCKET_UNKNOWN) { 1096 @Override 1097 public boolean isInSection(ListEntry entry) { 1098 return true; 1099 } 1100 }; 1101 1102 private static final int MIN_CHILDREN_FOR_GROUP = 2; 1103 1104 private static final String TAG = "ShadeListBuilder"; 1105 } 1106