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