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.dump.LogBufferHelperKt.logcatLogBuffer;
20 import static com.android.systemui.statusbar.notification.collection.ListDumper.dumpTree;
21 import static com.android.systemui.statusbar.notification.collection.ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS;
22 
23 import static com.google.common.truth.Truth.assertThat;
24 
25 import static org.junit.Assert.assertEquals;
26 import static org.junit.Assert.assertFalse;
27 import static org.junit.Assert.assertNotNull;
28 import static org.junit.Assert.assertNull;
29 import static org.junit.Assert.assertTrue;
30 import static org.mockito.ArgumentMatchers.any;
31 import static org.mockito.ArgumentMatchers.anyList;
32 import static org.mockito.ArgumentMatchers.anyLong;
33 import static org.mockito.ArgumentMatchers.eq;
34 import static org.mockito.Mockito.atLeast;
35 import static org.mockito.Mockito.atLeastOnce;
36 import static org.mockito.Mockito.clearInvocations;
37 import static org.mockito.Mockito.inOrder;
38 import static org.mockito.Mockito.mock;
39 import static org.mockito.Mockito.never;
40 import static org.mockito.Mockito.spy;
41 import static org.mockito.Mockito.times;
42 import static org.mockito.Mockito.verify;
43 
44 import static java.util.Arrays.asList;
45 import static java.util.Collections.singletonList;
46 
47 import android.os.SystemClock;
48 import android.testing.AndroidTestingRunner;
49 import android.testing.TestableLooper;
50 import android.util.ArrayMap;
51 import android.util.Log;
52 
53 import androidx.annotation.NonNull;
54 import androidx.annotation.Nullable;
55 import androidx.test.filters.SmallTest;
56 
57 import com.android.systemui.SysuiTestCase;
58 import com.android.systemui.dump.DumpManager;
59 import com.android.systemui.statusbar.NotificationInteractionTracker;
60 import com.android.systemui.statusbar.RankingBuilder;
61 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
62 import com.android.systemui.statusbar.notification.collection.ShadeListBuilder.OnRenderListListener;
63 import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection;
64 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener;
65 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener;
66 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener;
67 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener;
68 import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderLogger;
69 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator;
70 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator;
71 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
72 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter;
73 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
74 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager;
75 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable;
76 import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener;
77 import com.android.systemui.util.time.FakeSystemClock;
78 
79 import org.junit.Before;
80 import org.junit.Test;
81 import org.junit.runner.RunWith;
82 import org.mockito.ArgumentCaptor;
83 import org.mockito.Captor;
84 import org.mockito.InOrder;
85 import org.mockito.Mock;
86 import org.mockito.Mockito;
87 import org.mockito.MockitoAnnotations;
88 import org.mockito.Spy;
89 
90 import java.util.ArrayList;
91 import java.util.Arrays;
92 import java.util.Collections;
93 import java.util.Comparator;
94 import java.util.List;
95 import java.util.Map;
96 import java.util.Objects;
97 import java.util.stream.Collectors;
98 
99 @SmallTest
100 @RunWith(AndroidTestingRunner.class)
101 @TestableLooper.RunWithLooper
102 public class ShadeListBuilderTest extends SysuiTestCase {
103 
104     private ShadeListBuilder mListBuilder;
105     private final FakeSystemClock mSystemClock = new FakeSystemClock();
106     private final NotifPipelineFlags mNotifPipelineFlags = mock(NotifPipelineFlags.class);
107     private final ShadeListBuilderLogger mLogger = new ShadeListBuilderLogger(
108             mNotifPipelineFlags, logcatLogBuffer());
109     @Mock private DumpManager mDumpManager;
110     @Mock private NotifCollection mNotifCollection;
111     @Mock private NotificationInteractionTracker mInteractionTracker;
112     @Spy private OnBeforeTransformGroupsListener mOnBeforeTransformGroupsListener;
113     @Spy private OnBeforeSortListener mOnBeforeSortListener;
114     @Spy private OnBeforeFinalizeFilterListener mOnBeforeFinalizeFilterListener;
115     @Spy private OnBeforeRenderListListener mOnBeforeRenderListListener;
116     @Spy private OnRenderListListener mOnRenderListListener = list -> mBuiltList = list;
117 
118     @Captor private ArgumentCaptor<CollectionReadyForBuildListener> mBuildListenerCaptor;
119 
120     private final FakeNotifPipelineChoreographer mPipelineChoreographer =
121             new FakeNotifPipelineChoreographer();
122     private CollectionReadyForBuildListener mReadyForBuildListener;
123     private List<NotificationEntryBuilder> mPendingSet = new ArrayList<>();
124     private List<NotificationEntry> mEntrySet = new ArrayList<>();
125     private List<ListEntry> mBuiltList = new ArrayList<>();
126     private TestableStabilityManager mStabilityManager;
127     private TestableNotifFilter mFinalizeFilter;
128 
129     private Map<String, Integer> mNextIdMap = new ArrayMap<>();
130     private int mNextRank = 0;
131 
132     private Log.TerribleFailureHandler mOldWtfHandler = null;
133     private Log.TerribleFailure mLastWtf = null;
134     private int mWtfCount = 0;
135 
136     @Before
setUp()137     public void setUp() {
138         MockitoAnnotations.initMocks(this);
139         allowTestableLooperAsMainThread();
140 
141         mListBuilder = new ShadeListBuilder(
142                 mDumpManager,
143                 mPipelineChoreographer,
144                 mNotifPipelineFlags,
145                 mInteractionTracker,
146                 mLogger,
147                 mSystemClock
148         );
149         mListBuilder.setOnRenderListListener(mOnRenderListListener);
150 
151         mListBuilder.attach(mNotifCollection);
152 
153         mStabilityManager = spy(new TestableStabilityManager());
154         mListBuilder.setNotifStabilityManager(mStabilityManager);
155         mFinalizeFilter = spy(new TestableNotifFilter());
156         mListBuilder.addFinalizeFilter(mFinalizeFilter);
157 
158         Mockito.verify(mNotifCollection).setBuildListener(mBuildListenerCaptor.capture());
159         mReadyForBuildListener = Objects.requireNonNull(mBuildListenerCaptor.getValue());
160     }
161 
162     @Test
testNotifsAreSortedByRankAndWhen()163     public void testNotifsAreSortedByRankAndWhen() {
164         // GIVEN a simple pipeline
165 
166         // WHEN a series of notifs with jumbled ranks are added
167         addNotif(0, PACKAGE_1).setRank(2);
168         addNotif(1, PACKAGE_2).setRank(4).modifyNotification(mContext).setWhen(22);
169         addNotif(2, PACKAGE_3).setRank(4).modifyNotification(mContext).setWhen(33);
170         addNotif(3, PACKAGE_3).setRank(3);
171         addNotif(4, PACKAGE_5).setRank(4).modifyNotification(mContext).setWhen(11);
172         addNotif(5, PACKAGE_3).setRank(1);
173         addNotif(6, PACKAGE_1).setRank(0);
174         dispatchBuild();
175 
176         // The final output is sorted based first by rank and then by when
177         verifyBuiltList(
178                 notif(6),
179                 notif(5),
180                 notif(0),
181                 notif(3),
182                 notif(2),
183                 notif(1),
184                 notif(4)
185         );
186     }
187 
188     @Test
testNotifsAreGrouped()189     public void testNotifsAreGrouped() {
190         // GIVEN a simple pipeline
191 
192         // WHEN a group is added
193         addGroupChild(0, PACKAGE_1, GROUP_1);
194         addGroupChild(1, PACKAGE_1, GROUP_1);
195         addGroupChild(2, PACKAGE_1, GROUP_1);
196         addGroupSummary(3, PACKAGE_1, GROUP_1);
197         dispatchBuild();
198 
199         // THEN the notifs are grouped together
200         verifyBuiltList(
201                 group(
202                         summary(3),
203                         child(0),
204                         child(1),
205                         child(2)
206                 )
207         );
208     }
209 
210     @Test
testNotifsWithDifferentGroupKeysAreGrouped()211     public void testNotifsWithDifferentGroupKeysAreGrouped() {
212         // GIVEN a simple pipeline
213 
214         // WHEN a package posts two different groups
215         addGroupChild(0, PACKAGE_1, GROUP_1);
216         addGroupChild(1, PACKAGE_1, GROUP_2);
217         addGroupSummary(2, PACKAGE_1, GROUP_2);
218         addGroupChild(3, PACKAGE_1, GROUP_2);
219         addGroupChild(4, PACKAGE_1, GROUP_1);
220         addGroupChild(5, PACKAGE_1, GROUP_2);
221         addGroupChild(6, PACKAGE_1, GROUP_1);
222         addGroupSummary(7, PACKAGE_1, GROUP_1);
223         dispatchBuild();
224 
225         // THEN the groups are separated separately
226         verifyBuiltList(
227                 group(
228                         summary(2),
229                         child(1),
230                         child(3),
231                         child(5)
232                 ),
233                 group(
234                         summary(7),
235                         child(0),
236                         child(4),
237                         child(6)
238                 )
239         );
240     }
241 
242     @Test
testNotifsNotifChildrenAreSorted()243     public void testNotifsNotifChildrenAreSorted() {
244         // GIVEN a simple pipeline
245 
246         // WHEN a group is added
247         addGroupChild(0, PACKAGE_1, GROUP_1).setRank(4);
248         addGroupChild(1, PACKAGE_1, GROUP_1).setRank(2)
249                 .modifyNotification(mContext).setWhen(11);
250         addGroupChild(2, PACKAGE_1, GROUP_1).setRank(1);
251         addGroupChild(3, PACKAGE_1, GROUP_1).setRank(2)
252                 .modifyNotification(mContext).setWhen(33);
253         addGroupChild(4, PACKAGE_1, GROUP_1).setRank(2)
254                 .modifyNotification(mContext).setWhen(22);
255         addGroupChild(5, PACKAGE_1, GROUP_1).setRank(0);
256         addGroupSummary(6, PACKAGE_1, GROUP_1).setRank(3);
257         dispatchBuild();
258 
259         // THEN the children are sorted by rank and when
260         verifyBuiltList(
261                 group(
262                         summary(6),
263                         child(5),
264                         child(2),
265                         child(3),
266                         child(4),
267                         child(1),
268                         child(0)
269                 )
270         );
271     }
272 
273     @Test
testDuplicateGroupSummariesAreDiscarded()274     public void testDuplicateGroupSummariesAreDiscarded() {
275         // GIVEN a simple pipeline
276 
277         // WHEN a group with multiple summaries is added
278         addNotif(0, PACKAGE_3);
279         addGroupChild(1, PACKAGE_1, GROUP_1);
280         addGroupChild(2, PACKAGE_1, GROUP_1);
281         addGroupSummary(3, PACKAGE_1, GROUP_1).setPostTime(22);
282         addGroupSummary(4, PACKAGE_1, GROUP_1).setPostTime(33);
283         addNotif(5, PACKAGE_2);
284         addGroupSummary(6, PACKAGE_1, GROUP_1).setPostTime(11);
285         addGroupChild(7, PACKAGE_1, GROUP_1);
286         dispatchBuild();
287 
288         // THEN only most recent summary is used
289         verifyBuiltList(
290                 notif(0),
291                 group(
292                         summary(4),
293                         child(1),
294                         child(2),
295                         child(7)
296                 ),
297                 notif(5)
298         );
299 
300         // THEN the extra summaries have their parents set to null
301         assertNull(mEntrySet.get(3).getParent());
302         assertNull(mEntrySet.get(6).getParent());
303     }
304 
305     @Test
testGroupsWithNoSummaryAreUngrouped()306     public void testGroupsWithNoSummaryAreUngrouped() {
307         // GIVEN a group with no summary
308         addNotif(0, PACKAGE_2);
309         addGroupChild(1, PACKAGE_4, GROUP_2);
310         addGroupChild(2, PACKAGE_4, GROUP_2);
311         addGroupChild(3, PACKAGE_4, GROUP_2);
312         addGroupChild(4, PACKAGE_4, GROUP_2);
313 
314         // WHEN we build the list
315         dispatchBuild();
316 
317         // THEN the children aren't grouped
318         verifyBuiltList(
319                 notif(0),
320                 notif(1),
321                 notif(2),
322                 notif(3),
323                 notif(4)
324         );
325     }
326 
327     @Test
testGroupsWithNoChildrenAreUngrouped()328     public void testGroupsWithNoChildrenAreUngrouped() {
329         // GIVEN a group with a summary but no children
330         addGroupSummary(0, PACKAGE_5, GROUP_1);
331         addNotif(1, PACKAGE_5);
332         addNotif(2, PACKAGE_1);
333 
334         // WHEN we build the list
335         dispatchBuild();
336 
337         // THEN the summary isn't grouped but is still added to the final list
338         verifyBuiltList(
339                 notif(0),
340                 notif(1),
341                 notif(2)
342         );
343     }
344 
345     @Test
testGroupsWithTooFewChildrenAreSplitUp()346     public void testGroupsWithTooFewChildrenAreSplitUp() {
347         // GIVEN a group with one child
348         addGroupChild(0, PACKAGE_2, GROUP_1);
349         addGroupSummary(1, PACKAGE_2, GROUP_1);
350 
351         // WHEN we build the list
352         dispatchBuild();
353 
354         // THEN the child is added at top level and the summary is discarded
355         verifyBuiltList(
356                 notif(0)
357         );
358 
359         assertNull(mEntrySet.get(1).getParent());
360     }
361 
362     @Test
testGroupsWhoLoseChildrenMidPipelineAreSplitUp()363     public void testGroupsWhoLoseChildrenMidPipelineAreSplitUp() {
364         // GIVEN a group with two children
365         addGroupChild(0, PACKAGE_2, GROUP_1);
366         addGroupSummary(1, PACKAGE_2, GROUP_1);
367         addGroupChild(2, PACKAGE_2, GROUP_1);
368 
369         // GIVEN a promoter that will promote one of children to top level
370         mListBuilder.addPromoter(new IdPromoter(0));
371 
372         // WHEN we build the list
373         dispatchBuild();
374 
375         // THEN both children end up at top level (because group is now too small)
376         verifyBuiltList(
377                 notif(0),
378                 notif(2)
379         );
380 
381         // THEN the summary is discarded
382         assertNull(mEntrySet.get(1).getParent());
383     }
384 
385     @Test
testGroupsWhoLoseAllChildrenToPromotionSuppressSummary()386     public void testGroupsWhoLoseAllChildrenToPromotionSuppressSummary() {
387         // GIVEN a group with two children
388         addGroupChild(0, PACKAGE_2, GROUP_1);
389         addGroupSummary(1, PACKAGE_2, GROUP_1);
390         addGroupChild(2, PACKAGE_2, GROUP_1);
391 
392         // GIVEN a promoter that will promote one of children to top level
393         mListBuilder.addPromoter(new IdPromoter(0, 2));
394 
395         // WHEN we build the list
396         dispatchBuild();
397 
398         // THEN both children end up at top level (because group is now too small)
399         verifyBuiltList(
400                 notif(0),
401                 notif(2)
402         );
403 
404         // THEN the summary is discarded
405         assertNull(mEntrySet.get(1).getParent());
406     }
407 
408     @Test
testGroupsWhoLoseOnlyChildToPromotionSuppressSummary()409     public void testGroupsWhoLoseOnlyChildToPromotionSuppressSummary() {
410         // GIVEN a group with two children
411         addGroupChild(0, PACKAGE_2, GROUP_1);
412         addGroupSummary(1, PACKAGE_2, GROUP_1);
413 
414         // GIVEN a promoter that will promote one of children to top level
415         mListBuilder.addPromoter(new IdPromoter(0));
416 
417         // WHEN we build the list
418         dispatchBuild();
419 
420         // THEN both children end up at top level (because group is now too small)
421         verifyBuiltList(
422                 notif(0)
423         );
424 
425         // THEN the summary is discarded
426         assertNull(mEntrySet.get(1).getParent());
427     }
428 
429     @Test
testPreviousParentsAreSetProperly()430     public void testPreviousParentsAreSetProperly() {
431         // GIVEN a notification that is initially added to the list
432         PackageFilter filter = new PackageFilter(PACKAGE_2);
433         filter.setEnabled(false);
434         mListBuilder.addPreGroupFilter(filter);
435 
436         addNotif(0, PACKAGE_1);
437         addNotif(1, PACKAGE_2);
438         addNotif(2, PACKAGE_3);
439         dispatchBuild();
440 
441         // WHEN it is suddenly filtered out
442         filter.setEnabled(true);
443         dispatchBuild();
444 
445         // THEN its previous parent indicates that it used to be added
446         assertNull(mEntrySet.get(1).getParent());
447         assertEquals(GroupEntry.ROOT_ENTRY, mEntrySet.get(1).getPreviousParent());
448     }
449 
450     @Test
testThatAnnulledGroupsAndSummariesAreProperlyRolledBack()451     public void testThatAnnulledGroupsAndSummariesAreProperlyRolledBack() {
452         // GIVEN a registered transform groups listener
453         RecordingOnBeforeTransformGroupsListener listener =
454                 new RecordingOnBeforeTransformGroupsListener();
455         mListBuilder.addOnBeforeTransformGroupsListener(listener);
456 
457         // GIVEN a malformed group that will be dismantled
458         addGroupChild(0, PACKAGE_2, GROUP_1);
459         addGroupSummary(1, PACKAGE_2, GROUP_1);
460         addNotif(2, PACKAGE_1);
461 
462         // WHEN we build the list
463         dispatchBuild();
464 
465         // THEN only the child appears in the final list
466         verifyBuiltList(
467                 notif(0),
468                 notif(2)
469         );
470 
471         // THEN the summary has a null parent and an unset firstAddedIteration
472         assertNull(mEntrySet.get(1).getParent());
473     }
474 
475     @Test
testPreGroupNotifsAreFiltered()476     public void testPreGroupNotifsAreFiltered() {
477         // GIVEN a PreGroupNotifFilter and PreRenderFilter that filters out the same package
478         NotifFilter preGroupFilter = spy(new PackageFilter(PACKAGE_2));
479         NotifFilter preRenderFilter = spy(new PackageFilter(PACKAGE_2));
480         mListBuilder.addPreGroupFilter(preGroupFilter);
481         mListBuilder.addFinalizeFilter(preRenderFilter);
482 
483         // WHEN the pipeline is kicked off on a list of notifs
484         addNotif(0, PACKAGE_1);
485         addNotif(1, PACKAGE_2);
486         addNotif(2, PACKAGE_3);
487         addNotif(3, PACKAGE_2);
488         dispatchBuild();
489 
490         // THEN the preGroupFilter is called on each notif in the original set
491         verify(preGroupFilter).shouldFilterOut(eq(mEntrySet.get(0)), anyLong());
492         verify(preGroupFilter).shouldFilterOut(eq(mEntrySet.get(1)), anyLong());
493         verify(preGroupFilter).shouldFilterOut(eq(mEntrySet.get(2)), anyLong());
494         verify(preGroupFilter).shouldFilterOut(eq(mEntrySet.get(3)), anyLong());
495 
496         // THEN the preRenderFilter is only called on the notifications not already filtered out
497         verify(preRenderFilter).shouldFilterOut(eq(mEntrySet.get(0)), anyLong());
498         verify(preRenderFilter, never()).shouldFilterOut(eq(mEntrySet.get(1)), anyLong());
499         verify(preRenderFilter).shouldFilterOut(eq(mEntrySet.get(2)), anyLong());
500         verify(preRenderFilter, never()).shouldFilterOut(eq(mEntrySet.get(3)), anyLong());
501 
502         // THEN the final list doesn't contain any filtered-out notifs
503         verifyBuiltList(
504                 notif(0),
505                 notif(2)
506         );
507 
508         // THEN each filtered notif records the NotifFilter that did it
509         assertEquals(preGroupFilter, mEntrySet.get(1).getExcludingFilter());
510         assertEquals(preGroupFilter, mEntrySet.get(3).getExcludingFilter());
511     }
512 
513     @Test
testPreRenderNotifsAreFiltered()514     public void testPreRenderNotifsAreFiltered() {
515         // GIVEN a NotifFilter that filters out a specific package
516         NotifFilter filter1 = spy(new PackageFilter(PACKAGE_2));
517         mListBuilder.addFinalizeFilter(filter1);
518 
519         // WHEN the pipeline is kicked off on a list of notifs
520         addNotif(0, PACKAGE_1);
521         addNotif(1, PACKAGE_2);
522         addNotif(2, PACKAGE_3);
523         addNotif(3, PACKAGE_2);
524         dispatchBuild();
525 
526         // THEN the filter is called on each notif in the original set
527         verify(filter1).shouldFilterOut(eq(mEntrySet.get(0)), anyLong());
528         verify(filter1).shouldFilterOut(eq(mEntrySet.get(1)), anyLong());
529         verify(filter1).shouldFilterOut(eq(mEntrySet.get(2)), anyLong());
530         verify(filter1).shouldFilterOut(eq(mEntrySet.get(3)), anyLong());
531 
532         // THEN the final list doesn't contain any filtered-out notifs
533         verifyBuiltList(
534                 notif(0),
535                 notif(2)
536         );
537 
538         // THEN each filtered notif records the filter that did it
539         assertEquals(filter1, mEntrySet.get(1).getExcludingFilter());
540         assertEquals(filter1, mEntrySet.get(3).getExcludingFilter());
541     }
542 
543     @Test
testPreRenderNotifsFilteredBreakupGroups()544     public void testPreRenderNotifsFilteredBreakupGroups() {
545         final String filterTag = "FILTER_ME";
546         // GIVEN a NotifFilter that filters out notifications with a tag
547         NotifFilter filter1 = spy(new NotifFilterWithTag(filterTag));
548         mListBuilder.addFinalizeFilter(filter1);
549 
550         // WHEN the pipeline is kicked off on a list of notifs
551         addGroupChildWithTag(0, PACKAGE_2, GROUP_1, filterTag);
552         addGroupChild(1, PACKAGE_2, GROUP_1);
553         addGroupSummary(2, PACKAGE_2, GROUP_1);
554         dispatchBuild();
555 
556         // THEN the final list doesn't contain any filtered-out notifs
557         // and groups that are too small are broken up
558         verifyBuiltList(
559                 notif(1)
560         );
561 
562         // THEN each filtered notif records the filter that did it
563         assertEquals(filter1, mEntrySet.get(0).getExcludingFilter());
564     }
565 
566     @Test
testFilter_resetsInitalizationTime()567     public void testFilter_resetsInitalizationTime() {
568         // GIVEN a NotifFilter that filters out a specific package
569         NotifFilter filter1 = spy(new PackageFilter(PACKAGE_1));
570         mListBuilder.addFinalizeFilter(filter1);
571 
572         // GIVEN a notification that was initialized 1 second ago that will be filtered out
573         final NotificationEntry entry = new NotificationEntryBuilder()
574                 .setPkg(PACKAGE_1)
575                 .setId(nextId(PACKAGE_1))
576                 .setRank(nextRank())
577                 .build();
578         entry.setInitializationTime(SystemClock.elapsedRealtime() - 1000);
579         assertTrue(entry.hasFinishedInitialization());
580 
581         // WHEN the pipeline is kicked off
582         mReadyForBuildListener.onBuildList(singletonList(entry), "test");
583         mPipelineChoreographer.runIfScheduled();
584 
585         // THEN the entry's initialization time is reset
586         assertFalse(entry.hasFinishedInitialization());
587     }
588 
589     @Test
testNotifFiltersCanBePreempted()590     public void testNotifFiltersCanBePreempted() {
591         // GIVEN two notif filters
592         NotifFilter filter1 = spy(new PackageFilter(PACKAGE_2));
593         NotifFilter filter2 = spy(new PackageFilter(PACKAGE_5));
594         mListBuilder.addPreGroupFilter(filter1);
595         mListBuilder.addPreGroupFilter(filter2);
596 
597         // WHEN the pipeline is kicked off on a list of notifs
598         addNotif(0, PACKAGE_1);
599         addNotif(1, PACKAGE_2);
600         addNotif(2, PACKAGE_5);
601         dispatchBuild();
602 
603         // THEN both filters are called on the first notif but the second filter is never called
604         // on the already-filtered second notif
605         verify(filter1).shouldFilterOut(eq(mEntrySet.get(0)), anyLong());
606         verify(filter1).shouldFilterOut(eq(mEntrySet.get(1)), anyLong());
607         verify(filter1).shouldFilterOut(eq(mEntrySet.get(2)), anyLong());
608         verify(filter2).shouldFilterOut(eq(mEntrySet.get(0)), anyLong());
609         verify(filter2).shouldFilterOut(eq(mEntrySet.get(2)), anyLong());
610 
611         // THEN the final list doesn't contain any filtered-out notifs
612         verifyBuiltList(
613                 notif(0)
614         );
615 
616         // THEN each filtered notif records the filter that did it
617         assertEquals(filter1, mEntrySet.get(1).getExcludingFilter());
618         assertEquals(filter2, mEntrySet.get(2).getExcludingFilter());
619     }
620 
621     @Test
testNotifsArePromoted()622     public void testNotifsArePromoted() {
623         // GIVEN a NotifPromoter that promotes certain notif IDs
624         NotifPromoter promoter = spy(new IdPromoter(1, 2));
625         mListBuilder.addPromoter(promoter);
626 
627         // WHEN the pipeline is kicked off
628         addNotif(0, PACKAGE_1);
629         addGroupChild(1, PACKAGE_2, GROUP_1);
630         addGroupChild(2, PACKAGE_2, GROUP_1);
631         addGroupChild(3, PACKAGE_2, GROUP_1);
632         addGroupChild(4, PACKAGE_2, GROUP_1);
633         addGroupSummary(5, PACKAGE_2, GROUP_1);
634         addNotif(6, PACKAGE_3);
635         dispatchBuild();
636 
637         // THEN the filter is called on each group child
638         verify(promoter).shouldPromoteToTopLevel(mEntrySet.get(1));
639         verify(promoter).shouldPromoteToTopLevel(mEntrySet.get(2));
640         verify(promoter).shouldPromoteToTopLevel(mEntrySet.get(3));
641         verify(promoter).shouldPromoteToTopLevel(mEntrySet.get(4));
642 
643         // THEN the final list contains the promoted entries at top level
644         verifyBuiltList(
645                 notif(0),
646                 notif(2),
647                 notif(3),
648                 group(
649                         summary(5),
650                         child(1),
651                         child(4)),
652                 notif(6)
653         );
654 
655         // THEN each promoted notif records the promoter that did it
656         assertEquals(promoter, mEntrySet.get(2).getNotifPromoter());
657         assertEquals(promoter, mEntrySet.get(3).getNotifPromoter());
658     }
659 
660     @Test
testNotifPromotersCanBePreempted()661     public void testNotifPromotersCanBePreempted() {
662         // GIVEN two notif promoters
663         NotifPromoter promoter1 = spy(new IdPromoter(1));
664         NotifPromoter promoter2 = spy(new IdPromoter(2));
665         mListBuilder.addPromoter(promoter1);
666         mListBuilder.addPromoter(promoter2);
667 
668         // WHEN the pipeline is kicked off on some notifs and a group
669         addNotif(0, PACKAGE_1);
670         addGroupChild(1, PACKAGE_2, GROUP_1);
671         addGroupChild(2, PACKAGE_2, GROUP_1);
672         addGroupChild(3, PACKAGE_2, GROUP_1);
673         addGroupSummary(4, PACKAGE_2, GROUP_1);
674         addNotif(5, PACKAGE_3);
675         dispatchBuild();
676 
677         // THEN both promoters are called on each child, except for children that a previous
678         // promoter has already promoted
679         verify(promoter1).shouldPromoteToTopLevel(mEntrySet.get(1));
680         verify(promoter1).shouldPromoteToTopLevel(mEntrySet.get(2));
681         verify(promoter1).shouldPromoteToTopLevel(mEntrySet.get(3));
682 
683         verify(promoter2).shouldPromoteToTopLevel(mEntrySet.get(1));
684         verify(promoter2).shouldPromoteToTopLevel(mEntrySet.get(3));
685 
686         // THEN each promoter is recorded on each notif it promoted
687         assertEquals(promoter1, mEntrySet.get(2).getNotifPromoter());
688         assertEquals(promoter2, mEntrySet.get(3).getNotifPromoter());
689     }
690 
691     @Test
testNotifSectionsChildrenUpdated()692     public void testNotifSectionsChildrenUpdated() {
693         ArrayList<ListEntry> pkg1Entries = new ArrayList<>();
694         ArrayList<ListEntry> pkg2Entries = new ArrayList<>();
695         ArrayList<ListEntry> pkg3Entries = new ArrayList<>();
696         final NotifSectioner pkg1Sectioner = spy(new PackageSectioner(PACKAGE_1) {
697             @Override
698             public void onEntriesUpdated(List<ListEntry> entries) {
699                 super.onEntriesUpdated(entries);
700                 pkg1Entries.addAll(entries);
701             }
702         });
703         final NotifSectioner pkg2Sectioner = spy(new PackageSectioner(PACKAGE_2) {
704             @Override
705             public void onEntriesUpdated(List<ListEntry> entries) {
706                 super.onEntriesUpdated(entries);
707                 pkg2Entries.addAll(entries);
708             }
709         });
710         final NotifSectioner pkg3Sectioner = spy(new PackageSectioner(PACKAGE_3) {
711             @Override
712             public void onEntriesUpdated(List<ListEntry> entries) {
713                 super.onEntriesUpdated(entries);
714                 pkg3Entries.addAll(entries);
715             }
716         });
717         mListBuilder.setSectioners(asList(pkg1Sectioner, pkg2Sectioner, pkg3Sectioner));
718 
719         addNotif(0, PACKAGE_1);
720         addNotif(1, PACKAGE_1);
721         addNotif(2, PACKAGE_3);
722         addNotif(3, PACKAGE_3);
723         addNotif(4, PACKAGE_3);
724 
725         dispatchBuild();
726 
727         verify(pkg1Sectioner).onEntriesUpdated(any());
728         verify(pkg2Sectioner).onEntriesUpdated(any());
729         verify(pkg3Sectioner).onEntriesUpdated(any());
730         assertThat(pkg1Entries).containsExactly(
731                 mEntrySet.get(0),
732                 mEntrySet.get(1)
733         ).inOrder();
734         assertThat(pkg2Entries).isEmpty();
735         assertThat(pkg3Entries).containsExactly(
736                 mEntrySet.get(2),
737                 mEntrySet.get(3),
738                 mEntrySet.get(4)
739         ).inOrder();
740     }
741 
742     @Test
testNotifSections()743     public void testNotifSections() {
744         // GIVEN a filter that removes all PACKAGE_4 notifs and sections that divide
745         // notifs based on package name
746         mListBuilder.addPreGroupFilter(new PackageFilter(PACKAGE_4));
747         final NotifSectioner pkg1Sectioner = spy(new PackageSectioner(PACKAGE_1));
748         final NotifSectioner pkg2Sectioner = spy(new PackageSectioner(PACKAGE_2));
749         // NOTE: no package 3 section explicitly added, so notifs with package 3 will get set by
750         // ShadeListBuilder's sDefaultSection which will demote it to the last section
751         final NotifSectioner pkg4Sectioner = spy(new PackageSectioner(PACKAGE_4));
752         final NotifSectioner pkg5Sectioner = spy(new PackageSectioner(PACKAGE_5));
753         mListBuilder.setSectioners(
754                 asList(pkg1Sectioner, pkg2Sectioner, pkg4Sectioner, pkg5Sectioner));
755 
756         final NotifSection pkg1Section = new NotifSection(pkg1Sectioner, 0);
757         final NotifSection pkg2Section = new NotifSection(pkg2Sectioner, 1);
758         final NotifSection pkg5Section = new NotifSection(pkg5Sectioner, 3);
759 
760         // WHEN we build a list with different packages
761         addNotif(0, PACKAGE_4);
762         addNotif(1, PACKAGE_2);
763         addNotif(2, PACKAGE_1);
764         addNotif(3, PACKAGE_3);
765         addGroupSummary(4, PACKAGE_2, GROUP_1);
766         addGroupChild(5, PACKAGE_2, GROUP_1);
767         addGroupChild(6, PACKAGE_2, GROUP_1);
768         addNotif(7, PACKAGE_1);
769         addNotif(8, PACKAGE_2);
770         addNotif(9, PACKAGE_5);
771         addNotif(10, PACKAGE_4);
772         dispatchBuild();
773 
774         // THEN the list is sorted according to section
775         verifyBuiltList(
776                 notif(2),
777                 notif(7),
778                 notif(1),
779                 group(
780                         summary(4),
781                         child(5),
782                         child(6)
783                 ),
784                 notif(8),
785                 notif(9),
786                 notif(3)
787         );
788 
789         // THEN the first section (pkg1Section) is called on all top level elements (but
790         // no children and no entries that were filtered out)
791         verify(pkg1Sectioner).isInSection(mEntrySet.get(1));
792         verify(pkg1Sectioner).isInSection(mEntrySet.get(2));
793         verify(pkg1Sectioner).isInSection(mEntrySet.get(3));
794         verify(pkg1Sectioner).isInSection(mEntrySet.get(7));
795         verify(pkg1Sectioner).isInSection(mEntrySet.get(8));
796         verify(pkg1Sectioner).isInSection(mEntrySet.get(9));
797         verify(pkg1Sectioner).isInSection(mBuiltList.get(3));
798 
799         verify(pkg1Sectioner, never()).isInSection(mEntrySet.get(0));
800         verify(pkg1Sectioner, never()).isInSection(mEntrySet.get(4));
801         verify(pkg1Sectioner, never()).isInSection(mEntrySet.get(5));
802         verify(pkg1Sectioner, never()).isInSection(mEntrySet.get(6));
803         verify(pkg1Sectioner, never()).isInSection(mEntrySet.get(10));
804 
805         // THEN the last section (pkg5Section) is not called on any of the entries that were
806         // filtered or already in a section
807         verify(pkg5Sectioner, never()).isInSection(mEntrySet.get(0));
808         verify(pkg5Sectioner, never()).isInSection(mEntrySet.get(1));
809         verify(pkg5Sectioner, never()).isInSection(mEntrySet.get(2));
810         verify(pkg5Sectioner, never()).isInSection(mEntrySet.get(4));
811         verify(pkg5Sectioner, never()).isInSection(mEntrySet.get(5));
812         verify(pkg5Sectioner, never()).isInSection(mEntrySet.get(6));
813         verify(pkg5Sectioner, never()).isInSection(mEntrySet.get(7));
814         verify(pkg5Sectioner, never()).isInSection(mEntrySet.get(8));
815         verify(pkg5Sectioner, never()).isInSection(mEntrySet.get(10));
816 
817         verify(pkg5Sectioner).isInSection(mEntrySet.get(3));
818         verify(pkg5Sectioner).isInSection(mEntrySet.get(9));
819 
820         // THEN the correct section is assigned for entries in pkg1Section
821         assertEquals(pkg1Section, mEntrySet.get(2).getSection());
822         assertEquals(pkg1Section, mEntrySet.get(7).getSection());
823 
824         // THEN the correct section is assigned for entries in pkg2Section
825         assertEquals(pkg2Section, mEntrySet.get(1).getSection());
826         assertEquals(pkg2Section, mEntrySet.get(8).getSection());
827         assertEquals(pkg2Section, mBuiltList.get(3).getSection());
828 
829         // THEN no section was assigned to entries in pkg4Section (since they were filtered)
830         assertNull(mEntrySet.get(0).getSection());
831         assertNull(mEntrySet.get(10).getSection());
832 
833         // THEN the correct section is assigned for entries in pkg5Section
834         assertEquals(pkg5Section, mEntrySet.get(9).getSection());
835 
836         // THEN the children entries are assigned the same section as its parent
837         assertEquals(mBuiltList.get(3).getSection(), child(5).entry.getSection());
838         assertEquals(mBuiltList.get(3).getSection(), child(6).entry.getSection());
839     }
840 
841     @Test
testNotifUsesDefaultSection()842     public void testNotifUsesDefaultSection() {
843         // GIVEN a Section for Package2
844         final NotifSectioner pkg2Section = spy(new PackageSectioner(PACKAGE_2));
845         mListBuilder.setSectioners(singletonList(pkg2Section));
846 
847         // WHEN we build a list with pkg1 and pkg2 packages
848         addNotif(0, PACKAGE_1);
849         addNotif(1, PACKAGE_2);
850         dispatchBuild();
851 
852         // THEN the list is sorted according to section
853         verifyBuiltList(
854                 notif(1),
855                 notif(0)
856         );
857 
858         // THEN the entry that didn't have an explicit section gets assigned the DefaultSection
859         assertNotNull(notif(0).entry.getSection());
860         assertEquals(1, notif(0).entry.getSectionIndex());
861     }
862 
863     @Test
testThatNotifComparatorsAreCalled()864     public void testThatNotifComparatorsAreCalled() {
865         // GIVEN a set of comparators that care about specific packages
866         mListBuilder.setComparators(asList(
867                 new HypeComparator(PACKAGE_4),
868                 new HypeComparator(PACKAGE_1, PACKAGE_3),
869                 new HypeComparator(PACKAGE_2)
870         ));
871 
872         // WHEN the pipeline is kicked off on a bunch of notifications
873         addNotif(0, PACKAGE_1);
874         addNotif(1, PACKAGE_5);
875         addNotif(2, PACKAGE_3);
876         addNotif(3, PACKAGE_4);
877         addNotif(4, PACKAGE_2);
878         dispatchBuild();
879 
880         // THEN the notifs are sorted according to the hierarchy of comparators
881         verifyBuiltList(
882                 notif(3),
883                 notif(0),
884                 notif(2),
885                 notif(4),
886                 notif(1)
887         );
888     }
889 
890     @Test
testThatSectionComparatorsAreCalled()891     public void testThatSectionComparatorsAreCalled() {
892         // GIVEN a section with a comparator that elevates some packages over others
893         NotifComparator comparator = spy(new HypeComparator(PACKAGE_2, PACKAGE_4));
894         NotifSectioner sectioner = new PackageSectioner(
895                 List.of(PACKAGE_1, PACKAGE_2, PACKAGE_4, PACKAGE_5), comparator);
896         mListBuilder.setSectioners(List.of(sectioner));
897 
898         // WHEN the pipeline is kicked off on a bunch of notifications
899         addNotif(0, PACKAGE_0);
900         addNotif(1, PACKAGE_1);
901         addNotif(2, PACKAGE_2);
902         addNotif(3, PACKAGE_3);
903         addNotif(4, PACKAGE_4);
904         addNotif(5, PACKAGE_5);
905         dispatchBuild();
906 
907         // THEN the notifs are sorted according to both sectioning and the section's comparator
908         verifyBuiltList(
909                 notif(2),
910                 notif(4),
911                 notif(1),
912                 notif(5),
913                 notif(0),
914                 notif(3)
915         );
916 
917         // VERIFY that the comparator is invoked at least 3 times
918         verify(comparator, atLeast(3)).compare(any(), any());
919 
920         // VERIFY that the comparator is never invoked with the entry from package 0 or 3.
921         final NotificationEntry package0Entry = mEntrySet.get(0);
922         verify(comparator, never()).compare(eq(package0Entry), any());
923         verify(comparator, never()).compare(any(), eq(package0Entry));
924         final NotificationEntry package3Entry = mEntrySet.get(3);
925         verify(comparator, never()).compare(eq(package3Entry), any());
926         verify(comparator, never()).compare(any(), eq(package3Entry));
927     }
928 
929     @Test
testThatSectionComparatorsAreNotCalledForSectionWithSingleEntry()930     public void testThatSectionComparatorsAreNotCalledForSectionWithSingleEntry() {
931         // GIVEN a section with a comparator that will have only 1 element
932         NotifComparator comparator = spy(new HypeComparator(PACKAGE_3));
933         NotifSectioner sectioner = new PackageSectioner(List.of(PACKAGE_3), comparator);
934         mListBuilder.setSectioners(List.of(sectioner));
935 
936         // WHEN the pipeline is kicked off on a bunch of notifications
937         addNotif(0, PACKAGE_1);
938         addNotif(1, PACKAGE_2);
939         addNotif(2, PACKAGE_3);
940         addNotif(3, PACKAGE_4);
941         addNotif(4, PACKAGE_5);
942         dispatchBuild();
943 
944         // THEN the notifs are sorted according to the sectioning
945         verifyBuiltList(
946                 notif(2),
947                 notif(0),
948                 notif(1),
949                 notif(3),
950                 notif(4)
951         );
952 
953         // VERIFY that the comparator is never invoked
954         verify(comparator, never()).compare(any(), any());
955     }
956 
957     @Test
testListenersAndPluggablesAreFiredInOrder()958     public void testListenersAndPluggablesAreFiredInOrder() {
959         // GIVEN a bunch of registered listeners and pluggables
960         NotifFilter preGroupFilter = spy(new PackageFilter(PACKAGE_1));
961         NotifPromoter promoter = spy(new IdPromoter(3));
962         NotifSectioner section = spy(new PackageSectioner(PACKAGE_1));
963         NotifComparator comparator = spy(new HypeComparator(PACKAGE_4));
964         NotifFilter preRenderFilter = spy(new PackageFilter(PACKAGE_5));
965         mListBuilder.addPreGroupFilter(preGroupFilter);
966         mListBuilder.addOnBeforeTransformGroupsListener(mOnBeforeTransformGroupsListener);
967         mListBuilder.addPromoter(promoter);
968         mListBuilder.addOnBeforeSortListener(mOnBeforeSortListener);
969         mListBuilder.setComparators(singletonList(comparator));
970         mListBuilder.setSectioners(singletonList(section));
971         mListBuilder.addOnBeforeFinalizeFilterListener(mOnBeforeFinalizeFilterListener);
972         mListBuilder.addFinalizeFilter(preRenderFilter);
973         mListBuilder.addOnBeforeRenderListListener(mOnBeforeRenderListListener);
974 
975         // WHEN a few new notifs are added
976         addNotif(0, PACKAGE_1);
977         addGroupSummary(1, PACKAGE_2, GROUP_1);
978         addGroupChild(2, PACKAGE_2, GROUP_1);
979         addGroupChild(3, PACKAGE_2, GROUP_1);
980         addNotif(4, PACKAGE_5);
981         addNotif(5, PACKAGE_5);
982         addNotif(6, PACKAGE_4);
983         dispatchBuild();
984 
985         // THEN the pluggables and listeners are called in order
986         InOrder inOrder = inOrder(
987                 preGroupFilter,
988                 mOnBeforeTransformGroupsListener,
989                 promoter,
990                 mOnBeforeSortListener,
991                 section,
992                 comparator,
993                 mOnBeforeFinalizeFilterListener,
994                 preRenderFilter,
995                 mOnBeforeRenderListListener,
996                 mOnRenderListListener);
997 
998         inOrder.verify(preGroupFilter, atLeastOnce())
999                 .shouldFilterOut(any(NotificationEntry.class), anyLong());
1000         inOrder.verify(mOnBeforeTransformGroupsListener)
1001                 .onBeforeTransformGroups(anyList());
1002         inOrder.verify(promoter, atLeastOnce())
1003                 .shouldPromoteToTopLevel(any(NotificationEntry.class));
1004         inOrder.verify(mOnBeforeSortListener).onBeforeSort(anyList());
1005         inOrder.verify(section, atLeastOnce()).isInSection(any(ListEntry.class));
1006         inOrder.verify(comparator, atLeastOnce())
1007                 .compare(any(ListEntry.class), any(ListEntry.class));
1008         inOrder.verify(mOnBeforeFinalizeFilterListener).onBeforeFinalizeFilter(anyList());
1009         inOrder.verify(preRenderFilter, atLeastOnce())
1010                 .shouldFilterOut(any(NotificationEntry.class), anyLong());
1011         inOrder.verify(mOnBeforeRenderListListener).onBeforeRenderList(anyList());
1012         inOrder.verify(mOnRenderListListener).onRenderList(anyList());
1013     }
1014 
1015     @Test
testThatPluggableInvalidationsTriggersRerun()1016     public void testThatPluggableInvalidationsTriggersRerun() {
1017         // GIVEN a variety of pluggables
1018         NotifFilter packageFilter = new PackageFilter(PACKAGE_1);
1019         NotifPromoter idPromoter = new IdPromoter(4);
1020         NotifComparator sectionComparator = new HypeComparator(PACKAGE_1);
1021         NotifSectioner section = new PackageSectioner(List.of(PACKAGE_1), sectionComparator);
1022         NotifComparator hypeComparator = new HypeComparator(PACKAGE_2);
1023         Invalidator preRenderInvalidator = new Invalidator("PreRenderInvalidator") {};
1024 
1025         mListBuilder.addPreGroupFilter(packageFilter);
1026         mListBuilder.addPromoter(idPromoter);
1027         mListBuilder.setSectioners(singletonList(section));
1028         mListBuilder.setComparators(singletonList(hypeComparator));
1029         mListBuilder.addPreRenderInvalidator(preRenderInvalidator);
1030 
1031         // GIVEN a set of random notifs
1032         addNotif(0, PACKAGE_1);
1033         addNotif(1, PACKAGE_2);
1034         addNotif(2, PACKAGE_3);
1035         dispatchBuild();
1036 
1037         // WHEN each pluggable is invalidated THEN the list is re-rendered
1038 
1039         clearInvocations(mOnRenderListListener);
1040         packageFilter.invalidateList(null);
1041         assertTrue(mPipelineChoreographer.isScheduled());
1042         mPipelineChoreographer.runIfScheduled();
1043         verify(mOnRenderListListener).onRenderList(anyList());
1044 
1045         clearInvocations(mOnRenderListListener);
1046         idPromoter.invalidateList(null);
1047         assertTrue(mPipelineChoreographer.isScheduled());
1048         mPipelineChoreographer.runIfScheduled();
1049         verify(mOnRenderListListener).onRenderList(anyList());
1050 
1051         clearInvocations(mOnRenderListListener);
1052         section.invalidateList(null);
1053         assertTrue(mPipelineChoreographer.isScheduled());
1054         mPipelineChoreographer.runIfScheduled();
1055         verify(mOnRenderListListener).onRenderList(anyList());
1056 
1057         clearInvocations(mOnRenderListListener);
1058         hypeComparator.invalidateList(null);
1059         assertTrue(mPipelineChoreographer.isScheduled());
1060         mPipelineChoreographer.runIfScheduled();
1061         verify(mOnRenderListListener).onRenderList(anyList());
1062 
1063         clearInvocations(mOnRenderListListener);
1064         sectionComparator.invalidateList(null);
1065         assertTrue(mPipelineChoreographer.isScheduled());
1066         mPipelineChoreographer.runIfScheduled();
1067         verify(mOnRenderListListener).onRenderList(anyList());
1068 
1069         clearInvocations(mOnRenderListListener);
1070         preRenderInvalidator.invalidateList(null);
1071         assertTrue(mPipelineChoreographer.isScheduled());
1072         mPipelineChoreographer.runIfScheduled();
1073         verify(mOnRenderListListener).onRenderList(anyList());
1074     }
1075 
1076     @Test
testNotifFiltersAreAllSentTheSameNow()1077     public void testNotifFiltersAreAllSentTheSameNow() {
1078         // GIVEN three notif filters
1079         NotifFilter filter1 = spy(new PackageFilter(PACKAGE_5));
1080         NotifFilter filter2 = spy(new PackageFilter(PACKAGE_5));
1081         NotifFilter filter3 = spy(new PackageFilter(PACKAGE_5));
1082         mListBuilder.addPreGroupFilter(filter1);
1083         mListBuilder.addPreGroupFilter(filter2);
1084         mListBuilder.addPreGroupFilter(filter3);
1085 
1086         // GIVEN the SystemClock is set to a particular time:
1087         mSystemClock.setUptimeMillis(10047);
1088 
1089         // WHEN the pipeline is kicked off on a list of notifs
1090         addNotif(0, PACKAGE_1);
1091         addNotif(1, PACKAGE_2);
1092         dispatchBuild();
1093 
1094         // THEN the value of `now` is the same for all calls to shouldFilterOut
1095         verify(filter1).shouldFilterOut(mEntrySet.get(0), 10047);
1096         verify(filter2).shouldFilterOut(mEntrySet.get(0), 10047);
1097         verify(filter3).shouldFilterOut(mEntrySet.get(0), 10047);
1098         verify(filter1).shouldFilterOut(mEntrySet.get(1), 10047);
1099         verify(filter2).shouldFilterOut(mEntrySet.get(1), 10047);
1100         verify(filter3).shouldFilterOut(mEntrySet.get(1), 10047);
1101     }
1102 
1103     @Test
testGroupTransformEntries()1104     public void testGroupTransformEntries() {
1105         // GIVEN a registered OnBeforeTransformGroupsListener
1106         RecordingOnBeforeTransformGroupsListener listener =
1107                 new RecordingOnBeforeTransformGroupsListener();
1108         mListBuilder.addOnBeforeTransformGroupsListener(listener);
1109 
1110         // GIVEN some new notifs
1111         addNotif(0, PACKAGE_1);
1112         addGroupChild(1, PACKAGE_2, GROUP_1);
1113         addGroupSummary(2, PACKAGE_2, GROUP_1);
1114         addGroupChild(3, PACKAGE_2, GROUP_1);
1115         addNotif(4, PACKAGE_3);
1116         addGroupChild(5, PACKAGE_2, GROUP_1);
1117 
1118         // WHEN we run the pipeline
1119         dispatchBuild();
1120 
1121         verifyBuiltList(
1122                 notif(0),
1123                 group(
1124                         summary(2),
1125                         child(1),
1126                         child(3),
1127                         child(5)
1128                 ),
1129                 notif(4)
1130         );
1131 
1132         // THEN all the new notifs, including the new GroupEntry, are passed to the listener
1133         assertThat(listener.mEntriesReceived).containsExactly(
1134                 mEntrySet.get(0),
1135                 mBuiltList.get(1),
1136                 mEntrySet.get(4)
1137         ).inOrder(); // Order is a bonus because this listener is before sort
1138     }
1139 
1140     @Test
testGroupTransformEntriesOnSecondRun()1141     public void testGroupTransformEntriesOnSecondRun() {
1142         // GIVEN a registered OnBeforeTransformGroupsListener
1143         RecordingOnBeforeTransformGroupsListener listener =
1144                 spy(new RecordingOnBeforeTransformGroupsListener());
1145         mListBuilder.addOnBeforeTransformGroupsListener(listener);
1146 
1147         // GIVEN some notifs that have already been added (two of which are in malformed groups)
1148         addNotif(0, PACKAGE_1);
1149         addGroupChild(1, PACKAGE_2, GROUP_1);
1150         addGroupChild(2, PACKAGE_3, GROUP_2);
1151 
1152         dispatchBuild();
1153         clearInvocations(listener);
1154 
1155         // WHEN we run the pipeline
1156         addGroupSummary(3, PACKAGE_2, GROUP_1);
1157         addGroupChild(4, PACKAGE_3, GROUP_2);
1158         addGroupSummary(5, PACKAGE_3, GROUP_2);
1159         addGroupChild(6, PACKAGE_3, GROUP_2);
1160         addNotif(7, PACKAGE_2);
1161 
1162         dispatchBuild();
1163 
1164         verifyBuiltList(
1165                 notif(0),
1166                 notif(1),
1167                 group(
1168                         summary(5),
1169                         child(2),
1170                         child(4),
1171                         child(6)
1172                 ),
1173                 notif(7)
1174         );
1175 
1176         // THEN all the new notifs, including the new GroupEntry, are passed to the listener
1177         assertThat(listener.mEntriesReceived).containsExactly(
1178                 mEntrySet.get(0),
1179                 mEntrySet.get(1),
1180                 mBuiltList.get(2),
1181                 mEntrySet.get(7)
1182         ).inOrder(); // Order is a bonus because this listener is before sort
1183     }
1184 
1185     @Test
testStabilizeGroupsAlwaysAllowsGroupChangeFromDeletedGroupToRoot()1186     public void testStabilizeGroupsAlwaysAllowsGroupChangeFromDeletedGroupToRoot() {
1187         // GIVEN a group w/ summary and two children
1188         addGroupSummary(0, PACKAGE_1, GROUP_1);
1189         addGroupChild(1, PACKAGE_1, GROUP_1);
1190         addGroupChild(2, PACKAGE_1, GROUP_1);
1191         dispatchBuild();
1192 
1193         // GIVEN visual stability manager doesn't allow any group changes
1194         mStabilityManager.setAllowGroupChanges(false);
1195 
1196         // WHEN we run the pipeline with the summary and one child removed
1197         mEntrySet.remove(2);
1198         mEntrySet.remove(0);
1199         dispatchBuild();
1200 
1201         // THEN all that remains is the one child at top-level, despite no group change allowed by
1202         // visual stability manager.
1203         verifyBuiltList(
1204                 notif(0)
1205         );
1206     }
1207 
1208     @Test
testStabilizeGroupsDoesNotAllowGroupingExistingNotifications()1209     public void testStabilizeGroupsDoesNotAllowGroupingExistingNotifications() {
1210         // GIVEN one group child without a summary yet
1211         addGroupChild(0, PACKAGE_1, GROUP_1);
1212 
1213         dispatchBuild();
1214 
1215         // GIVEN visual stability manager doesn't allow any group changes
1216         mStabilityManager.setAllowGroupChanges(false);
1217 
1218         // WHEN we run the pipeline with the addition of a group summary & child
1219         addGroupSummary(1, PACKAGE_1, GROUP_1);
1220         addGroupChild(2, PACKAGE_1, GROUP_1);
1221 
1222         dispatchBuild();
1223 
1224         // THEN all notifications are top-level and the summary doesn't show yet
1225         // because group changes aren't allowed by the stability manager
1226         verifyBuiltList(
1227                 notif(0),
1228                 group(
1229                         summary(1),
1230                         child(2)
1231                 )
1232         );
1233     }
1234 
1235     @Test
testStabilizeGroupsAllowsGroupingAllNewNotifications()1236     public void testStabilizeGroupsAllowsGroupingAllNewNotifications() {
1237         // GIVEN visual stability manager doesn't allow any group changes
1238         mStabilityManager.setAllowGroupChanges(false);
1239 
1240         // WHEN we run the pipeline with all new notification groups
1241         addGroupChild(0, PACKAGE_1, GROUP_1);
1242         addGroupSummary(1, PACKAGE_1, GROUP_1);
1243         addGroupChild(2, PACKAGE_1, GROUP_1);
1244         addGroupSummary(3, PACKAGE_2, GROUP_2);
1245         addGroupChild(4, PACKAGE_2, GROUP_2);
1246         addGroupChild(5, PACKAGE_2, GROUP_2);
1247 
1248         dispatchBuild();
1249 
1250         // THEN all notifications are grouped since they're all new
1251         verifyBuiltList(
1252                 group(
1253                         summary(1),
1254                         child(0),
1255                         child(2)
1256                 ),
1257                 group(
1258                         summary(3),
1259                         child(4),
1260                         child(5)
1261                 )
1262         );
1263     }
1264 
1265 
1266     @Test
testStabilizeGroupsAllowsGroupingOnlyNewNotifications()1267     public void testStabilizeGroupsAllowsGroupingOnlyNewNotifications() {
1268         // GIVEN one group child without a summary yet
1269         addGroupChild(0, PACKAGE_1, GROUP_1);
1270 
1271         dispatchBuild();
1272 
1273         // GIVEN visual stability manager doesn't allow any group changes
1274         mStabilityManager.setAllowGroupChanges(false);
1275 
1276         // WHEN we run the pipeline with the addition of a group summary & child
1277         addGroupSummary(1, PACKAGE_1, GROUP_1);
1278         addGroupChild(2, PACKAGE_1, GROUP_1);
1279         addGroupSummary(3, PACKAGE_2, GROUP_2);
1280         addGroupChild(4, PACKAGE_2, GROUP_2);
1281         addGroupChild(5, PACKAGE_2, GROUP_2);
1282 
1283         dispatchBuild();
1284 
1285         // THEN first notification stays top-level but the other notifications are grouped.
1286         verifyBuiltList(
1287                 notif(0),
1288                 group(
1289                         summary(1),
1290                         child(2)
1291                 ),
1292                 group(
1293                         summary(3),
1294                         child(4),
1295                         child(5)
1296                 )
1297         );
1298     }
1299 
1300     @Test
testFinalizeFilteringGroupSummaryDoesNotBreakSort()1301     public void testFinalizeFilteringGroupSummaryDoesNotBreakSort() {
1302         // GIVEN children from 3 packages, with one in the middle of the sort order being a group
1303         addNotif(0, PACKAGE_1);
1304         addNotif(1, PACKAGE_2);
1305         addNotif(2, PACKAGE_3);
1306         addNotif(3, PACKAGE_1);
1307         addNotif(4, PACKAGE_2);
1308         addNotif(5, PACKAGE_3);
1309         addGroupSummary(6, PACKAGE_2, GROUP_1);
1310         addGroupChild(7, PACKAGE_2, GROUP_1);
1311         addGroupChild(8, PACKAGE_2, GROUP_1);
1312 
1313         // GIVEN that they should be sorted by package
1314         mListBuilder.setComparators(asList(
1315                 new HypeComparator(PACKAGE_1),
1316                 new HypeComparator(PACKAGE_2),
1317                 new HypeComparator(PACKAGE_3)
1318         ));
1319 
1320         // WHEN a finalize filter removes the summary
1321         mListBuilder.addFinalizeFilter(new NotifFilter("Test") {
1322             @Override
1323             public boolean shouldFilterOut(@NonNull NotificationEntry entry, long now) {
1324                 return entry == notif(6).entry;
1325             }
1326         });
1327 
1328         dispatchBuild();
1329 
1330         // THEN the notifications remain ordered by package, even though the children were promoted
1331         verifyBuiltList(
1332                 notif(0),
1333                 notif(3),
1334                 notif(1),
1335                 notif(4),
1336                 notif(7),  // promoted child
1337                 notif(8),  // promoted child
1338                 notif(2),
1339                 notif(5)
1340         );
1341     }
1342 
1343     @Test
testFinalizeFilteringGroupChildDoesNotBreakSort()1344     public void testFinalizeFilteringGroupChildDoesNotBreakSort() {
1345         // GIVEN children from 3 packages, with one in the middle of the sort order being a group
1346         addNotif(0, PACKAGE_1);
1347         addNotif(1, PACKAGE_2);
1348         addNotif(2, PACKAGE_3);
1349         addNotif(3, PACKAGE_1);
1350         addNotif(4, PACKAGE_2);
1351         addNotif(5, PACKAGE_3);
1352         addGroupSummary(6, PACKAGE_2, GROUP_1);
1353         addGroupChild(7, PACKAGE_2, GROUP_1);
1354         addGroupChild(8, PACKAGE_2, GROUP_1);
1355 
1356         // GIVEN that they should be sorted by package
1357         mListBuilder.setComparators(asList(
1358                 new HypeComparator(PACKAGE_1),
1359                 new HypeComparator(PACKAGE_2),
1360                 new HypeComparator(PACKAGE_3)
1361         ));
1362 
1363         // WHEN a finalize filter one of the 2 children from a group
1364         mListBuilder.addFinalizeFilter(new NotifFilter("Test") {
1365             @Override
1366             public boolean shouldFilterOut(@NonNull NotificationEntry entry, long now) {
1367                 return entry == notif(7).entry;
1368             }
1369         });
1370 
1371         dispatchBuild();
1372 
1373         // THEN the notifications remain ordered by package, even though the children were promoted
1374         verifyBuiltList(
1375                 notif(0),
1376                 notif(3),
1377                 notif(1),
1378                 notif(4),
1379                 notif(8),  // promoted child
1380                 notif(2),
1381                 notif(5)
1382         );
1383     }
1384 
1385     @Test
testStabilityIsolationAllowsGroupToHaveSingleChild()1386     public void testStabilityIsolationAllowsGroupToHaveSingleChild() {
1387         // GIVEN a group with only one child was already drawn
1388         addGroupSummary(0, PACKAGE_1, GROUP_1);
1389         addGroupChild(1, PACKAGE_1, GROUP_1);
1390 
1391         dispatchBuild();
1392         // NOTICE that the group is pruned and the child is moved to the top level
1393         verifyBuiltList(
1394                 notif(1)  // group with only one child is promoted
1395         );
1396 
1397         // WHEN another child is added while group changes are disabled.
1398         mStabilityManager.setAllowGroupChanges(false);
1399         addGroupChild(2, PACKAGE_1, GROUP_1);
1400 
1401         dispatchBuild();
1402 
1403         // THEN the new child should be added to the group
1404         verifyBuiltList(
1405                 group(
1406                         summary(0),
1407                         child(2)
1408                 ),
1409                 notif(1)
1410         );
1411     }
1412 
1413     @Test
testStabilityIsolationExemptsGroupWithFinalizeFilteredChildFromShowingSummary()1414     public void testStabilityIsolationExemptsGroupWithFinalizeFilteredChildFromShowingSummary() {
1415         // GIVEN a group with only one child was already drawn
1416         addGroupSummary(0, PACKAGE_1, GROUP_1);
1417         addGroupChild(1, PACKAGE_1, GROUP_1);
1418 
1419         dispatchBuild();
1420         // NOTICE that the group is pruned and the child is moved to the top level
1421         verifyBuiltList(
1422                 notif(1)  // group with only one child is promoted
1423         );
1424 
1425         // WHEN another child is added but still filtered while group changes are disabled.
1426         mStabilityManager.setAllowGroupChanges(false);
1427         mFinalizeFilter.mIndicesToFilter.add(2);
1428         addGroupChild(2, PACKAGE_1, GROUP_1);
1429 
1430         dispatchBuild();
1431 
1432         // THEN the new child should be shown without the summary
1433         verifyBuiltList(
1434                 notif(1)  // previously promoted child
1435         );
1436     }
1437 
1438     @Test
testStabilityIsolationOfRemovedChildDoesNotExemptGroupFromPrune()1439     public void testStabilityIsolationOfRemovedChildDoesNotExemptGroupFromPrune() {
1440         // GIVEN a group with only one child was already drawn
1441         addGroupSummary(0, PACKAGE_1, GROUP_1);
1442         addGroupChild(1, PACKAGE_1, GROUP_1);
1443 
1444         dispatchBuild();
1445         // NOTICE that the group is pruned and the child is moved to the top level
1446         verifyBuiltList(
1447                 notif(1)  // group with only one child is promoted
1448         );
1449 
1450         // WHEN a new child is added and the old one gets filtered while group changes are disabled.
1451         mStabilityManager.setAllowGroupChanges(false);
1452         mStabilityManager.setAllowGroupPruning(false);
1453         mFinalizeFilter.mIndicesToFilter.add(1);
1454         addGroupChild(2, PACKAGE_1, GROUP_1);
1455 
1456         dispatchBuild();
1457 
1458         // THEN the new child should be shown without a group
1459         // (Note that this is the same as the expected result if there were no stability rules.)
1460         verifyBuiltList(
1461                 notif(2)  // new child
1462         );
1463     }
1464 
1465     @Test
testGroupWithChildRemovedByFilterIsPrunedWhenOtherwiseEmpty()1466     public void testGroupWithChildRemovedByFilterIsPrunedWhenOtherwiseEmpty() {
1467         // GIVEN a group with only one child
1468         addGroupSummary(0, PACKAGE_1, GROUP_1);
1469         addGroupChild(1, PACKAGE_1, GROUP_1);
1470         dispatchBuild();
1471         // NOTICE that the group is pruned and the child is moved to the top level
1472         verifyBuiltList(
1473                 notif(1)  // group with only one child is promoted
1474         );
1475 
1476         // WHEN the only child is filtered
1477         mFinalizeFilter.mIndicesToFilter.add(1);
1478         dispatchBuild();
1479 
1480         // THEN the new list should be empty (the group summary should not be promoted)
1481         verifyBuiltList();
1482     }
1483 
1484     @Test
testFinalizeFilteredSummaryPromotesChildren()1485     public void testFinalizeFilteredSummaryPromotesChildren() {
1486         // GIVEN a group with only one child was already drawn
1487         addGroupSummary(0, PACKAGE_1, GROUP_1);
1488         addGroupChild(1, PACKAGE_1, GROUP_1);
1489         addGroupChild(2, PACKAGE_1, GROUP_1);
1490 
1491         // WHEN the parent is filtered out at the finalize step
1492         mFinalizeFilter.mIndicesToFilter.add(0);
1493 
1494         dispatchBuild();
1495 
1496         // THEN the children should be promoted to the top level
1497         verifyBuiltList(
1498                 notif(1),
1499                 notif(2)
1500         );
1501     }
1502 
1503     @Test
testFinalizeFilteredChildPromotesSibling()1504     public void testFinalizeFilteredChildPromotesSibling() {
1505         // GIVEN a group with only one child was already drawn
1506         addGroupSummary(0, PACKAGE_1, GROUP_1);
1507         addGroupChild(1, PACKAGE_1, GROUP_1);
1508         addGroupChild(2, PACKAGE_1, GROUP_1);
1509 
1510         // WHEN the parent is filtered out at the finalize step
1511         mFinalizeFilter.mIndicesToFilter.add(1);
1512 
1513         dispatchBuild();
1514 
1515         // THEN the children should be promoted to the top level
1516         verifyBuiltList(
1517                 notif(2)
1518         );
1519     }
1520 
1521     @Test
testBrokenGroupNotificationOrdering()1522     public void testBrokenGroupNotificationOrdering() {
1523         // GIVEN two group children with different sections & without a summary yet
1524         addGroupChild(0, PACKAGE_2, GROUP_1);
1525         addNotif(1, PACKAGE_1);
1526         addGroupChild(2, PACKAGE_2, GROUP_1);
1527         addGroupChild(3, PACKAGE_2, GROUP_1);
1528 
1529         dispatchBuild();
1530 
1531         // THEN all notifications are not grouped and posted in order by index
1532         verifyBuiltList(
1533                 notif(0),
1534                 notif(1),
1535                 notif(2),
1536                 notif(3)
1537         );
1538     }
1539 
1540     @Test
testContiguousSections()1541     public void testContiguousSections() {
1542         mListBuilder.setSectioners(List.of(
1543                 new PackageSectioner("pkg", 1),
1544                 new PackageSectioner("pkg", 1),
1545                 new PackageSectioner("pkg", 3),
1546                 new PackageSectioner("pkg", 2)
1547         ));
1548     }
1549 
1550     @Test(expected = IllegalStateException.class)
testNonContiguousSections()1551     public void testNonContiguousSections() {
1552         mListBuilder.setSectioners(List.of(
1553                 new PackageSectioner("pkg", 1),
1554                 new PackageSectioner("pkg", 1),
1555                 new PackageSectioner("pkg", 3),
1556                 new PackageSectioner("pkg", 1)
1557         ));
1558     }
1559 
1560     @Test(expected = IllegalStateException.class)
testBucketZeroNotAllowed()1561     public void testBucketZeroNotAllowed() {
1562         mListBuilder.setSectioners(List.of(
1563                 new PackageSectioner("pkg", 0),
1564                 new PackageSectioner("pkg", 1)
1565         ));
1566     }
1567 
1568     @Test
testStabilizeGroupsDelayedSummaryRendersAllNotifsTopLevel()1569     public void testStabilizeGroupsDelayedSummaryRendersAllNotifsTopLevel() {
1570         // GIVEN group children posted without a summary
1571         addGroupChild(0, PACKAGE_1, GROUP_1);
1572         addGroupChild(1, PACKAGE_1, GROUP_1);
1573         addGroupChild(2, PACKAGE_1, GROUP_1);
1574         addGroupChild(3, PACKAGE_1, GROUP_1);
1575 
1576         dispatchBuild();
1577 
1578         // GIVEN visual stability manager doesn't allow any group changes
1579         mStabilityManager.setAllowGroupChanges(false);
1580 
1581         // WHEN the delayed summary is posted
1582         addGroupSummary(4, PACKAGE_1, GROUP_1);
1583 
1584         dispatchBuild();
1585 
1586         // THEN all entries are top-level, but summary is suppressed
1587         verifyBuiltList(
1588                 notif(0),
1589                 notif(1),
1590                 notif(2),
1591                 notif(3)
1592         );
1593 
1594         // WHEN visual stability manager allows group changes again
1595         mStabilityManager.setAllowGroupChanges(true);
1596         mStabilityManager.invalidateList(null);
1597         mPipelineChoreographer.runIfScheduled();
1598 
1599         // THEN entries are grouped
1600         verifyBuiltList(
1601                 group(
1602                         summary(4),
1603                         child(0),
1604                         child(1),
1605                         child(2),
1606                         child(3)
1607                 )
1608         );
1609     }
1610 
1611     @Test
testStabilizeSectionDisallowsNewSection()1612     public void testStabilizeSectionDisallowsNewSection() {
1613         // GIVEN one non-default sections
1614         final NotifSectioner originalSectioner = new PackageSectioner(PACKAGE_1);
1615         mListBuilder.setSectioners(List.of(originalSectioner));
1616 
1617         // GIVEN notifications that's sectioned by sectioner1
1618         addNotif(0, PACKAGE_1);
1619         dispatchBuild();
1620         assertEquals(originalSectioner, mEntrySet.get(0).getSection().getSectioner());
1621 
1622         // WHEN section changes aren't allowed
1623         mStabilityManager.setAllowSectionChanges(false);
1624 
1625         // WHEN we try to change the section
1626         final NotifSectioner newSectioner = new PackageSectioner(PACKAGE_1);
1627         mListBuilder.setSectioners(List.of(newSectioner, originalSectioner));
1628         dispatchBuild();
1629 
1630         // THEN the section remains the same since section changes aren't allowed
1631         assertEquals(originalSectioner, mEntrySet.get(0).getSection().getSectioner());
1632 
1633         // WHEN section changes are allowed again
1634         mStabilityManager.setAllowSectionChanges(true);
1635         mStabilityManager.invalidateList(null);
1636         mPipelineChoreographer.runIfScheduled();
1637 
1638         // THEN the section updates
1639         assertEquals(newSectioner, mEntrySet.get(0).getSection().getSectioner());
1640     }
1641 
1642     @Test
testDispatchListOnBeforeSort()1643     public void testDispatchListOnBeforeSort() {
1644         // GIVEN a registered OnBeforeSortListener
1645         RecordingOnBeforeSortListener listener =
1646                 new RecordingOnBeforeSortListener();
1647         mListBuilder.addOnBeforeSortListener(listener);
1648         mListBuilder.setComparators(singletonList(new HypeComparator(PACKAGE_3)));
1649 
1650         // GIVEN some new notifs out of order
1651         addNotif(0, PACKAGE_1);
1652         addNotif(1, PACKAGE_2);
1653         addNotif(2, PACKAGE_3);
1654 
1655         // WHEN we run the pipeline
1656         dispatchBuild();
1657 
1658         // THEN all the new notifs are passed to the listener out of order
1659         assertThat(listener.mEntriesReceived).containsExactly(
1660                 mEntrySet.get(0),
1661                 mEntrySet.get(1),
1662                 mEntrySet.get(2)
1663         ).inOrder();  // Checking out-of-order input to validate sorted output
1664 
1665         // THEN the final list is in order
1666         verifyBuiltList(
1667                 notif(2),
1668                 notif(0),
1669                 notif(1)
1670         );
1671     }
1672 
1673     @Test
testDispatchListOnBeforeRender()1674     public void testDispatchListOnBeforeRender() {
1675         // GIVEN a registered OnBeforeRenderList
1676         RecordingOnBeforeRenderListener listener =
1677                 new RecordingOnBeforeRenderListener();
1678         mListBuilder.addOnBeforeRenderListListener(listener);
1679 
1680         // GIVEN some new notifs out of order
1681         addNotif(0, PACKAGE_1);
1682         addNotif(1, PACKAGE_2);
1683         addNotif(2, PACKAGE_3);
1684 
1685         // WHEN we run the pipeline
1686         dispatchBuild();
1687 
1688         // THEN all the new notifs are passed to the listener
1689         assertThat(listener.mEntriesReceived).containsExactly(
1690                 mEntrySet.get(0),
1691                 mEntrySet.get(1),
1692                 mEntrySet.get(2)
1693         ).inOrder();
1694     }
1695 
1696     @Test
testAnnulledGroupsHaveParentSetProperly()1697     public void testAnnulledGroupsHaveParentSetProperly() {
1698         // GIVEN a list containing a small group that's already been built once
1699         addGroupChild(0, PACKAGE_2, GROUP_2);
1700         addGroupSummary(1, PACKAGE_2, GROUP_2);
1701         addGroupChild(2, PACKAGE_2, GROUP_2);
1702         dispatchBuild();
1703 
1704         verifyBuiltList(
1705                 group(
1706                         summary(1),
1707                         child(0),
1708                         child(2)
1709                 )
1710         );
1711         GroupEntry group = (GroupEntry) mBuiltList.get(0);
1712 
1713         // WHEN a child is removed such that the group is no longer big enough
1714         mEntrySet.remove(2);
1715         dispatchBuild();
1716 
1717         // THEN the group is annulled and its parent is set back to null
1718         verifyBuiltList(
1719                 notif(0)
1720         );
1721         assertNull(group.getParent());
1722 
1723         // but its previous parent indicates that it was added in the previous iteration
1724         assertEquals(GroupEntry.ROOT_ENTRY, group.getPreviousParent());
1725     }
1726 
1727     static class CountingInvalidator {
CountingInvalidator(Pluggable pluggableToInvalidate)1728         CountingInvalidator(Pluggable pluggableToInvalidate) {
1729             mPluggableToInvalidate = pluggableToInvalidate;
1730             mInvalidationCount = 0;
1731         }
1732 
setInvalidationCount(int invalidationCount)1733         public void setInvalidationCount(int invalidationCount) {
1734             mInvalidationCount = invalidationCount;
1735         }
1736 
maybeInvalidate()1737         public void maybeInvalidate() {
1738             if (mInvalidationCount > 0) {
1739                 mPluggableToInvalidate.invalidateList("test invalidation");
1740                 mInvalidationCount--;
1741             }
1742         }
1743 
1744         private Pluggable mPluggableToInvalidate;
1745         private int mInvalidationCount;
1746 
1747         private static final String TAG = "ShadeListBuilderTestCountingInvalidator";
1748     }
1749 
1750     @Test
testOutOfOrderPreGroupFilterInvalidationDoesNotThrowBeforeTooManyRuns()1751     public void testOutOfOrderPreGroupFilterInvalidationDoesNotThrowBeforeTooManyRuns() {
1752         // GIVEN a PreGroupNotifFilter that gets invalidated during the grouping stage,
1753         NotifFilter filter = new PackageFilter(PACKAGE_1);
1754         CountingInvalidator invalidator = new CountingInvalidator(filter);
1755         OnBeforeTransformGroupsListener listener = (list) -> invalidator.maybeInvalidate();
1756         mListBuilder.addPreGroupFilter(filter);
1757         mListBuilder.addOnBeforeTransformGroupsListener(listener);
1758 
1759         interceptWtfs();
1760 
1761         // WHEN we try to run the pipeline and the filter is invalidated exactly
1762         // MAX_CONSECUTIVE_REENTRANT_REBUILDS times,
1763         addNotif(0, PACKAGE_2);
1764         invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
1765         dispatchBuild();
1766         runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
1767 
1768         // THEN an exception is NOT thrown directly, but a WTF IS logged.
1769         expectWtfs(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
1770     }
1771 
1772     @Test(expected = IllegalStateException.class)
testOutOfOrderPreGroupFilterInvalidationThrowsAfterTooManyRuns()1773     public void testOutOfOrderPreGroupFilterInvalidationThrowsAfterTooManyRuns() {
1774         // GIVEN a PreGroupNotifFilter that gets invalidated during the grouping stage,
1775         NotifFilter filter = new PackageFilter(PACKAGE_1);
1776         CountingInvalidator invalidator = new CountingInvalidator(filter);
1777         OnBeforeTransformGroupsListener listener = (list) -> invalidator.maybeInvalidate();
1778         mListBuilder.addPreGroupFilter(filter);
1779         mListBuilder.addOnBeforeTransformGroupsListener(listener);
1780 
1781         interceptWtfs();
1782 
1783         // WHEN we try to run the pipeline and the filter is invalidated more than
1784         // MAX_CONSECUTIVE_REENTRANT_REBUILDS times,
1785         addNotif(0, PACKAGE_2);
1786         invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1);
1787         dispatchBuild();
1788         try {
1789             runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
1790         } finally {
1791             expectWtfs(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
1792         }
1793 
1794         // THEN an exception IS thrown.
1795     }
1796 
1797     @Test
testNonConsecutiveOutOfOrderInvalidationsDontThrowAfterTooManyRuns()1798     public void testNonConsecutiveOutOfOrderInvalidationsDontThrowAfterTooManyRuns() {
1799         // GIVEN a PreGroupNotifFilter that gets invalidated during the grouping stage,
1800         NotifFilter filter = new PackageFilter(PACKAGE_1);
1801         CountingInvalidator invalidator = new CountingInvalidator(filter);
1802         OnBeforeTransformGroupsListener listener = (list) -> invalidator.maybeInvalidate();
1803         mListBuilder.addPreGroupFilter(filter);
1804         mListBuilder.addOnBeforeTransformGroupsListener(listener);
1805 
1806         interceptWtfs();
1807 
1808         // WHEN we try to run the pipeline and the filter is invalidated
1809         // MAX_CONSECUTIVE_REENTRANT_REBUILDS times, the pipeline runs for a non-reentrant reason,
1810         // and then the filter is invalidated MAX_CONSECUTIVE_REENTRANT_REBUILDS times again,
1811         addNotif(0, PACKAGE_2);
1812         invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
1813         dispatchBuild();
1814         runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
1815         invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
1816         // Note: dispatchBuild itself triggers a non-reentrant pipeline run.
1817         dispatchBuild();
1818         runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
1819 
1820         // THEN an exception is NOT thrown, but WTFs ARE logged.
1821         expectWtfs(MAX_CONSECUTIVE_REENTRANT_REBUILDS * 2);
1822     }
1823 
1824     @Test
testOutOfOrderPrompterInvalidationDoesNotThrowBeforeTooManyRuns()1825     public void testOutOfOrderPrompterInvalidationDoesNotThrowBeforeTooManyRuns() {
1826         // GIVEN a NotifPromoter that gets invalidated during the sorting stage,
1827         NotifPromoter promoter = new IdPromoter(47);
1828         CountingInvalidator invalidator = new CountingInvalidator(promoter);
1829         OnBeforeSortListener listener = (list) -> invalidator.maybeInvalidate();
1830         mListBuilder.addPromoter(promoter);
1831         mListBuilder.addOnBeforeSortListener(listener);
1832 
1833         interceptWtfs();
1834 
1835         // WHEN we try to run the pipeline and the promoter is invalidated exactly
1836         // MAX_CONSECUTIVE_REENTRANT_REBUILDS times,
1837         addNotif(0, PACKAGE_1);
1838         invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
1839         dispatchBuild();
1840         runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
1841 
1842         // THEN an exception is NOT thrown directly, but a WTF IS logged.
1843         expectWtfs(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
1844 
1845     }
1846 
1847     @Test(expected = IllegalStateException.class)
testOutOfOrderPrompterInvalidationThrowsAfterTooManyRuns()1848     public void testOutOfOrderPrompterInvalidationThrowsAfterTooManyRuns() {
1849         // GIVEN a NotifPromoter that gets invalidated during the sorting stage,
1850         NotifPromoter promoter = new IdPromoter(47);
1851         CountingInvalidator invalidator = new CountingInvalidator(promoter);
1852         OnBeforeSortListener listener = (list) -> invalidator.maybeInvalidate();
1853         mListBuilder.addPromoter(promoter);
1854         mListBuilder.addOnBeforeSortListener(listener);
1855 
1856         interceptWtfs();
1857 
1858         // WHEN we try to run the pipeline and the promoter is invalidated more than
1859         // MAX_CONSECUTIVE_REENTRANT_REBUILDS times,
1860         addNotif(0, PACKAGE_1);
1861         invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1);
1862         dispatchBuild();
1863         try {
1864             runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
1865         } finally {
1866             expectWtfs(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
1867         }
1868 
1869         // THEN an exception IS thrown.
1870     }
1871 
1872     @Test
testOutOfOrderComparatorInvalidationDoesNotThrowBeforeTooManyRuns()1873     public void testOutOfOrderComparatorInvalidationDoesNotThrowBeforeTooManyRuns() {
1874         // GIVEN a NotifComparator that gets invalidated during the finalizing stage,
1875         NotifComparator comparator = new HypeComparator(PACKAGE_1);
1876         CountingInvalidator invalidator = new CountingInvalidator(comparator);
1877         OnBeforeRenderListListener listener = (list) -> invalidator.maybeInvalidate();
1878         mListBuilder.setComparators(singletonList(comparator));
1879         mListBuilder.addOnBeforeRenderListListener(listener);
1880 
1881         interceptWtfs();
1882 
1883         // WHEN we try to run the pipeline and the comparator is invalidated exactly
1884         // MAX_CONSECUTIVE_REENTRANT_REBUILDS times,
1885         addNotif(0, PACKAGE_2);
1886         invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
1887         dispatchBuild();
1888         runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
1889 
1890         // THEN an exception is NOT thrown directly, but a WTF IS logged.
1891         expectWtfs(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
1892     }
1893 
1894     @Test(expected = IllegalStateException.class)
testOutOfOrderComparatorInvalidationThrowsAfterTooManyRuns()1895     public void testOutOfOrderComparatorInvalidationThrowsAfterTooManyRuns() {
1896         // GIVEN a NotifComparator that gets invalidated during the finalizing stage,
1897         NotifComparator comparator = new HypeComparator(PACKAGE_1);
1898         CountingInvalidator invalidator = new CountingInvalidator(comparator);
1899         OnBeforeRenderListListener listener = (list) -> invalidator.maybeInvalidate();
1900         mListBuilder.setComparators(singletonList(comparator));
1901         mListBuilder.addOnBeforeRenderListListener(listener);
1902 
1903         interceptWtfs();
1904 
1905         // WHEN we try to run the pipeline and the comparator is invalidated more than
1906         // MAX_CONSECUTIVE_REENTRANT_REBUILDS times,
1907         addNotif(0, PACKAGE_2);
1908         invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1);
1909         dispatchBuild();
1910         runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
1911 
1912         // THEN an exception IS thrown.
1913     }
1914 
1915     @Test
testOutOfOrderPreRenderFilterInvalidationDoesNotThrowBeforeTooManyRuns()1916     public void testOutOfOrderPreRenderFilterInvalidationDoesNotThrowBeforeTooManyRuns() {
1917         // GIVEN a PreRenderNotifFilter that gets invalidated during the finalizing stage,
1918         NotifFilter filter = new PackageFilter(PACKAGE_1);
1919         CountingInvalidator invalidator = new CountingInvalidator(filter);
1920         OnBeforeRenderListListener listener = (list) -> invalidator.maybeInvalidate();
1921         mListBuilder.addFinalizeFilter(filter);
1922         mListBuilder.addOnBeforeRenderListListener(listener);
1923 
1924         interceptWtfs();
1925 
1926         // WHEN we try to run the pipeline and the PreRenderFilter is invalidated exactly
1927         // MAX_CONSECUTIVE_REENTRANT_REBUILDS times,
1928         addNotif(0, PACKAGE_2);
1929         invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
1930         dispatchBuild();
1931         runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
1932 
1933         // THEN an exception is NOT thrown directly, but a WTF IS logged.
1934         expectWtfs(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
1935     }
1936 
1937     @Test(expected = IllegalStateException.class)
testOutOfOrderPreRenderFilterInvalidationThrowsAfterTooManyRuns()1938     public void testOutOfOrderPreRenderFilterInvalidationThrowsAfterTooManyRuns() {
1939         // GIVEN a PreRenderNotifFilter that gets invalidated during the finalizing stage,
1940         NotifFilter filter = new PackageFilter(PACKAGE_1);
1941         CountingInvalidator invalidator = new CountingInvalidator(filter);
1942         OnBeforeRenderListListener listener = (list) -> invalidator.maybeInvalidate();
1943         mListBuilder.addFinalizeFilter(filter);
1944         mListBuilder.addOnBeforeRenderListListener(listener);
1945 
1946         interceptWtfs();
1947 
1948         // WHEN we try to run the pipeline and the PreRenderFilter is invalidated more than
1949         // MAX_CONSECUTIVE_REENTRANT_REBUILDS times,
1950         addNotif(0, PACKAGE_2);
1951         invalidator.setInvalidationCount(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1);
1952         dispatchBuild();
1953         try {
1954             runWhileScheduledUpTo(MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
1955         } finally {
1956             expectWtfs(MAX_CONSECUTIVE_REENTRANT_REBUILDS);
1957         }
1958 
1959         // THEN an exception IS thrown.
1960     }
1961 
interceptWtfs()1962     private void interceptWtfs() {
1963         assertNull(mOldWtfHandler);
1964 
1965         mLastWtf = null;
1966         mWtfCount = 0;
1967 
1968         mOldWtfHandler = Log.setWtfHandler((tag, e, system) -> {
1969             Log.e("ShadeListBuilderTest", "Observed WTF: " + e);
1970             mLastWtf = e;
1971             mWtfCount++;
1972         });
1973     }
1974 
expectNoWtfs()1975     private void expectNoWtfs() {
1976         assertNull(expectWtfs(0));
1977     }
1978 
expectWtf()1979     private Log.TerribleFailure expectWtf() {
1980         return expectWtfs(1);
1981     }
1982 
expectWtfs(int expectedWtfCount)1983     private Log.TerribleFailure expectWtfs(int expectedWtfCount) {
1984         assertNotNull(mOldWtfHandler);
1985 
1986         Log.setWtfHandler(mOldWtfHandler);
1987         mOldWtfHandler = null;
1988 
1989         Log.TerribleFailure wtf = mLastWtf;
1990         int wtfCount = mWtfCount;
1991 
1992         mLastWtf = null;
1993         mWtfCount = 0;
1994 
1995         assertEquals(expectedWtfCount, wtfCount);
1996         return wtf;
1997     }
1998 
1999     @Test
testStableOrdering()2000     public void testStableOrdering() {
2001         mStabilityManager.setAllowEntryReordering(false);
2002         // No input or output
2003         assertOrder("", "", "", true);
2004         // Remove everything
2005         assertOrder("ABCDEFG", "", "", true);
2006         // Literally no changes
2007         assertOrder("ABCDEFG", "ABCDEFG", "ABCDEFG", true);
2008 
2009         // No stable order
2010         assertOrder("", "ABCDEFG", "ABCDEFG", true);
2011 
2012         // F moved after A, and...
2013         assertOrder("ABCDEFG", "AFBCDEG", "ABCDEFG", false);   // No other changes
2014         assertOrder("ABCDEFG", "AXFBCDEG", "AXBCDEFG", false); // Insert X before F
2015         assertOrder("ABCDEFG", "AFXBCDEG", "AXBCDEFG", false); // Insert X after F
2016         assertOrder("ABCDEFG", "AFBCDEXG", "ABCDEFXG", false); // Insert X where F was
2017 
2018         // B moved after F, and...
2019         assertOrder("ABCDEFG", "ACDEFBG", "ABCDEFG", false);   // No other changes
2020         assertOrder("ABCDEFG", "ACDEFXBG", "ABCDEFXG", false); // Insert X before B
2021         assertOrder("ABCDEFG", "ACDEFBXG", "ABCDEFXG", false); // Insert X after B
2022         assertOrder("ABCDEFG", "AXCDEFBG", "AXBCDEFG", false); // Insert X where B was
2023 
2024         // Swap F and B, and...
2025         assertOrder("ABCDEFG", "AFCDEBG", "ABCDEFG", false);   // No other changes
2026         assertOrder("ABCDEFG", "AXFCDEBG", "AXBCDEFG", false); // Insert X before F
2027         assertOrder("ABCDEFG", "AFXCDEBG", "AXBCDEFG", false); // Insert X after F
2028         assertOrder("ABCDEFG", "AFCXDEBG", "AXBCDEFG", false); // Insert X between CD (or: ABCXDEFG)
2029         assertOrder("ABCDEFG", "AFCDXEBG", "ABCDXEFG", false); // Insert X between DE (or: ABCDEFXG)
2030         assertOrder("ABCDEFG", "AFCDEXBG", "ABCDEFXG", false); // Insert X before B
2031         assertOrder("ABCDEFG", "AFCDEBXG", "ABCDEFXG", false); // Insert X after B
2032 
2033         // Remove a bunch of entries at once
2034         assertOrder("ABCDEFGHIJKL", "ACEGHI", "ACEGHI", true);
2035 
2036         // Remove a bunch of entries and scramble
2037         assertOrder("ABCDEFGHIJKL", "GCEHAI", "ACEGHI", false);
2038 
2039         // Add a bunch of entries at once
2040         assertOrder("ABCDEFG", "AVBWCXDYZEFG", "AVBWCXDYZEFG", true);
2041 
2042         // Add a bunch of entries and reverse originals
2043         // NOTE: Some of these don't have obviously correct answers
2044         assertOrder("ABCDEFG", "GFEBCDAVWXYZ", "ABCDEFGVWXYZ", false); // appended
2045         assertOrder("ABCDEFG", "VWXYZGFEBCDA", "VWXYZABCDEFG", false); // prepended
2046         assertOrder("ABCDEFG", "GFEBVWXYZCDA", "ABCDEFGVWXYZ", false); // closer to back: append
2047         assertOrder("ABCDEFG", "GFEVWXYZBCDA", "VWXYZABCDEFG", false); // closer to front: prepend
2048         assertOrder("ABCDEFG", "GFEVWBXYZCDA", "VWABCDEFGXYZ", false); // split new entries
2049 
2050         // Swap 2 pairs ("*BC*NO*"->"*NO*CB*"), remove EG, add UVWXYZ throughout
2051         assertOrder("ABCDEFGHIJKLMNOP", "AUNOVDFHWXIJKLMYCBZP", "AUVBCDFHWXIJKLMNOYZP", false);
2052     }
2053 
2054     @Test
testActiveOrdering()2055     public void testActiveOrdering() {
2056         assertOrder("ABCDEFG", "ACDEFXBG", "ACDEFXBG", true); // X
2057         assertOrder("ABCDEFG", "ACDEFBG", "ACDEFBG", true); // no change
2058         assertOrder("ABCDEFG", "ACDEFBXZG", "ACDEFBXZG", true); // Z and X
2059         assertOrder("ABCDEFG", "AXCDEZFBG", "AXCDEZFBG", true); // Z and X + gap
2060     }
2061 
2062     @Test
testStableMultipleSectionOrdering()2063     public void testStableMultipleSectionOrdering() {
2064         // WHEN the list is originally built with reordering disabled
2065         mListBuilder.setSectioners(asList(
2066                 new PackageSectioner(PACKAGE_1), new PackageSectioner(PACKAGE_2)));
2067         mStabilityManager.setAllowEntryReordering(false);
2068 
2069         addNotif(0, PACKAGE_1).setRank(1);
2070         addNotif(1, PACKAGE_1).setRank(2);
2071         addNotif(2, PACKAGE_2).setRank(0);
2072         addNotif(3, PACKAGE_1).setRank(3);
2073         dispatchBuild();
2074 
2075         // VERIFY the order and that entry reordering has not been suppressed
2076         verifyBuiltList(
2077                 notif(0),
2078                 notif(1),
2079                 notif(3),
2080                 notif(2)
2081         );
2082         verify(mStabilityManager, never()).onEntryReorderSuppressed();
2083 
2084         // WHEN the ranks change
2085         setNewRank(notif(0).entry, 4);
2086         dispatchBuild();
2087 
2088         // VERIFY the order does not change that entry reordering has been suppressed
2089         verifyBuiltList(
2090                 notif(0),
2091                 notif(1),
2092                 notif(3),
2093                 notif(2)
2094         );
2095         verify(mStabilityManager).onEntryReorderSuppressed();
2096 
2097         // WHEN reordering is now allowed again
2098         mStabilityManager.setAllowEntryReordering(true);
2099         dispatchBuild();
2100 
2101         // VERIFY that list order changes
2102         verifyBuiltList(
2103                 notif(1),
2104                 notif(3),
2105                 notif(0),
2106                 notif(2)
2107         );
2108     }
2109 
2110     @Test
stableOrderingDisregardedWithSectionChange()2111     public void stableOrderingDisregardedWithSectionChange() {
2112         // GIVEN the first sectioner's packages can be changed from run-to-run
2113         List<String> mutableSectionerPackages = new ArrayList<>();
2114         mutableSectionerPackages.add(PACKAGE_1);
2115         mListBuilder.setSectioners(asList(
2116                 new PackageSectioner(mutableSectionerPackages, null),
2117                 new PackageSectioner(List.of(PACKAGE_1, PACKAGE_2, PACKAGE_3), null)));
2118         mStabilityManager.setAllowEntryReordering(false);
2119 
2120         // WHEN the list is originally built with reordering disabled (and section changes allowed)
2121         addNotif(0, PACKAGE_1).setRank(4);
2122         addNotif(1, PACKAGE_1).setRank(5);
2123         addNotif(2, PACKAGE_2).setRank(1);
2124         addNotif(3, PACKAGE_2).setRank(2);
2125         addNotif(4, PACKAGE_3).setRank(3);
2126         dispatchBuild();
2127 
2128         // VERIFY the order and that entry reordering has not been suppressed
2129         verifyBuiltList(
2130                 notif(0),
2131                 notif(1),
2132                 notif(2),
2133                 notif(3),
2134                 notif(4)
2135         );
2136         verify(mStabilityManager, never()).onEntryReorderSuppressed();
2137 
2138         // WHEN the first section now claims PACKAGE_3 notifications
2139         mutableSectionerPackages.add(PACKAGE_3);
2140         dispatchBuild();
2141 
2142         // VERIFY the re-sectioned notification is inserted at #1 of the first section, which
2143         // is the correct position based on its rank, rather than #3 in the new section simply
2144         // because it was #3 in its previous section.
2145         verifyBuiltList(
2146                 notif(4),
2147                 notif(0),
2148                 notif(1),
2149                 notif(2),
2150                 notif(3)
2151         );
2152         verify(mStabilityManager, never()).onEntryReorderSuppressed();
2153     }
2154 
2155     @Test
testStableChildOrdering()2156     public void testStableChildOrdering() {
2157         // WHEN the list is originally built with reordering disabled
2158         mStabilityManager.setAllowEntryReordering(false);
2159         addGroupSummary(0, PACKAGE_1, GROUP_1).setRank(0);
2160         addGroupChild(1, PACKAGE_1, GROUP_1).setRank(1);
2161         addGroupChild(2, PACKAGE_1, GROUP_1).setRank(2);
2162         addGroupChild(3, PACKAGE_1, GROUP_1).setRank(3);
2163         dispatchBuild();
2164 
2165         // VERIFY the order and that entry reordering has not been suppressed
2166         verifyBuiltList(
2167                 group(
2168                         summary(0),
2169                         child(1),
2170                         child(2),
2171                         child(3)
2172                 )
2173         );
2174         verify(mStabilityManager, never()).onEntryReorderSuppressed();
2175 
2176         // WHEN the ranks change
2177         setNewRank(notif(2).entry, 5);
2178         dispatchBuild();
2179 
2180         // VERIFY the order does not change that entry reordering has been suppressed
2181         verifyBuiltList(
2182                 group(
2183                         summary(0),
2184                         child(1),
2185                         child(2),
2186                         child(3)
2187                 )
2188         );
2189         verify(mStabilityManager).onEntryReorderSuppressed();
2190 
2191         // WHEN reordering is now allowed again
2192         mStabilityManager.setAllowEntryReordering(true);
2193         dispatchBuild();
2194 
2195         // VERIFY that list order changes
2196         verifyBuiltList(
2197                 group(
2198                         summary(0),
2199                         child(1),
2200                         child(3),
2201                         child(2)
2202                 )
2203         );
2204     }
2205 
2206     @Test
groupRevertingToSummaryRetainsStablePosition()2207     public void groupRevertingToSummaryRetainsStablePosition() {
2208         // GIVEN a notification group is on screen
2209         mStabilityManager.setAllowEntryReordering(false);
2210 
2211         // WHEN the list is originally built with reordering disabled (and section changes allowed)
2212         addNotif(0, PACKAGE_1).setRank(2);
2213         addNotif(1, PACKAGE_1).setRank(3);
2214         addGroupSummary(2, PACKAGE_1, "group").setRank(4);
2215         addGroupChild(3, PACKAGE_1, "group").setRank(5);
2216         addGroupChild(4, PACKAGE_1, "group").setRank(6);
2217         dispatchBuild();
2218 
2219         verifyBuiltList(
2220                 notif(0),
2221                 notif(1),
2222                 group(
2223                         summary(2),
2224                         child(3),
2225                         child(4)
2226                 )
2227         );
2228 
2229         // WHEN the notification summary rank increases and children removed
2230         setNewRank(notif(2).entry, 1);
2231         mEntrySet.remove(4);
2232         mEntrySet.remove(3);
2233         dispatchBuild();
2234 
2235         // VERIFY the summary stays in the same location on rebuild
2236         verifyBuiltList(
2237                 notif(0),
2238                 notif(1),
2239                 notif(2)
2240         );
2241     }
2242 
setNewRank(NotificationEntry entry, int rank)2243     private static void setNewRank(NotificationEntry entry, int rank) {
2244         entry.setRanking(new RankingBuilder(entry.getRanking()).setRank(rank).build());
2245     }
2246 
2247     @Test
testInOrderPreRenderFilter()2248     public void testInOrderPreRenderFilter() {
2249         // GIVEN a PreRenderFilter that gets invalidated during the grouping stage
2250         NotifFilter filter = new PackageFilter(PACKAGE_5);
2251         OnBeforeTransformGroupsListener listener = (list) -> filter.invalidateList(null);
2252         mListBuilder.addFinalizeFilter(filter);
2253         mListBuilder.addOnBeforeTransformGroupsListener(listener);
2254 
2255         // WHEN we try to run the pipeline and the filter is invalidated
2256         addNotif(0, PACKAGE_1);
2257         dispatchBuild();
2258 
2259         // THEN no exception thrown
2260     }
2261 
2262     @Test
testPipelineRunDisallowedDueToVisualStability()2263     public void testPipelineRunDisallowedDueToVisualStability() {
2264         // GIVEN pipeline run not allowed due to visual stability
2265         mStabilityManager.setAllowPipelineRun(false);
2266 
2267         // WHEN we try to run the pipeline with a change
2268         addNotif(0, PACKAGE_1);
2269         dispatchBuild();
2270 
2271         // THEN there is no change; the pipeline did not run
2272         verifyBuiltList();
2273     }
2274 
2275     @Test
testMultipleInvalidationsCoalesce()2276     public void testMultipleInvalidationsCoalesce() {
2277         // GIVEN a PreGroupFilter and a FinalizeFilter
2278         NotifFilter filter1 = new PackageFilter(PACKAGE_5);
2279         NotifFilter filter2 = new PackageFilter(PACKAGE_0);
2280         mListBuilder.addPreGroupFilter(filter1);
2281         mListBuilder.addFinalizeFilter(filter2);
2282 
2283         // WHEN both filters invalidate
2284         filter1.invalidateList(null);
2285         filter2.invalidateList(null);
2286 
2287         // THEN the pipeline choreographer is scheduled to evaluate, AND the pipeline hasn't
2288         // actually run.
2289         assertTrue(mPipelineChoreographer.isScheduled());
2290         verify(mOnRenderListListener, never()).onRenderList(anyList());
2291 
2292         // WHEN the pipeline choreographer actually runs
2293         mPipelineChoreographer.runIfScheduled();
2294 
2295         // THEN the pipeline runs
2296         verify(mOnRenderListListener).onRenderList(anyList());
2297     }
2298 
2299     @Test
testIsSorted()2300     public void testIsSorted() {
2301         Comparator<Integer> intCmp = Integer::compare;
2302         assertTrue(ShadeListBuilder.isSorted(Collections.emptyList(), intCmp));
2303         assertTrue(ShadeListBuilder.isSorted(Collections.singletonList(1), intCmp));
2304         assertTrue(ShadeListBuilder.isSorted(Arrays.asList(1, 2), intCmp));
2305         assertTrue(ShadeListBuilder.isSorted(Arrays.asList(1, 2, 3), intCmp));
2306         assertTrue(ShadeListBuilder.isSorted(Arrays.asList(1, 2, 3, 4), intCmp));
2307         assertTrue(ShadeListBuilder.isSorted(Arrays.asList(1, 2, 3, 4, 5), intCmp));
2308         assertTrue(ShadeListBuilder.isSorted(Arrays.asList(1, 1, 1, 1, 1), intCmp));
2309         assertTrue(ShadeListBuilder.isSorted(Arrays.asList(1, 1, 2, 2, 3, 3), intCmp));
2310 
2311         assertFalse(ShadeListBuilder.isSorted(Arrays.asList(2, 1), intCmp));
2312         assertFalse(ShadeListBuilder.isSorted(Arrays.asList(2, 1, 2), intCmp));
2313         assertFalse(ShadeListBuilder.isSorted(Arrays.asList(1, 2, 1), intCmp));
2314         assertFalse(ShadeListBuilder.isSorted(Arrays.asList(1, 2, 3, 2, 5), intCmp));
2315         assertFalse(ShadeListBuilder.isSorted(Arrays.asList(5, 2, 3, 4, 5), intCmp));
2316         assertFalse(ShadeListBuilder.isSorted(Arrays.asList(1, 2, 3, 4, 1), intCmp));
2317     }
2318 
2319     /**
2320      * Adds a notif to the collection that will be passed to the list builder when
2321      * {@link #dispatchBuild()}s is called.
2322      *
2323      * @param index Index of this notification in the set. This must be the current size of the set.
2324      *              it exists to improve readability of the resulting code, since later tests will
2325      *              have to refer to notifs by index.
2326      * @param packageId Package that the notif should be posted under
2327      * @return A NotificationEntryBuilder that can be used to further modify the notif. Do not call
2328      *         build() on the builder; that will be done on the next dispatchBuild().
2329      */
addNotif(int index, String packageId)2330     private NotificationEntryBuilder addNotif(int index, String packageId) {
2331         final NotificationEntryBuilder builder = new NotificationEntryBuilder()
2332                 .setPkg(packageId)
2333                 .setId(nextId(packageId))
2334                 .setRank(nextRank());
2335 
2336         builder.modifyNotification(mContext)
2337                 .setContentTitle("Top level singleton")
2338                 .setChannelId("test_channel");
2339 
2340         assertEquals(mEntrySet.size() + mPendingSet.size(), index);
2341         mPendingSet.add(builder);
2342         return builder;
2343     }
2344 
2345     /** Same behavior as {@link #addNotif(int, String)}. */
addGroupSummary(int index, String packageId, String groupId)2346     private NotificationEntryBuilder addGroupSummary(int index, String packageId, String groupId) {
2347         final NotificationEntryBuilder builder = new NotificationEntryBuilder()
2348                 .setPkg(packageId)
2349                 .setId(nextId(packageId))
2350                 .setRank(nextRank());
2351 
2352         builder.modifyNotification(mContext)
2353                 .setChannelId("test_channel")
2354                 .setContentTitle("Group summary")
2355                 .setGroup(groupId)
2356                 .setGroupSummary(true);
2357 
2358         assertEquals(mEntrySet.size() + mPendingSet.size(), index);
2359         mPendingSet.add(builder);
2360         return builder;
2361     }
2362 
addGroupChildWithTag(int index, String packageId, String groupId, String tag)2363     private NotificationEntryBuilder addGroupChildWithTag(int index, String packageId,
2364             String groupId, String tag) {
2365         final NotificationEntryBuilder builder = new NotificationEntryBuilder()
2366                 .setTag(tag)
2367                 .setPkg(packageId)
2368                 .setId(nextId(packageId))
2369                 .setRank(nextRank());
2370 
2371         builder.modifyNotification(mContext)
2372                 .setChannelId("test_channel")
2373                 .setContentTitle("Group child")
2374                 .setGroup(groupId);
2375 
2376         assertEquals(mEntrySet.size() + mPendingSet.size(), index);
2377         mPendingSet.add(builder);
2378         return builder;
2379     }
2380 
2381     /** Same behavior as {@link #addNotif(int, String)}. */
addGroupChild(int index, String packageId, String groupId)2382     private NotificationEntryBuilder addGroupChild(int index, String packageId, String groupId) {
2383         return addGroupChildWithTag(index, packageId, groupId, null);
2384     }
2385 
assertOrder(String visible, String active, String expected, boolean isOrderedCorrectly)2386     private void assertOrder(String visible, String active, String expected,
2387             boolean isOrderedCorrectly) {
2388         StringBuilder differenceSb = new StringBuilder();
2389         NotifSection section = new NotifSection(mock(NotifSectioner.class), 0);
2390         for (char c : active.toCharArray()) {
2391             if (visible.indexOf(c) < 0) differenceSb.append(c);
2392         }
2393         String difference = differenceSb.toString();
2394 
2395         int globalIndex = 0;
2396         for (int i = 0; i < visible.length(); i++) {
2397             final char c = visible.charAt(i);
2398             // Skip notifications which aren't active anymore
2399             if (!active.contains(String.valueOf(c))) continue;
2400             addNotif(globalIndex++, String.valueOf(c))
2401                     .setRank(active.indexOf(c))
2402                     .setSection(section)
2403                     .setStableIndex(i);
2404         }
2405 
2406         for (char c : difference.toCharArray()) {
2407             addNotif(globalIndex++, String.valueOf(c))
2408                     .setRank(active.indexOf(c))
2409                     .setSection(section)
2410                     .setStableIndex(-1);
2411         }
2412 
2413         clearInvocations(mStabilityManager);
2414 
2415         dispatchBuild();
2416         StringBuilder resultSb = new StringBuilder();
2417         for (int i = 0; i < expected.length(); i++) {
2418             resultSb.append(mBuiltList.get(i).getRepresentativeEntry().getSbn().getPackageName());
2419         }
2420 
2421         assertEquals("visible [" + visible + "] active [" + active + "]",
2422                 expected, resultSb.toString());
2423         mEntrySet.clear();
2424 
2425         verify(mStabilityManager, isOrderedCorrectly ? never() : times(1))
2426                 .onEntryReorderSuppressed();
2427     }
2428 
nextId(String packageName)2429     private int nextId(String packageName) {
2430         Integer nextId = mNextIdMap.get(packageName);
2431         if (nextId == null) {
2432             nextId = 0;
2433         }
2434         mNextIdMap.put(packageName, nextId + 1);
2435         return nextId;
2436     }
2437 
nextRank()2438     private int nextRank() {
2439         int nextRank = mNextRank;
2440         mNextRank++;
2441         return nextRank;
2442     }
2443 
dispatchBuild()2444     private void dispatchBuild() {
2445         if (mPendingSet.size() > 0) {
2446             for (NotificationEntryBuilder builder : mPendingSet) {
2447                 mEntrySet.add(builder.build());
2448             }
2449             mPendingSet.clear();
2450         }
2451 
2452         mReadyForBuildListener.onBuildList(mEntrySet, "test");
2453         mPipelineChoreographer.runIfScheduled();
2454     }
2455 
runWhileScheduledUpTo(int maxRuns)2456     private void runWhileScheduledUpTo(int maxRuns) {
2457         int runs = 0;
2458         while (mPipelineChoreographer.isScheduled()) {
2459             if (runs > maxRuns) {
2460                 throw new IndexOutOfBoundsException(
2461                         "Pipeline scheduled itself more than " + maxRuns + "times");
2462             }
2463             runs++;
2464             mPipelineChoreographer.runIfScheduled();
2465         }
2466     }
2467 
verifyBuiltList(ExpectedEntry ....expectedEntries)2468     private void verifyBuiltList(ExpectedEntry ...expectedEntries) {
2469         try {
2470             assertEquals(
2471                     "List is the wrong length",
2472                     expectedEntries.length,
2473                     mBuiltList.size());
2474 
2475             for (int i = 0; i < expectedEntries.length; i++) {
2476                 ListEntry outEntry = mBuiltList.get(i);
2477                 ExpectedEntry expectedEntry = expectedEntries[i];
2478 
2479                 if (expectedEntry instanceof ExpectedNotif) {
2480                     assertEquals(
2481                             "Entry " + i + " isn't a NotifEntry",
2482                             NotificationEntry.class,
2483                             outEntry.getClass());
2484                     assertEquals(
2485                             "Entry " + i + " doesn't match expected value.",
2486                             ((ExpectedNotif) expectedEntry).entry, outEntry);
2487                 } else {
2488                     ExpectedGroup cmpGroup = (ExpectedGroup) expectedEntry;
2489 
2490                     assertEquals(
2491                             "Entry " + i + " isn't a GroupEntry",
2492                             GroupEntry.class,
2493                             outEntry.getClass());
2494 
2495                     GroupEntry outGroup = (GroupEntry) outEntry;
2496 
2497                     assertEquals(
2498                             "Summary notif for entry " + i
2499                                     + " doesn't match expected value",
2500                             cmpGroup.summary,
2501                             outGroup.getSummary());
2502                     assertEquals(
2503                             "Summary notif for entry " + i
2504                                         + " doesn't have proper parent",
2505                             outGroup,
2506                             outGroup.getSummary().getParent());
2507 
2508                     assertEquals("Children for entry " + i,
2509                             cmpGroup.children,
2510                             outGroup.getChildren());
2511 
2512                     for (int j = 0; j < outGroup.getChildren().size(); j++) {
2513                         NotificationEntry child = outGroup.getChildren().get(j);
2514                         assertEquals(
2515                                 "Child " + j + " for entry " + i
2516                                         + " doesn't have proper parent",
2517                                 outGroup,
2518                                 child.getParent());
2519                     }
2520                 }
2521             }
2522         } catch (AssertionError err) {
2523             throw new AssertionError(
2524                     "List under test failed verification:\n" + dumpTree(mBuiltList,
2525                             mInteractionTracker, true, ""), err);
2526         }
2527     }
2528 
notif(int index)2529     private ExpectedNotif notif(int index) {
2530         return new ExpectedNotif(mEntrySet.get(index));
2531     }
2532 
group(ExpectedSummary summary, ExpectedChild...children)2533     private ExpectedGroup group(ExpectedSummary summary, ExpectedChild...children) {
2534         return new ExpectedGroup(
2535                 summary.entry,
2536                 Arrays.stream(children)
2537                         .map(child -> child.entry)
2538                         .collect(Collectors.toList()));
2539     }
2540 
summary(int index)2541     private ExpectedSummary summary(int index) {
2542         return new ExpectedSummary(mEntrySet.get(index));
2543     }
2544 
child(int index)2545     private ExpectedChild child(int index) {
2546         return new ExpectedChild(mEntrySet.get(index));
2547     }
2548 
2549     private abstract static class ExpectedEntry {
2550     }
2551 
2552     private static class ExpectedNotif extends ExpectedEntry {
2553         public final NotificationEntry entry;
2554 
ExpectedNotif(NotificationEntry entry)2555         private ExpectedNotif(NotificationEntry entry) {
2556             this.entry = entry;
2557         }
2558     }
2559 
2560     private static class ExpectedGroup extends ExpectedEntry {
2561         public final NotificationEntry summary;
2562         public final List<NotificationEntry> children;
2563 
ExpectedGroup( NotificationEntry summary, List<NotificationEntry> children)2564         private ExpectedGroup(
2565                 NotificationEntry summary,
2566                 List<NotificationEntry> children) {
2567             this.summary = summary;
2568             this.children = children;
2569         }
2570     }
2571 
2572     private static class ExpectedSummary {
2573         public final NotificationEntry entry;
2574 
ExpectedSummary(NotificationEntry entry)2575         private ExpectedSummary(NotificationEntry entry) {
2576             this.entry = entry;
2577         }
2578     }
2579 
2580     private static class ExpectedChild {
2581         public final NotificationEntry entry;
2582 
ExpectedChild(NotificationEntry entry)2583         private ExpectedChild(NotificationEntry entry) {
2584             this.entry = entry;
2585         }
2586     }
2587 
2588     /** Filters out notifs from a particular package */
2589     private static class PackageFilter extends NotifFilter {
2590         private final String mPackageName;
2591 
2592         private boolean mEnabled = true;
2593 
PackageFilter(String packageName)2594         PackageFilter(String packageName) {
2595             super("PackageFilter");
2596 
2597             mPackageName = packageName;
2598         }
2599 
2600         @Override
shouldFilterOut(NotificationEntry entry, long now)2601         public boolean shouldFilterOut(NotificationEntry entry, long now) {
2602             return mEnabled && entry.getSbn().getPackageName().equals(mPackageName);
2603         }
2604 
setEnabled(boolean enabled)2605         public void setEnabled(boolean enabled) {
2606             mEnabled = enabled;
2607         }
2608     }
2609 
2610     /** Filters out notifications with a particular tag */
2611     private static class NotifFilterWithTag extends NotifFilter {
2612         private final String mTag;
2613 
NotifFilterWithTag(String tag)2614         NotifFilterWithTag(String tag) {
2615             super("NotifFilterWithTag_" + tag);
2616             mTag = tag;
2617         }
2618 
2619         @Override
shouldFilterOut(NotificationEntry entry, long now)2620         public boolean shouldFilterOut(NotificationEntry entry, long now) {
2621             return Objects.equals(entry.getSbn().getTag(), mTag);
2622         }
2623     }
2624 
2625     /** Promotes notifs with particular IDs */
2626     private static class IdPromoter extends NotifPromoter {
2627         private final List<Integer> mIds;
2628 
IdPromoter(Integer... ids)2629         IdPromoter(Integer... ids) {
2630             super("IdPromoter");
2631             mIds = asList(ids);
2632         }
2633 
2634         @Override
shouldPromoteToTopLevel(NotificationEntry child)2635         public boolean shouldPromoteToTopLevel(NotificationEntry child) {
2636             return mIds.contains(child.getSbn().getId());
2637         }
2638     }
2639 
2640     /** Sorts specific notifs above all others. */
2641     private static class HypeComparator extends NotifComparator {
2642 
2643         private final List<String> mPreferredPackages;
2644 
HypeComparator(String ....preferredPackages)2645         HypeComparator(String ...preferredPackages) {
2646             super("HypeComparator");
2647             mPreferredPackages = asList(preferredPackages);
2648         }
2649 
2650         @Override
compare(@onNull ListEntry o1, @NonNull ListEntry o2)2651         public int compare(@NonNull ListEntry o1, @NonNull ListEntry o2) {
2652             boolean contains1 = mPreferredPackages.contains(
2653                     o1.getRepresentativeEntry().getSbn().getPackageName());
2654             boolean contains2 = mPreferredPackages.contains(
2655                     o2.getRepresentativeEntry().getSbn().getPackageName());
2656 
2657             return Boolean.compare(contains2, contains1);
2658         }
2659     }
2660 
2661     /** Represents a section for the passed pkg */
2662     private static class PackageSectioner extends NotifSectioner {
2663         private final List<String> mPackages;
2664         private final NotifComparator mComparator;
2665 
PackageSectioner(List<String> pkgs, NotifComparator comparator)2666         PackageSectioner(List<String> pkgs, NotifComparator comparator) {
2667             super("PackageSection_" + pkgs, 0);
2668             mPackages = pkgs;
2669             mComparator = comparator;
2670         }
2671 
PackageSectioner(String pkg)2672         PackageSectioner(String pkg) {
2673             this(pkg, 0);
2674         }
2675 
PackageSectioner(String pkg, int bucket)2676         PackageSectioner(String pkg, int bucket) {
2677             super("PackageSection_" + pkg, bucket);
2678             mPackages = List.of(pkg);
2679             mComparator = null;
2680         }
2681 
2682         @Nullable
2683         @Override
getComparator()2684         public NotifComparator getComparator() {
2685             return mComparator;
2686         }
2687 
2688         @Override
isInSection(ListEntry entry)2689         public boolean isInSection(ListEntry entry) {
2690             return mPackages.contains(entry.getRepresentativeEntry().getSbn().getPackageName());
2691         }
2692     }
2693 
2694     private static class RecordingOnBeforeTransformGroupsListener
2695             implements OnBeforeTransformGroupsListener {
2696         List<ListEntry> mEntriesReceived;
2697 
2698         @Override
onBeforeTransformGroups(List<ListEntry> list)2699         public void onBeforeTransformGroups(List<ListEntry> list) {
2700             mEntriesReceived = new ArrayList<>(list);
2701         }
2702     }
2703 
2704     private static class RecordingOnBeforeSortListener
2705             implements OnBeforeSortListener {
2706         List<ListEntry> mEntriesReceived;
2707 
2708         @Override
onBeforeSort(List<ListEntry> list)2709         public void onBeforeSort(List<ListEntry> list) {
2710             mEntriesReceived = new ArrayList<>(list);
2711         }
2712     }
2713 
2714     private static class RecordingOnBeforeRenderListener
2715             implements OnBeforeRenderListListener {
2716         List<ListEntry> mEntriesReceived;
2717 
2718         @Override
onBeforeRenderList(List<ListEntry> list)2719         public void onBeforeRenderList(List<ListEntry> list) {
2720             mEntriesReceived = new ArrayList<>(list);
2721         }
2722     }
2723 
2724     private class TestableNotifFilter extends NotifFilter {
2725         ArrayList<Integer> mIndicesToFilter = new ArrayList<>();
2726 
TestableNotifFilter()2727         protected TestableNotifFilter() {
2728             super("TestFilter");
2729         }
2730 
2731         @Override
shouldFilterOut(@onNull NotificationEntry entry, long now)2732         public boolean shouldFilterOut(@NonNull NotificationEntry entry, long now) {
2733             return mIndicesToFilter.stream().anyMatch(i -> notif(i).entry == entry);
2734         }
2735     }
2736 
2737     private static class TestableStabilityManager extends NotifStabilityManager {
2738         boolean mAllowPipelineRun = true;
2739         boolean mAllowGroupChanges = true;
2740         boolean mAllowGroupPruning = true;
2741         boolean mAllowSectionChanges = true;
2742         boolean mAllowEntryReodering = true;
2743 
TestableStabilityManager()2744         TestableStabilityManager() {
2745             super("Test");
2746         }
2747 
setAllowGroupChanges(boolean allowGroupChanges)2748         TestableStabilityManager setAllowGroupChanges(boolean allowGroupChanges) {
2749             mAllowGroupChanges = allowGroupChanges;
2750             return this;
2751         }
2752 
setAllowGroupPruning(boolean allowGroupPruning)2753         TestableStabilityManager setAllowGroupPruning(boolean allowGroupPruning) {
2754             mAllowGroupPruning = allowGroupPruning;
2755             return this;
2756         }
2757 
setAllowSectionChanges(boolean allowSectionChanges)2758         TestableStabilityManager setAllowSectionChanges(boolean allowSectionChanges) {
2759             mAllowSectionChanges = allowSectionChanges;
2760             return this;
2761         }
2762 
setAllowEntryReordering(boolean allowSectionChanges)2763         TestableStabilityManager setAllowEntryReordering(boolean allowSectionChanges) {
2764             mAllowEntryReodering = allowSectionChanges;
2765             return this;
2766         }
2767 
setAllowPipelineRun(boolean allowPipelineRun)2768         TestableStabilityManager setAllowPipelineRun(boolean allowPipelineRun) {
2769             mAllowPipelineRun = allowPipelineRun;
2770             return this;
2771         }
2772 
2773         @Override
isPipelineRunAllowed()2774         public boolean isPipelineRunAllowed() {
2775             return mAllowPipelineRun;
2776         }
2777 
2778         @Override
onBeginRun()2779         public void onBeginRun() {
2780         }
2781 
2782         @Override
isGroupChangeAllowed(@onNull NotificationEntry entry)2783         public boolean isGroupChangeAllowed(@NonNull NotificationEntry entry) {
2784             return mAllowGroupChanges;
2785         }
2786 
2787         @Override
isGroupPruneAllowed(@onNull GroupEntry entry)2788         public boolean isGroupPruneAllowed(@NonNull GroupEntry entry) {
2789             return mAllowGroupPruning;
2790         }
2791 
2792         @Override
isSectionChangeAllowed(@onNull NotificationEntry entry)2793         public boolean isSectionChangeAllowed(@NonNull NotificationEntry entry) {
2794             return mAllowSectionChanges;
2795         }
2796 
2797         @Override
isEntryReorderingAllowed(@onNull ListEntry entry)2798         public boolean isEntryReorderingAllowed(@NonNull ListEntry entry) {
2799             return mAllowEntryReodering;
2800         }
2801 
2802         @Override
isEveryChangeAllowed()2803         public boolean isEveryChangeAllowed() {
2804             return mAllowEntryReodering && mAllowGroupChanges && mAllowSectionChanges;
2805         }
2806 
2807         @Override
onEntryReorderSuppressed()2808         public void onEntryReorderSuppressed() {
2809         }
2810     }
2811 
2812     private static final String PACKAGE_0 = "com.test0";
2813     private static final String PACKAGE_1 = "com.test1";
2814     private static final String PACKAGE_2 = "com.test2";
2815     private static final String PACKAGE_3 = "org.test3";
2816     private static final String PACKAGE_4 = "com.test4";
2817     private static final String PACKAGE_5 = "com.test5";
2818 
2819     private static final String GROUP_1 = "group_1";
2820     private static final String GROUP_2 = "group_2";
2821 }
2822