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