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