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