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