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