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