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 android.app.Notification.FLAG_NO_CLEAR; 20 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; 21 import static android.service.notification.NotificationListenerService.REASON_CANCEL; 22 import static android.service.notification.NotificationListenerService.REASON_CLICK; 23 import static android.service.notification.NotificationStats.DISMISSAL_SHADE; 24 import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL; 25 26 import static com.android.systemui.statusbar.notification.collection.NotifCollection.REASON_NOT_CANCELED; 27 import static com.android.systemui.statusbar.notification.collection.NotifCollection.REASON_UNKNOWN; 28 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.DISMISSED; 29 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.NOT_DISMISSED; 30 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED; 31 32 import static org.junit.Assert.assertEquals; 33 import static org.junit.Assert.assertFalse; 34 import static org.junit.Assert.assertNotEquals; 35 import static org.junit.Assert.assertNotNull; 36 import static org.junit.Assert.assertTrue; 37 import static org.mockito.ArgumentMatchers.any; 38 import static org.mockito.ArgumentMatchers.anyInt; 39 import static org.mockito.ArgumentMatchers.eq; 40 import static org.mockito.Mockito.clearInvocations; 41 import static org.mockito.Mockito.inOrder; 42 import static org.mockito.Mockito.mock; 43 import static org.mockito.Mockito.never; 44 import static org.mockito.Mockito.times; 45 import static org.mockito.Mockito.verify; 46 import static org.mockito.Mockito.when; 47 48 import static java.util.Collections.singletonList; 49 import static java.util.Objects.requireNonNull; 50 51 import android.annotation.Nullable; 52 import android.app.Notification; 53 import android.os.RemoteException; 54 import android.service.notification.NotificationListenerService.Ranking; 55 import android.service.notification.NotificationListenerService.RankingMap; 56 import android.service.notification.StatusBarNotification; 57 import android.testing.AndroidTestingRunner; 58 import android.testing.TestableLooper; 59 import android.util.ArrayMap; 60 import android.util.ArraySet; 61 import android.util.Pair; 62 63 import androidx.test.filters.SmallTest; 64 65 import com.android.internal.statusbar.IStatusBarService; 66 import com.android.internal.statusbar.NotificationVisibility; 67 import com.android.systemui.SysuiTestCase; 68 import com.android.systemui.dump.DumpManager; 69 import com.android.systemui.dump.LogBufferEulogizer; 70 import com.android.systemui.statusbar.FeatureFlags; 71 import com.android.systemui.statusbar.RankingBuilder; 72 import com.android.systemui.statusbar.notification.collection.NoManSimulator.NotifEvent; 73 import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason; 74 import com.android.systemui.statusbar.notification.collection.coalescer.CoalescedEvent; 75 import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer; 76 import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer.BatchableNotificationHandler; 77 import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener; 78 import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats; 79 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; 80 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLogger; 81 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor; 82 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender; 83 import com.android.systemui.util.time.FakeSystemClock; 84 85 import org.junit.Before; 86 import org.junit.Test; 87 import org.junit.runner.RunWith; 88 import org.mockito.ArgumentCaptor; 89 import org.mockito.Captor; 90 import org.mockito.InOrder; 91 import org.mockito.Mock; 92 import org.mockito.MockitoAnnotations; 93 import org.mockito.Spy; 94 95 import java.util.Arrays; 96 import java.util.Collection; 97 import java.util.List; 98 import java.util.Map; 99 100 @SmallTest 101 @RunWith(AndroidTestingRunner.class) 102 @TestableLooper.RunWithLooper 103 public class NotifCollectionTest extends SysuiTestCase { 104 105 @Mock private IStatusBarService mStatusBarService; 106 @Mock private FeatureFlags mFeatureFlags; 107 @Mock private NotifCollectionLogger mLogger; 108 @Mock private LogBufferEulogizer mEulogizer; 109 110 @Mock private GroupCoalescer mGroupCoalescer; 111 @Spy private RecordingCollectionListener mCollectionListener; 112 @Mock private CollectionReadyForBuildListener mBuildListener; 113 114 @Spy private RecordingLifetimeExtender mExtender1 = new RecordingLifetimeExtender("Extender1"); 115 @Spy private RecordingLifetimeExtender mExtender2 = new RecordingLifetimeExtender("Extender2"); 116 @Spy private RecordingLifetimeExtender mExtender3 = new RecordingLifetimeExtender("Extender3"); 117 118 @Spy private RecordingDismissInterceptor mInterceptor1 = new RecordingDismissInterceptor( 119 "Interceptor1"); 120 @Spy private RecordingDismissInterceptor mInterceptor2 = new RecordingDismissInterceptor( 121 "Interceptor2"); 122 @Spy private RecordingDismissInterceptor mInterceptor3 = new RecordingDismissInterceptor( 123 "Interceptor3"); 124 125 @Captor private ArgumentCaptor<BatchableNotificationHandler> mListenerCaptor; 126 @Captor private ArgumentCaptor<NotificationEntry> mEntryCaptor; 127 @Captor private ArgumentCaptor<Collection<NotificationEntry>> mBuildListCaptor; 128 129 private NotifCollection mCollection; 130 private BatchableNotificationHandler mNotifHandler; 131 132 private InOrder mListenerInOrder; 133 134 private NoManSimulator mNoMan; 135 private FakeSystemClock mClock = new FakeSystemClock(); 136 137 @Before setUp()138 public void setUp() { 139 MockitoAnnotations.initMocks(this); 140 allowTestableLooperAsMainThread(); 141 142 when(mFeatureFlags.isNewNotifPipelineRenderingEnabled()).thenReturn(true); 143 when(mFeatureFlags.isNewNotifPipelineEnabled()).thenReturn(true); 144 145 when(mEulogizer.record(any(Exception.class))).thenAnswer(i -> i.getArguments()[0]); 146 147 mListenerInOrder = inOrder(mCollectionListener); 148 149 mCollection = new NotifCollection( 150 mStatusBarService, 151 mClock, 152 mFeatureFlags, 153 mLogger, 154 mEulogizer, 155 mock(DumpManager.class)); 156 mCollection.attach(mGroupCoalescer); 157 mCollection.addCollectionListener(mCollectionListener); 158 mCollection.setBuildListener(mBuildListener); 159 160 // Capture the listener object that the collection registers with the listener service so 161 // we can simulate listener service events in tests below 162 verify(mGroupCoalescer).setNotificationHandler(mListenerCaptor.capture()); 163 mNotifHandler = requireNonNull(mListenerCaptor.getValue()); 164 165 mNoMan = new NoManSimulator(); 166 mNoMan.addListener(mNotifHandler); 167 168 mNotifHandler.onNotificationsInitialized(); 169 } 170 171 @Test testEventDispatchedWhenNotifPosted()172 public void testEventDispatchedWhenNotifPosted() { 173 // WHEN a notification is posted 174 NotifEvent notif1 = mNoMan.postNotif( 175 buildNotif(TEST_PACKAGE, 3) 176 .setRank(4747)); 177 178 // THEN the listener is notified 179 final NotificationEntry entry = mCollectionListener.getEntry(notif1.key); 180 181 mListenerInOrder.verify(mCollectionListener).onEntryInit(entry); 182 mListenerInOrder.verify(mCollectionListener).onEntryAdded(entry); 183 mListenerInOrder.verify(mCollectionListener).onRankingApplied(); 184 185 assertEquals(notif1.key, entry.getKey()); 186 assertEquals(notif1.sbn, entry.getSbn()); 187 assertEquals(notif1.ranking, entry.getRanking()); 188 } 189 190 @Test testEventDispatchedWhenNotifBatchPosted()191 public void testEventDispatchedWhenNotifBatchPosted() { 192 // GIVEN a NotifCollection with one notif already posted 193 mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2) 194 .setGroup(mContext, "group_1") 195 .setContentTitle(mContext, "Old version")); 196 197 clearInvocations(mCollectionListener); 198 clearInvocations(mBuildListener); 199 200 // WHEN three notifications from the same group are posted (one of them an update, two of 201 // them new) 202 NotificationEntry entry1 = buildNotif(TEST_PACKAGE, 1) 203 .setGroup(mContext, "group_1") 204 .build(); 205 NotificationEntry entry2 = buildNotif(TEST_PACKAGE, 2) 206 .setGroup(mContext, "group_1") 207 .setContentTitle(mContext, "New version") 208 .build(); 209 NotificationEntry entry3 = buildNotif(TEST_PACKAGE, 3) 210 .setGroup(mContext, "group_1") 211 .build(); 212 213 mNotifHandler.onNotificationBatchPosted(Arrays.asList( 214 new CoalescedEvent(entry1.getKey(), 0, entry1.getSbn(), entry1.getRanking(), null), 215 new CoalescedEvent(entry2.getKey(), 1, entry2.getSbn(), entry2.getRanking(), null), 216 new CoalescedEvent(entry3.getKey(), 2, entry3.getSbn(), entry3.getRanking(), null) 217 )); 218 219 // THEN onEntryAdded is called on the new ones 220 verify(mCollectionListener, times(2)).onEntryAdded(mEntryCaptor.capture()); 221 222 List<NotificationEntry> capturedAdds = mEntryCaptor.getAllValues(); 223 224 assertEquals(entry1.getSbn(), capturedAdds.get(0).getSbn()); 225 assertEquals(entry1.getRanking(), capturedAdds.get(0).getRanking()); 226 227 assertEquals(entry3.getSbn(), capturedAdds.get(1).getSbn()); 228 assertEquals(entry3.getRanking(), capturedAdds.get(1).getRanking()); 229 230 // THEN onEntryUpdated is called on the middle one 231 verify(mCollectionListener).onEntryUpdated(mEntryCaptor.capture()); 232 NotificationEntry capturedUpdate = mEntryCaptor.getValue(); 233 assertEquals(entry2.getSbn(), capturedUpdate.getSbn()); 234 assertEquals(entry2.getRanking(), capturedUpdate.getRanking()); 235 236 // THEN onBuildList is called only once 237 verifyBuiltList( 238 List.of( 239 capturedAdds.get(0), 240 capturedAdds.get(1), 241 capturedUpdate)); 242 } 243 244 @Test testEventDispatchedWhenNotifUpdated()245 public void testEventDispatchedWhenNotifUpdated() { 246 // GIVEN a collection with one notif 247 mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3) 248 .setRank(4747)); 249 250 // WHEN the notif is reposted 251 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3) 252 .setRank(89)); 253 254 // THEN the listener is notified 255 final NotificationEntry entry = mCollectionListener.getEntry(notif2.key); 256 257 mListenerInOrder.verify(mCollectionListener).onEntryUpdated(entry); 258 mListenerInOrder.verify(mCollectionListener).onRankingApplied(); 259 260 assertEquals(notif2.key, entry.getKey()); 261 assertEquals(notif2.sbn, entry.getSbn()); 262 assertEquals(notif2.ranking, entry.getRanking()); 263 } 264 265 @Test testEventDispatchedWhenNotifRemoved()266 public void testEventDispatchedWhenNotifRemoved() { 267 // GIVEN a collection with one notif 268 mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3)); 269 clearInvocations(mCollectionListener); 270 271 NotifEvent notif = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47)); 272 NotificationEntry entry = mCollectionListener.getEntry(notif.key); 273 clearInvocations(mCollectionListener); 274 275 // WHEN a notif is retracted 276 mNoMan.retractNotif(notif.sbn, REASON_APP_CANCEL); 277 278 // THEN the listener is notified 279 mListenerInOrder.verify(mCollectionListener).onEntryRemoved(entry, REASON_APP_CANCEL); 280 mListenerInOrder.verify(mCollectionListener).onEntryCleanUp(entry); 281 mListenerInOrder.verify(mCollectionListener).onRankingApplied(); 282 283 assertEquals(notif.sbn, entry.getSbn()); 284 assertEquals(notif.ranking, entry.getRanking()); 285 } 286 287 @Test testRankingsAreUpdatedForOtherNotifs()288 public void testRankingsAreUpdatedForOtherNotifs() { 289 // GIVEN a collection with one notif 290 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3) 291 .setRank(47)); 292 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); 293 294 // WHEN a new notif is posted, triggering a rerank 295 mNoMan.setRanking(notif1.sbn.getKey(), new RankingBuilder(notif1.ranking) 296 .setRank(56) 297 .build()); 298 mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 77)); 299 300 // THEN the ranking is updated on the first entry 301 assertEquals(56, entry1.getRanking().getRank()); 302 } 303 304 @Test testRankingUpdateIsProperlyIssuedToEveryone()305 public void testRankingUpdateIsProperlyIssuedToEveryone() { 306 // GIVEN a collection with a couple notifs 307 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3) 308 .setRank(3)); 309 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 8) 310 .setRank(2)); 311 NotifEvent notif3 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 77) 312 .setRank(1)); 313 314 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); 315 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); 316 NotificationEntry entry3 = mCollectionListener.getEntry(notif3.key); 317 318 // WHEN a ranking update is delivered 319 Ranking newRanking1 = new RankingBuilder(notif1.ranking) 320 .setRank(4) 321 .setExplanation("Foo bar") 322 .build(); 323 Ranking newRanking2 = new RankingBuilder(notif2.ranking) 324 .setRank(5) 325 .setExplanation("baz buzz") 326 .build(); 327 328 // WHEN entry3's ranking update includes an update to its overrideGroupKey 329 final String newOverrideGroupKey = "newOverrideGroupKey"; 330 Ranking newRanking3 = new RankingBuilder(notif3.ranking) 331 .setRank(6) 332 .setExplanation("Penguin pizza") 333 .setOverrideGroupKey(newOverrideGroupKey) 334 .build(); 335 336 mNoMan.setRanking(notif1.sbn.getKey(), newRanking1); 337 mNoMan.setRanking(notif2.sbn.getKey(), newRanking2); 338 mNoMan.setRanking(notif3.sbn.getKey(), newRanking3); 339 mNoMan.issueRankingUpdate(); 340 341 // THEN all of the NotifEntries have their rankings properly updated 342 assertEquals(newRanking1, entry1.getRanking()); 343 assertEquals(newRanking2, entry2.getRanking()); 344 assertEquals(newRanking3, entry3.getRanking()); 345 346 // THEN the entry3's overrideGroupKey is updated along with its groupKey 347 assertEquals(newOverrideGroupKey, entry3.getSbn().getOverrideGroupKey()); 348 assertNotNull(entry3.getSbn().getGroupKey()); 349 } 350 351 @Test testNotifEntriesAreNotPersistedAcrossRemovalAndReposting()352 public void testNotifEntriesAreNotPersistedAcrossRemovalAndReposting() { 353 // GIVEN a notification that has been posted 354 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3)); 355 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); 356 357 // WHEN the notification is retracted and then reposted 358 mNoMan.retractNotif(notif1.sbn, REASON_APP_CANCEL); 359 mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3)); 360 361 // THEN the new NotificationEntry is a new object 362 NotificationEntry entry2 = mCollectionListener.getEntry(notif1.key); 363 assertNotEquals(entry2, entry1); 364 } 365 366 @Test testDismissNotificationSentToSystemServer()367 public void testDismissNotificationSentToSystemServer() throws RemoteException { 368 // GIVEN a collection with a couple notifications 369 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); 370 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag")); 371 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); 372 373 // WHEN a notification is manually dismissed 374 DismissedByUserStats stats = defaultStats(entry2); 375 mCollection.dismissNotification(entry2, defaultStats(entry2)); 376 377 // THEN we send the dismissal to system server 378 verify(mStatusBarService).onNotificationClear( 379 notif2.sbn.getPackageName(), 380 notif2.sbn.getUser().getIdentifier(), 381 notif2.sbn.getKey(), 382 stats.dismissalSurface, 383 stats.dismissalSentiment, 384 stats.notificationVisibility); 385 } 386 387 @Test testDismissedNotificationsAreMarkedAsDismissedLocally()388 public void testDismissedNotificationsAreMarkedAsDismissedLocally() { 389 // GIVEN a collection with a notification 390 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); 391 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); 392 393 // WHEN a notification is manually dismissed 394 mCollection.dismissNotification(entry1, defaultStats(entry1)); 395 396 // THEN the entry is marked as dismissed locally 397 assertEquals(DISMISSED, entry1.getDismissState()); 398 } 399 400 @Test testDismissedNotificationsCannotBeLifetimeExtended()401 public void testDismissedNotificationsCannotBeLifetimeExtended() { 402 // GIVEN a collection with a notification and a lifetime extender 403 mCollection.addNotificationLifetimeExtender(mExtender1); 404 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); 405 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); 406 407 // WHEN a notification is manually dismissed 408 mCollection.dismissNotification(entry1, defaultStats(entry1)); 409 410 // THEN lifetime extenders are never queried 411 verify(mExtender1, never()).shouldExtendLifetime(eq(entry1), anyInt()); 412 } 413 414 @Test testDismissedNotificationsDoNotTriggerRemovalEvents()415 public void testDismissedNotificationsDoNotTriggerRemovalEvents() { 416 // GIVEN a collection with a notification 417 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); 418 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); 419 420 // WHEN a notification is manually dismissed 421 mCollection.dismissNotification(entry1, defaultStats(entry1)); 422 423 // THEN onEntryRemoved is not called 424 verify(mCollectionListener, never()).onEntryRemoved(eq(entry1), anyInt()); 425 } 426 427 @Test testDismissedNotificationsStillAppearInNotificationSet()428 public void testDismissedNotificationsStillAppearInNotificationSet() { 429 // GIVEN a collection with a notification 430 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); 431 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); 432 433 // WHEN a notification is manually dismissed 434 mCollection.dismissNotification(entry1, defaultStats(entry1)); 435 436 // THEN the dismissed entry still appears in the notification set 437 assertEquals( 438 new ArraySet<>(singletonList(entry1)), 439 new ArraySet<>(mCollection.getAllNotifs())); 440 } 441 442 @Test testRetractingLifetimeExtendedSummaryDoesNotDismissChildren()443 public void testRetractingLifetimeExtendedSummaryDoesNotDismissChildren() { 444 // GIVEN A notif group with one summary and two children 445 mCollection.addNotificationLifetimeExtender(mExtender1); 446 CollectionEvent notif1 = postNotif( 447 buildNotif(TEST_PACKAGE, 1, "myTag") 448 .setGroup(mContext, GROUP_1) 449 .setGroupSummary(mContext, true)); 450 CollectionEvent notif2 = postNotif( 451 buildNotif(TEST_PACKAGE, 2, "myTag") 452 .setGroup(mContext, GROUP_1)); 453 CollectionEvent notif3 = postNotif( 454 buildNotif(TEST_PACKAGE, 3, "myTag") 455 .setGroup(mContext, GROUP_1)); 456 457 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); 458 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); 459 NotificationEntry entry3 = mCollectionListener.getEntry(notif3.key); 460 461 // GIVEN that the summary and one child are retracted by the app, but both are 462 // lifetime-extended 463 mExtender1.shouldExtendLifetime = true; 464 mNoMan.retractNotif(notif1.sbn, REASON_APP_CANCEL); 465 mNoMan.retractNotif(notif2.sbn, REASON_APP_CANCEL); 466 assertEquals( 467 new ArraySet<>(List.of(entry1, entry2, entry3)), 468 new ArraySet<>(mCollection.getAllNotifs())); 469 470 // WHEN the summary is retracted by the app 471 mCollection.dismissNotification(entry1, defaultStats(entry1)); 472 473 // THEN the summary is removed, but both children stick around 474 assertEquals( 475 new ArraySet<>(List.of(entry2, entry3)), 476 new ArraySet<>(mCollection.getAllNotifs())); 477 assertEquals(NOT_DISMISSED, entry2.getDismissState()); 478 assertEquals(NOT_DISMISSED, entry3.getDismissState()); 479 } 480 481 @Test testNMSReportsUserDismissalAlwaysRemovesNotif()482 public void testNMSReportsUserDismissalAlwaysRemovesNotif() throws RemoteException { 483 // GIVEN notifications are lifetime extended 484 mExtender1.shouldExtendLifetime = true; 485 CollectionEvent notif = postNotif(buildNotif(TEST_PACKAGE, 1, "myTag")); 486 CollectionEvent notif2 = postNotif(buildNotif(TEST_PACKAGE, 2, "myTag")); 487 NotificationEntry entry = mCollectionListener.getEntry(notif.key); 488 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); 489 assertEquals( 490 new ArraySet<>(List.of(entry, entry2)), 491 new ArraySet<>(mCollection.getAllNotifs())); 492 493 // WHEN the notifications are reported to be dismissed by the user by NMS 494 mNoMan.retractNotif(notif.sbn, REASON_CANCEL); 495 mNoMan.retractNotif(notif2.sbn, REASON_CLICK); 496 497 // THEN the notifications are removed b/c they were dismissed by the user 498 assertEquals( 499 new ArraySet<>(List.of()), 500 new ArraySet<>(mCollection.getAllNotifs())); 501 } 502 503 @Test testDismissNotificationCallsDismissInterceptors()504 public void testDismissNotificationCallsDismissInterceptors() throws RemoteException { 505 // GIVEN a collection with notifications with multiple dismiss interceptors 506 mInterceptor1.shouldInterceptDismissal = true; 507 mInterceptor2.shouldInterceptDismissal = true; 508 mInterceptor3.shouldInterceptDismissal = false; 509 mCollection.addNotificationDismissInterceptor(mInterceptor1); 510 mCollection.addNotificationDismissInterceptor(mInterceptor2); 511 mCollection.addNotificationDismissInterceptor(mInterceptor3); 512 513 NotifEvent notif = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); 514 NotificationEntry entry = mCollectionListener.getEntry(notif.key); 515 516 // WHEN a notification is manually dismissed 517 DismissedByUserStats stats = defaultStats(entry); 518 mCollection.dismissNotification(entry, stats); 519 520 // THEN all interceptors get checked 521 verify(mInterceptor1).shouldInterceptDismissal(entry); 522 verify(mInterceptor2).shouldInterceptDismissal(entry); 523 verify(mInterceptor3).shouldInterceptDismissal(entry); 524 assertEquals(List.of(mInterceptor1, mInterceptor2), entry.mDismissInterceptors); 525 526 // THEN we never send the dismissal to system server 527 verify(mStatusBarService, never()).onNotificationClear( 528 notif.sbn.getPackageName(), 529 notif.sbn.getUser().getIdentifier(), 530 notif.sbn.getKey(), 531 stats.dismissalSurface, 532 stats.dismissalSentiment, 533 stats.notificationVisibility); 534 } 535 536 @Test testDismissInterceptorsCanceledWhenNotifIsUpdated()537 public void testDismissInterceptorsCanceledWhenNotifIsUpdated() throws RemoteException { 538 // GIVEN a few lifetime extenders and a couple notifications 539 mCollection.addNotificationDismissInterceptor(mInterceptor1); 540 mCollection.addNotificationDismissInterceptor(mInterceptor2); 541 542 mInterceptor1.shouldInterceptDismissal = true; 543 mInterceptor2.shouldInterceptDismissal = true; 544 545 NotifEvent notif = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47)); 546 NotificationEntry entry = mCollectionListener.getEntry(notif.key); 547 548 // WHEN a notification is manually dismissed and intercepted 549 DismissedByUserStats stats = defaultStats(entry); 550 mCollection.dismissNotification(entry, stats); 551 assertEquals(List.of(mInterceptor1, mInterceptor2), entry.mDismissInterceptors); 552 clearInvocations(mInterceptor1, mInterceptor2); 553 554 // WHEN the notification is reposted 555 mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47)); 556 557 // THEN all of the active dismissal interceptors are canceled 558 verify(mInterceptor1).cancelDismissInterception(entry); 559 verify(mInterceptor2).cancelDismissInterception(entry); 560 assertEquals(List.of(), entry.mDismissInterceptors); 561 562 // THEN the notification is never sent to system server to dismiss 563 verify(mStatusBarService, never()).onNotificationClear( 564 eq(notif.sbn.getPackageName()), 565 eq(notif.sbn.getUser().getIdentifier()), 566 eq(notif.sbn.getKey()), 567 anyInt(), 568 anyInt(), 569 eq(stats.notificationVisibility)); 570 } 571 572 @Test testEndingAllDismissInterceptorsSendsDismiss()573 public void testEndingAllDismissInterceptorsSendsDismiss() throws RemoteException { 574 // GIVEN a collection with notifications a dismiss interceptor 575 mInterceptor1.shouldInterceptDismissal = true; 576 mCollection.addNotificationDismissInterceptor(mInterceptor1); 577 578 NotifEvent notif = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); 579 NotificationEntry entry = mCollectionListener.getEntry(notif.key); 580 581 // GIVEN a notification is manually dismissed 582 DismissedByUserStats stats = defaultStats(entry); 583 mCollection.dismissNotification(entry, stats); 584 585 // WHEN all interceptors end their interception dismissal 586 mInterceptor1.shouldInterceptDismissal = false; 587 mInterceptor1.onEndInterceptionCallback.onEndDismissInterception(mInterceptor1, entry, 588 stats); 589 590 // THEN we send the dismissal to system server 591 verify(mStatusBarService).onNotificationClear( 592 eq(notif.sbn.getPackageName()), 593 eq(notif.sbn.getUser().getIdentifier()), 594 eq(notif.sbn.getKey()), 595 anyInt(), 596 anyInt(), 597 eq(stats.notificationVisibility)); 598 } 599 600 @Test testEndDismissInterceptionUpdatesDismissInterceptors()601 public void testEndDismissInterceptionUpdatesDismissInterceptors() { 602 // GIVEN a collection with notifications with multiple dismiss interceptors 603 mInterceptor1.shouldInterceptDismissal = true; 604 mInterceptor2.shouldInterceptDismissal = true; 605 mInterceptor3.shouldInterceptDismissal = false; 606 mCollection.addNotificationDismissInterceptor(mInterceptor1); 607 mCollection.addNotificationDismissInterceptor(mInterceptor2); 608 mCollection.addNotificationDismissInterceptor(mInterceptor3); 609 610 NotifEvent notif = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); 611 NotificationEntry entry = mCollectionListener.getEntry(notif.key); 612 613 // GIVEN a notification is manually dismissed 614 mCollection.dismissNotification(entry, defaultStats(entry)); 615 616 // WHEN an interceptor ends its interception 617 mInterceptor1.shouldInterceptDismissal = false; 618 mInterceptor1.onEndInterceptionCallback.onEndDismissInterception(mInterceptor1, entry, 619 defaultStats(entry)); 620 621 // THEN all interceptors get checked 622 verify(mInterceptor1).shouldInterceptDismissal(entry); 623 verify(mInterceptor2).shouldInterceptDismissal(entry); 624 verify(mInterceptor3).shouldInterceptDismissal(entry); 625 626 // THEN mInterceptor2 is the only dismiss interceptor 627 assertEquals(List.of(mInterceptor2), entry.mDismissInterceptors); 628 } 629 630 631 @Test(expected = IllegalStateException.class) testEndingDismissalOfNonInterceptedThrows()632 public void testEndingDismissalOfNonInterceptedThrows() { 633 // GIVEN a collection with notifications with a dismiss interceptor that hasn't been called 634 mInterceptor1.shouldInterceptDismissal = false; 635 mCollection.addNotificationDismissInterceptor(mInterceptor1); 636 637 NotifEvent notif = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); 638 NotificationEntry entry = mCollectionListener.getEntry(notif.key); 639 640 // WHEN we try to end the dismissal of an interceptor that didn't intercept the notif 641 mInterceptor1.onEndInterceptionCallback.onEndDismissInterception(mInterceptor1, entry, 642 defaultStats(entry)); 643 644 // THEN an exception is thrown 645 } 646 647 @Test(expected = IllegalStateException.class) testDismissingNonExistentNotificationThrows()648 public void testDismissingNonExistentNotificationThrows() { 649 // GIVEN a collection that originally had three notifs, but where one was dismissed 650 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47)); 651 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88)); 652 NotifEvent notif3 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 99)); 653 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); 654 mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN); 655 656 // WHEN we try to dismiss a notification that isn't present 657 mCollection.dismissNotification(entry2, defaultStats(entry2)); 658 659 // THEN an exception is thrown 660 } 661 662 @Test testGroupChildrenAreDismissedLocallyWhenSummaryIsDismissed()663 public void testGroupChildrenAreDismissedLocallyWhenSummaryIsDismissed() { 664 // GIVEN a collection with two grouped notifs in it 665 CollectionEvent notif0 = postNotif( 666 buildNotif(TEST_PACKAGE, 0) 667 .setGroup(mContext, GROUP_1) 668 .setGroupSummary(mContext, true)); 669 CollectionEvent notif1 = postNotif( 670 buildNotif(TEST_PACKAGE, 1) 671 .setGroup(mContext, GROUP_1)); 672 NotificationEntry entry0 = mCollectionListener.getEntry(notif0.key); 673 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); 674 675 // WHEN the summary is dismissed 676 mCollection.dismissNotification(entry0, defaultStats(entry0)); 677 678 // THEN all members of the group are marked as dismissed locally 679 assertEquals(DISMISSED, entry0.getDismissState()); 680 assertEquals(PARENT_DISMISSED, entry1.getDismissState()); 681 } 682 683 @Test testUpdatingDismissedSummaryBringsChildrenBack()684 public void testUpdatingDismissedSummaryBringsChildrenBack() { 685 // GIVEN a collection with two grouped notifs in it 686 CollectionEvent notif0 = postNotif( 687 buildNotif(TEST_PACKAGE, 0) 688 .setGroup(mContext, GROUP_1) 689 .setGroupSummary(mContext, true)); 690 CollectionEvent notif1 = postNotif( 691 buildNotif(TEST_PACKAGE, 1) 692 .setGroup(mContext, GROUP_1)); 693 NotificationEntry entry0 = mCollectionListener.getEntry(notif0.key); 694 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); 695 696 // WHEN the summary is dismissed but then reposted without a group 697 mCollection.dismissNotification(entry0, defaultStats(entry0)); 698 NotifEvent notif0a = mNoMan.postNotif( 699 buildNotif(TEST_PACKAGE, 0)); 700 701 // THEN it and all of its previous children are no longer dismissed locally 702 assertEquals(NOT_DISMISSED, entry0.getDismissState()); 703 assertEquals(NOT_DISMISSED, entry1.getDismissState()); 704 } 705 706 @Test testDismissedChildrenAreNotResetByParentUpdate()707 public void testDismissedChildrenAreNotResetByParentUpdate() { 708 // GIVEN a collection with three grouped notifs in it 709 CollectionEvent notif0 = postNotif( 710 buildNotif(TEST_PACKAGE, 0) 711 .setGroup(mContext, GROUP_1) 712 .setGroupSummary(mContext, true)); 713 CollectionEvent notif1 = postNotif( 714 buildNotif(TEST_PACKAGE, 1) 715 .setGroup(mContext, GROUP_1)); 716 CollectionEvent notif2 = postNotif( 717 buildNotif(TEST_PACKAGE, 2) 718 .setGroup(mContext, GROUP_1)); 719 NotificationEntry entry0 = mCollectionListener.getEntry(notif0.key); 720 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); 721 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); 722 723 // WHEN a child is dismissed, then the parent is dismissed, then the parent is updated 724 mCollection.dismissNotification(entry1, defaultStats(entry1)); 725 mCollection.dismissNotification(entry0, defaultStats(entry0)); 726 NotifEvent notif0a = mNoMan.postNotif( 727 buildNotif(TEST_PACKAGE, 0)); 728 729 // THEN the manually-dismissed child is still marked as dismissed 730 assertEquals(NOT_DISMISSED, entry0.getDismissState()); 731 assertEquals(DISMISSED, entry1.getDismissState()); 732 assertEquals(NOT_DISMISSED, entry2.getDismissState()); 733 } 734 735 @Test testUpdatingGroupKeyOfDismissedSummaryBringsChildrenBack()736 public void testUpdatingGroupKeyOfDismissedSummaryBringsChildrenBack() { 737 // GIVEN a collection with two grouped notifs in it 738 CollectionEvent notif0 = postNotif( 739 buildNotif(TEST_PACKAGE, 0) 740 .setOverrideGroupKey(GROUP_1) 741 .setGroupSummary(mContext, true)); 742 CollectionEvent notif1 = postNotif( 743 buildNotif(TEST_PACKAGE, 1) 744 .setOverrideGroupKey(GROUP_1)); 745 NotificationEntry entry0 = mCollectionListener.getEntry(notif0.key); 746 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); 747 748 // WHEN the summary is dismissed but then reposted AND in the same update one of the 749 // children's ranking loses its override group 750 mCollection.dismissNotification(entry0, defaultStats(entry0)); 751 mNoMan.setRanking(entry1.getKey(), new RankingBuilder() 752 .setKey(entry1.getKey()) 753 .build()); 754 mNoMan.postNotif( 755 buildNotif(TEST_PACKAGE, 0) 756 .setOverrideGroupKey(GROUP_1) 757 .setGroupSummary(mContext, true)); 758 759 // THEN it and all of its previous children are no longer dismissed locally, including the 760 // child that is no longer part of the group 761 assertEquals(NOT_DISMISSED, entry0.getDismissState()); 762 assertEquals(NOT_DISMISSED, entry1.getDismissState()); 763 } 764 765 @Test testDismissingSummaryDoesNotDismissForegroundServiceChildren()766 public void testDismissingSummaryDoesNotDismissForegroundServiceChildren() { 767 // GIVEN a collection with three grouped notifs in it 768 CollectionEvent notif0 = postNotif( 769 buildNotif(TEST_PACKAGE, 0) 770 .setGroup(mContext, GROUP_1) 771 .setGroupSummary(mContext, true)); 772 CollectionEvent notif1 = postNotif( 773 buildNotif(TEST_PACKAGE, 1) 774 .setGroup(mContext, GROUP_1) 775 .setFlag(mContext, Notification.FLAG_FOREGROUND_SERVICE, true)); 776 CollectionEvent notif2 = postNotif( 777 buildNotif(TEST_PACKAGE, 2) 778 .setGroup(mContext, GROUP_1)); 779 780 // WHEN the summary is dismissed 781 mCollection.dismissNotification(notif0.entry, defaultStats(notif0.entry)); 782 783 // THEN the foreground service child is not dismissed 784 assertEquals(DISMISSED, notif0.entry.getDismissState()); 785 assertEquals(NOT_DISMISSED, notif1.entry.getDismissState()); 786 assertEquals(PARENT_DISMISSED, notif2.entry.getDismissState()); 787 } 788 789 @Test testDismissingSummaryDoesNotDismissBubbledChildren()790 public void testDismissingSummaryDoesNotDismissBubbledChildren() { 791 // GIVEN a collection with three grouped notifs in it 792 CollectionEvent notif0 = postNotif( 793 buildNotif(TEST_PACKAGE, 0) 794 .setGroup(mContext, GROUP_1) 795 .setGroupSummary(mContext, true)); 796 CollectionEvent notif1 = postNotif( 797 buildNotif(TEST_PACKAGE, 1) 798 .setGroup(mContext, GROUP_1) 799 .setFlag(mContext, Notification.FLAG_BUBBLE, true)); 800 CollectionEvent notif2 = postNotif( 801 buildNotif(TEST_PACKAGE, 2) 802 .setGroup(mContext, GROUP_1)); 803 804 // WHEN the summary is dismissed 805 mCollection.dismissNotification(notif0.entry, defaultStats(notif0.entry)); 806 807 // THEN the bubbled child is not dismissed 808 assertEquals(DISMISSED, notif0.entry.getDismissState()); 809 assertEquals(NOT_DISMISSED, notif1.entry.getDismissState()); 810 assertEquals(PARENT_DISMISSED, notif2.entry.getDismissState()); 811 } 812 813 @Test testDismissingSummaryDoesNotDismissDuplicateSummaries()814 public void testDismissingSummaryDoesNotDismissDuplicateSummaries() { 815 // GIVEN a group with a two summaries 816 CollectionEvent notif0 = postNotif( 817 buildNotif(TEST_PACKAGE, 0) 818 .setGroup(mContext, GROUP_1) 819 .setGroupSummary(mContext, true)); 820 CollectionEvent notif1 = postNotif( 821 buildNotif(TEST_PACKAGE, 1) 822 .setGroup(mContext, GROUP_1) 823 .setGroupSummary(mContext, true)); 824 CollectionEvent notif2 = postNotif( 825 buildNotif(TEST_PACKAGE, 2) 826 .setGroup(mContext, GROUP_1)); 827 828 // WHEN the first summary is dismissed 829 mCollection.dismissNotification(notif0.entry, defaultStats(notif0.entry)); 830 831 // THEN the second summary is not auto-dismissed (but the child is) 832 assertEquals(DISMISSED, notif0.entry.getDismissState()); 833 assertEquals(NOT_DISMISSED, notif1.entry.getDismissState()); 834 assertEquals(PARENT_DISMISSED, notif2.entry.getDismissState()); 835 } 836 837 @Test testLifetimeExtendersAreQueriedWhenNotifRemoved()838 public void testLifetimeExtendersAreQueriedWhenNotifRemoved() { 839 // GIVEN a couple notifications and a few lifetime extenders 840 mExtender1.shouldExtendLifetime = true; 841 mExtender2.shouldExtendLifetime = true; 842 843 mCollection.addNotificationLifetimeExtender(mExtender1); 844 mCollection.addNotificationLifetimeExtender(mExtender2); 845 mCollection.addNotificationLifetimeExtender(mExtender3); 846 847 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47)); 848 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88)); 849 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); 850 851 // WHEN a notification is removed by the app 852 mNoMan.retractNotif(notif2.sbn, REASON_APP_CANCEL); 853 854 // THEN each extender is asked whether to extend, even if earlier ones return true 855 verify(mExtender1).shouldExtendLifetime(entry2, REASON_APP_CANCEL); 856 verify(mExtender2).shouldExtendLifetime(entry2, REASON_APP_CANCEL); 857 verify(mExtender3).shouldExtendLifetime(entry2, REASON_APP_CANCEL); 858 859 // THEN the entry is not removed 860 assertTrue(mCollection.getAllNotifs().contains(entry2)); 861 862 // THEN the entry properly records all extenders that returned true 863 assertEquals(Arrays.asList(mExtender1, mExtender2), entry2.mLifetimeExtenders); 864 } 865 866 @Test testWhenLastLifetimeExtenderExpiresAllAreReQueried()867 public void testWhenLastLifetimeExtenderExpiresAllAreReQueried() { 868 // GIVEN a couple notifications and a few lifetime extenders 869 mExtender2.shouldExtendLifetime = true; 870 871 mCollection.addNotificationLifetimeExtender(mExtender1); 872 mCollection.addNotificationLifetimeExtender(mExtender2); 873 mCollection.addNotificationLifetimeExtender(mExtender3); 874 875 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47)); 876 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88)); 877 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); 878 879 // GIVEN a notification gets lifetime-extended by one of them 880 mNoMan.retractNotif(notif2.sbn, REASON_APP_CANCEL); 881 assertTrue(mCollection.getAllNotifs().contains(entry2)); 882 clearInvocations(mExtender1, mExtender2, mExtender3); 883 884 // WHEN the last active extender expires (but new ones become active) 885 mExtender1.shouldExtendLifetime = true; 886 mExtender2.shouldExtendLifetime = false; 887 mExtender3.shouldExtendLifetime = true; 888 mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2); 889 890 // THEN each extender is re-queried 891 verify(mExtender1).shouldExtendLifetime(entry2, REASON_APP_CANCEL); 892 verify(mExtender2).shouldExtendLifetime(entry2, REASON_APP_CANCEL); 893 verify(mExtender3).shouldExtendLifetime(entry2, REASON_APP_CANCEL); 894 895 // THEN the entry is not removed 896 assertTrue(mCollection.getAllNotifs().contains(entry2)); 897 898 // THEN the entry properly records all extenders that returned true 899 assertEquals(Arrays.asList(mExtender1, mExtender3), entry2.mLifetimeExtenders); 900 } 901 902 @Test testExtendersAreNotReQueriedUntilFinalActiveExtenderExpires()903 public void testExtendersAreNotReQueriedUntilFinalActiveExtenderExpires() { 904 // GIVEN a couple notifications and a few lifetime extenders 905 mExtender1.shouldExtendLifetime = true; 906 mExtender2.shouldExtendLifetime = true; 907 908 mCollection.addNotificationLifetimeExtender(mExtender1); 909 mCollection.addNotificationLifetimeExtender(mExtender2); 910 mCollection.addNotificationLifetimeExtender(mExtender3); 911 912 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47)); 913 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88)); 914 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); 915 916 // GIVEN a notification gets lifetime-extended by a couple of them 917 mNoMan.retractNotif(notif2.sbn, REASON_APP_CANCEL); 918 assertTrue(mCollection.getAllNotifs().contains(entry2)); 919 clearInvocations(mExtender1, mExtender2, mExtender3); 920 921 // WHEN one (but not all) of the extenders expires 922 mExtender2.shouldExtendLifetime = false; 923 mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2); 924 925 // THEN the entry is not removed 926 assertTrue(mCollection.getAllNotifs().contains(entry2)); 927 928 // THEN we don't re-query the extenders 929 verify(mExtender1, never()).shouldExtendLifetime(entry2, REASON_APP_CANCEL); 930 verify(mExtender2, never()).shouldExtendLifetime(entry2, REASON_APP_CANCEL); 931 verify(mExtender3, never()).shouldExtendLifetime(entry2, REASON_APP_CANCEL); 932 933 // THEN the entry properly records all extenders that returned true 934 assertEquals(singletonList(mExtender1), entry2.mLifetimeExtenders); 935 } 936 937 @Test testNotificationIsRemovedWhenAllLifetimeExtendersExpire()938 public void testNotificationIsRemovedWhenAllLifetimeExtendersExpire() { 939 // GIVEN a couple notifications and a few lifetime extenders 940 mExtender1.shouldExtendLifetime = true; 941 mExtender2.shouldExtendLifetime = true; 942 943 mCollection.addNotificationLifetimeExtender(mExtender1); 944 mCollection.addNotificationLifetimeExtender(mExtender2); 945 mCollection.addNotificationLifetimeExtender(mExtender3); 946 947 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47)); 948 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88)); 949 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); 950 951 // GIVEN a notification gets lifetime-extended by a couple of them 952 mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN); 953 assertTrue(mCollection.getAllNotifs().contains(entry2)); 954 clearInvocations(mExtender1, mExtender2, mExtender3); 955 956 // WHEN all of the active extenders expire 957 mExtender2.shouldExtendLifetime = false; 958 mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2); 959 mExtender1.shouldExtendLifetime = false; 960 mExtender1.callback.onEndLifetimeExtension(mExtender1, entry2); 961 962 // THEN the entry removed 963 assertFalse(mCollection.getAllNotifs().contains(entry2)); 964 verify(mCollectionListener).onEntryRemoved(entry2, REASON_UNKNOWN); 965 } 966 967 @Test testLifetimeExtensionIsCanceledWhenNotifIsUpdated()968 public void testLifetimeExtensionIsCanceledWhenNotifIsUpdated() { 969 // GIVEN a few lifetime extenders and a couple notifications 970 mCollection.addNotificationLifetimeExtender(mExtender1); 971 mCollection.addNotificationLifetimeExtender(mExtender2); 972 mCollection.addNotificationLifetimeExtender(mExtender3); 973 974 mExtender1.shouldExtendLifetime = true; 975 mExtender2.shouldExtendLifetime = true; 976 977 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47)); 978 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88)); 979 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); 980 981 // GIVEN a notification gets lifetime-extended by a couple of them 982 mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN); 983 assertTrue(mCollection.getAllNotifs().contains(entry2)); 984 clearInvocations(mExtender1, mExtender2, mExtender3); 985 986 // WHEN the notification is reposted 987 mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88)); 988 989 // THEN all of the active lifetime extenders are canceled 990 verify(mExtender1).cancelLifetimeExtension(entry2); 991 verify(mExtender2).cancelLifetimeExtension(entry2); 992 993 // THEN the notification is still present 994 assertTrue(mCollection.getAllNotifs().contains(entry2)); 995 } 996 997 @Test(expected = IllegalStateException.class) testReentrantCallsToLifetimeExtendersThrow()998 public void testReentrantCallsToLifetimeExtendersThrow() { 999 // GIVEN a few lifetime extenders and a couple notifications 1000 mCollection.addNotificationLifetimeExtender(mExtender1); 1001 mCollection.addNotificationLifetimeExtender(mExtender2); 1002 mCollection.addNotificationLifetimeExtender(mExtender3); 1003 1004 mExtender1.shouldExtendLifetime = true; 1005 mExtender2.shouldExtendLifetime = true; 1006 1007 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47)); 1008 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88)); 1009 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); 1010 1011 // GIVEN a notification gets lifetime-extended by a couple of them 1012 mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN); 1013 assertTrue(mCollection.getAllNotifs().contains(entry2)); 1014 clearInvocations(mExtender1, mExtender2, mExtender3); 1015 1016 // WHEN a lifetime extender makes a reentrant call during cancelLifetimeExtension() 1017 mExtender2.onCancelLifetimeExtension = () -> { 1018 mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2); 1019 }; 1020 // This triggers the call to cancelLifetimeExtension() 1021 mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88)); 1022 1023 // THEN an exception is thrown 1024 } 1025 1026 @Test testRankingIsUpdatedWhenALifetimeExtendedNotifIsReposted()1027 public void testRankingIsUpdatedWhenALifetimeExtendedNotifIsReposted() { 1028 // GIVEN a few lifetime extenders and a couple notifications 1029 mCollection.addNotificationLifetimeExtender(mExtender1); 1030 mCollection.addNotificationLifetimeExtender(mExtender2); 1031 mCollection.addNotificationLifetimeExtender(mExtender3); 1032 1033 mExtender1.shouldExtendLifetime = true; 1034 mExtender2.shouldExtendLifetime = true; 1035 1036 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47)); 1037 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88)); 1038 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); 1039 1040 // GIVEN a notification gets lifetime-extended by a couple of them 1041 mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN); 1042 assertTrue(mCollection.getAllNotifs().contains(entry2)); 1043 clearInvocations(mExtender1, mExtender2, mExtender3); 1044 1045 // WHEN the notification is reposted 1046 NotifEvent notif2a = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88) 1047 .setRank(4747) 1048 .setExplanation("Some new explanation")); 1049 1050 // THEN the notification's ranking is properly updated 1051 assertEquals(notif2a.ranking, entry2.getRanking()); 1052 } 1053 1054 @Test testCancellationReasonIsSetWhenNotifIsCancelled()1055 public void testCancellationReasonIsSetWhenNotifIsCancelled() { 1056 // GIVEN a notification 1057 NotifEvent notif0 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3)); 1058 NotificationEntry entry0 = mCollectionListener.getEntry(notif0.key); 1059 1060 // WHEN the notification is retracted 1061 mNoMan.retractNotif(notif0.sbn, REASON_APP_CANCEL); 1062 1063 // THEN the retraction reason is stored on the notif 1064 assertEquals(REASON_APP_CANCEL, entry0.mCancellationReason); 1065 } 1066 1067 @Test testCancellationReasonIsClearedWhenNotifIsUpdated()1068 public void testCancellationReasonIsClearedWhenNotifIsUpdated() { 1069 // GIVEN a notification and a lifetime extender that will preserve it 1070 NotifEvent notif0 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3)); 1071 NotificationEntry entry0 = mCollectionListener.getEntry(notif0.key); 1072 mCollection.addNotificationLifetimeExtender(mExtender1); 1073 mExtender1.shouldExtendLifetime = true; 1074 1075 // WHEN the notification is retracted and subsequently reposted 1076 mNoMan.retractNotif(notif0.sbn, REASON_APP_CANCEL); 1077 assertEquals(REASON_APP_CANCEL, entry0.mCancellationReason); 1078 mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3)); 1079 1080 // THEN the notification has its cancellation reason cleared 1081 assertEquals(REASON_NOT_CANCELED, entry0.mCancellationReason); 1082 } 1083 1084 @Test testDismissNotificationsRebuildsOnce()1085 public void testDismissNotificationsRebuildsOnce() { 1086 // GIVEN a collection with a couple notifications 1087 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); 1088 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag")); 1089 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); 1090 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); 1091 clearInvocations(mBuildListener); 1092 1093 // WHEN both notifications are manually dismissed together 1094 mCollection.dismissNotifications( 1095 List.of(new Pair<>(entry1, defaultStats(entry1)), 1096 new Pair<>(entry2, defaultStats(entry2)))); 1097 1098 // THEN build list is only called one time 1099 verifyBuiltList(List.of(entry1, entry2)); 1100 } 1101 1102 @Test testDismissNotificationsSentToSystemServer()1103 public void testDismissNotificationsSentToSystemServer() throws RemoteException { 1104 // GIVEN a collection with a couple notifications 1105 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); 1106 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag")); 1107 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); 1108 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); 1109 1110 // WHEN both notifications are manually dismissed together 1111 DismissedByUserStats stats1 = defaultStats(entry1); 1112 DismissedByUserStats stats2 = defaultStats(entry2); 1113 mCollection.dismissNotifications( 1114 List.of(new Pair<>(entry1, defaultStats(entry1)), 1115 new Pair<>(entry2, defaultStats(entry2)))); 1116 1117 // THEN we send the dismissals to system server 1118 verify(mStatusBarService).onNotificationClear( 1119 notif1.sbn.getPackageName(), 1120 notif1.sbn.getUser().getIdentifier(), 1121 notif1.sbn.getKey(), 1122 stats1.dismissalSurface, 1123 stats1.dismissalSentiment, 1124 stats1.notificationVisibility); 1125 1126 verify(mStatusBarService).onNotificationClear( 1127 notif2.sbn.getPackageName(), 1128 notif2.sbn.getUser().getIdentifier(), 1129 notif2.sbn.getKey(), 1130 stats2.dismissalSurface, 1131 stats2.dismissalSentiment, 1132 stats2.notificationVisibility); 1133 } 1134 1135 @Test testDismissNotificationsMarkedAsDismissed()1136 public void testDismissNotificationsMarkedAsDismissed() { 1137 // GIVEN a collection with a couple notifications 1138 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); 1139 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag")); 1140 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); 1141 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); 1142 1143 // WHEN both notifications are manually dismissed together 1144 mCollection.dismissNotifications( 1145 List.of(new Pair<>(entry1, defaultStats(entry1)), 1146 new Pair<>(entry2, defaultStats(entry2)))); 1147 1148 // THEN the entries are marked as dismissed 1149 assertEquals(DISMISSED, entry1.getDismissState()); 1150 assertEquals(DISMISSED, entry2.getDismissState()); 1151 } 1152 1153 @Test testDismissNotificationssCallsDismissInterceptors()1154 public void testDismissNotificationssCallsDismissInterceptors() { 1155 // GIVEN a collection with notifications with multiple dismiss interceptors 1156 mInterceptor1.shouldInterceptDismissal = true; 1157 mInterceptor2.shouldInterceptDismissal = true; 1158 mInterceptor3.shouldInterceptDismissal = false; 1159 mCollection.addNotificationDismissInterceptor(mInterceptor1); 1160 mCollection.addNotificationDismissInterceptor(mInterceptor2); 1161 mCollection.addNotificationDismissInterceptor(mInterceptor3); 1162 1163 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); 1164 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag")); 1165 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); 1166 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); 1167 1168 // WHEN both notifications are manually dismissed together 1169 mCollection.dismissNotifications( 1170 List.of(new Pair<>(entry1, defaultStats(entry1)), 1171 new Pair<>(entry2, defaultStats(entry2)))); 1172 1173 // THEN all interceptors get checked 1174 verify(mInterceptor1).shouldInterceptDismissal(entry1); 1175 verify(mInterceptor2).shouldInterceptDismissal(entry1); 1176 verify(mInterceptor3).shouldInterceptDismissal(entry1); 1177 verify(mInterceptor1).shouldInterceptDismissal(entry2); 1178 verify(mInterceptor2).shouldInterceptDismissal(entry2); 1179 verify(mInterceptor3).shouldInterceptDismissal(entry2); 1180 1181 assertEquals(List.of(mInterceptor1, mInterceptor2), entry1.mDismissInterceptors); 1182 assertEquals(List.of(mInterceptor1, mInterceptor2), entry2.mDismissInterceptors); 1183 } 1184 1185 @Test testDismissAllNotificationsCallsRebuildOnce()1186 public void testDismissAllNotificationsCallsRebuildOnce() { 1187 // GIVEN a collection with a couple notifications 1188 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); 1189 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag")); 1190 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); 1191 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); 1192 clearInvocations(mBuildListener); 1193 1194 // WHEN all notifications are dismissed for the user who posted both notifs 1195 mCollection.dismissAllNotifications(entry1.getSbn().getUser().getIdentifier()); 1196 1197 // THEN build list is only called one time 1198 verifyBuiltList(List.of(entry1, entry2)); 1199 } 1200 1201 @Test testDismissAllNotificationsSentToSystemServer()1202 public void testDismissAllNotificationsSentToSystemServer() throws RemoteException { 1203 // GIVEN a collection with a couple notifications 1204 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); 1205 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag")); 1206 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); 1207 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); 1208 1209 // WHEN all notifications are dismissed for the user who posted both notifs 1210 mCollection.dismissAllNotifications(entry1.getSbn().getUser().getIdentifier()); 1211 1212 // THEN we send the dismissal to system server 1213 verify(mStatusBarService).onClearAllNotifications( 1214 entry1.getSbn().getUser().getIdentifier()); 1215 } 1216 1217 @Test testDismissAllNotificationsMarkedAsDismissed()1218 public void testDismissAllNotificationsMarkedAsDismissed() { 1219 // GIVEN a collection with a couple notifications 1220 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag")); 1221 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag")); 1222 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key); 1223 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); 1224 1225 // WHEN all notifications are dismissed for the user who posted both notifs 1226 mCollection.dismissAllNotifications(entry1.getSbn().getUser().getIdentifier()); 1227 1228 // THEN the entries are marked as dismissed 1229 assertEquals(DISMISSED, entry1.getDismissState()); 1230 assertEquals(DISMISSED, entry2.getDismissState()); 1231 } 1232 1233 @Test testDismissAllNotificationsDoesNotMarkDismissedUnclearableNotifs()1234 public void testDismissAllNotificationsDoesNotMarkDismissedUnclearableNotifs() { 1235 // GIVEN a collection with one unclearable notification and one clearable notification 1236 NotificationEntryBuilder notifEntryBuilder = buildNotif(TEST_PACKAGE, 47, "myTag"); 1237 notifEntryBuilder.modifyNotification(mContext) 1238 .setFlag(FLAG_NO_CLEAR, true); 1239 NotifEvent unclearabeNotif = mNoMan.postNotif(notifEntryBuilder); 1240 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag")); 1241 NotificationEntry unclearableEntry = mCollectionListener.getEntry(unclearabeNotif.key); 1242 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key); 1243 1244 // WHEN all notifications are dismissed for the user who posted both notifs 1245 mCollection.dismissAllNotifications(unclearableEntry.getSbn().getUser().getIdentifier()); 1246 1247 // THEN only the clearable entry is marked as dismissed 1248 assertEquals(NOT_DISMISSED, unclearableEntry.getDismissState()); 1249 assertEquals(DISMISSED, entry2.getDismissState()); 1250 } 1251 1252 @Test testDismissAllNotificationsCallsDismissInterceptorsOnlyOnUnclearableNotifs()1253 public void testDismissAllNotificationsCallsDismissInterceptorsOnlyOnUnclearableNotifs() { 1254 // GIVEN a collection with multiple dismiss interceptors 1255 mInterceptor1.shouldInterceptDismissal = true; 1256 mInterceptor2.shouldInterceptDismissal = true; 1257 mInterceptor3.shouldInterceptDismissal = false; 1258 mCollection.addNotificationDismissInterceptor(mInterceptor1); 1259 mCollection.addNotificationDismissInterceptor(mInterceptor2); 1260 mCollection.addNotificationDismissInterceptor(mInterceptor3); 1261 1262 // GIVEN a collection with one unclearable and one clearable notification 1263 NotifEvent unclearableNotif = mNoMan.postNotif( 1264 buildNotif(TEST_PACKAGE, 47, "myTag") 1265 .setFlag(mContext, FLAG_NO_CLEAR, true)); 1266 NotificationEntry unclearable = mCollectionListener.getEntry(unclearableNotif.key); 1267 NotifEvent clearableNotif = mNoMan.postNotif( 1268 buildNotif(TEST_PACKAGE, 88, "myTag") 1269 .setFlag(mContext, FLAG_NO_CLEAR, false)); 1270 NotificationEntry clearable = mCollectionListener.getEntry(clearableNotif.key); 1271 1272 // WHEN all notifications are dismissed for the user who posted the notif 1273 mCollection.dismissAllNotifications(clearable.getSbn().getUser().getIdentifier()); 1274 1275 // THEN all interceptors get checked for the unclearable notification 1276 verify(mInterceptor1).shouldInterceptDismissal(unclearable); 1277 verify(mInterceptor2).shouldInterceptDismissal(unclearable); 1278 verify(mInterceptor3).shouldInterceptDismissal(unclearable); 1279 assertEquals(List.of(mInterceptor1, mInterceptor2), unclearable.mDismissInterceptors); 1280 1281 // THEN no interceptors get checked for the clearable notification 1282 verify(mInterceptor1, never()).shouldInterceptDismissal(clearable); 1283 verify(mInterceptor2, never()).shouldInterceptDismissal(clearable); 1284 verify(mInterceptor3, never()).shouldInterceptDismissal(clearable); 1285 } 1286 1287 @Test testClearNotificationDoesntThrowIfMissing()1288 public void testClearNotificationDoesntThrowIfMissing() { 1289 // GIVEN that enough time has passed that we're beyond the forgiveness window 1290 mClock.advanceTime(5001); 1291 1292 // WHEN we get a remove event for a notification we don't know about 1293 final NotificationEntry container = new NotificationEntryBuilder() 1294 .setPkg(TEST_PACKAGE) 1295 .setId(47) 1296 .build(); 1297 mNotifHandler.onNotificationRemoved( 1298 container.getSbn(), 1299 new RankingMap(new Ranking[]{ container.getRanking() })); 1300 1301 // THEN the event is ignored 1302 verify(mCollectionListener, never()).onEntryRemoved(any(NotificationEntry.class), anyInt()); 1303 } 1304 1305 @Test testClearNotificationDoesntThrowIfInForgivenessWindow()1306 public void testClearNotificationDoesntThrowIfInForgivenessWindow() { 1307 // GIVEN that some time has passed but we're still within the initialization forgiveness 1308 // window 1309 mClock.advanceTime(4999); 1310 1311 // WHEN we get a remove event for a notification we don't know about 1312 final NotificationEntry container = new NotificationEntryBuilder() 1313 .setPkg(TEST_PACKAGE) 1314 .setId(47) 1315 .build(); 1316 mNotifHandler.onNotificationRemoved( 1317 container.getSbn(), 1318 new RankingMap(new Ranking[]{ container.getRanking() })); 1319 1320 // THEN no exception is thrown, but no event is fired 1321 verify(mCollectionListener, never()).onEntryRemoved(any(NotificationEntry.class), anyInt()); 1322 } 1323 buildNotif(String pkg, int id, String tag)1324 private static NotificationEntryBuilder buildNotif(String pkg, int id, String tag) { 1325 return new NotificationEntryBuilder() 1326 .setPkg(pkg) 1327 .setId(id) 1328 .setTag(tag); 1329 } 1330 buildNotif(String pkg, int id)1331 private static NotificationEntryBuilder buildNotif(String pkg, int id) { 1332 return new NotificationEntryBuilder() 1333 .setPkg(pkg) 1334 .setId(id); 1335 } 1336 defaultStats(NotificationEntry entry)1337 private static DismissedByUserStats defaultStats(NotificationEntry entry) { 1338 return new DismissedByUserStats( 1339 DISMISSAL_SHADE, 1340 DISMISS_SENTIMENT_NEUTRAL, 1341 NotificationVisibility.obtain(entry.getKey(), 7, 2, true)); 1342 } 1343 postNotif(NotificationEntryBuilder builder)1344 private CollectionEvent postNotif(NotificationEntryBuilder builder) { 1345 clearInvocations(mCollectionListener); 1346 NotifEvent rawEvent = mNoMan.postNotif(builder); 1347 verify(mCollectionListener).onEntryAdded(mEntryCaptor.capture()); 1348 return new CollectionEvent(rawEvent, requireNonNull(mEntryCaptor.getValue())); 1349 } 1350 verifyBuiltList(Collection<NotificationEntry> list)1351 private void verifyBuiltList(Collection<NotificationEntry> list) { 1352 verify(mBuildListener).onBuildList(mBuildListCaptor.capture()); 1353 assertEquals(new ArraySet<>(list), new ArraySet<>(mBuildListCaptor.getValue())); 1354 } 1355 1356 private static class RecordingCollectionListener implements NotifCollectionListener { 1357 private final Map<String, NotificationEntry> mLastSeenEntries = new ArrayMap<>(); 1358 1359 @Override onEntryInit(NotificationEntry entry)1360 public void onEntryInit(NotificationEntry entry) { 1361 } 1362 1363 @Override onEntryAdded(NotificationEntry entry)1364 public void onEntryAdded(NotificationEntry entry) { 1365 mLastSeenEntries.put(entry.getKey(), entry); 1366 } 1367 1368 @Override onEntryUpdated(NotificationEntry entry)1369 public void onEntryUpdated(NotificationEntry entry) { 1370 mLastSeenEntries.put(entry.getKey(), entry); 1371 } 1372 1373 @Override onEntryRemoved(NotificationEntry entry, int reason)1374 public void onEntryRemoved(NotificationEntry entry, int reason) { 1375 } 1376 1377 @Override onEntryCleanUp(NotificationEntry entry)1378 public void onEntryCleanUp(NotificationEntry entry) { 1379 } 1380 1381 @Override onRankingApplied()1382 public void onRankingApplied() { 1383 } 1384 1385 @Override onRankingUpdate(RankingMap rankingMap)1386 public void onRankingUpdate(RankingMap rankingMap) { 1387 } 1388 getEntry(String key)1389 public NotificationEntry getEntry(String key) { 1390 if (!mLastSeenEntries.containsKey(key)) { 1391 throw new RuntimeException("Key not found: " + key); 1392 } 1393 return mLastSeenEntries.get(key); 1394 } 1395 } 1396 1397 private static class RecordingLifetimeExtender implements NotifLifetimeExtender { 1398 private final String mName; 1399 1400 public @Nullable OnEndLifetimeExtensionCallback callback; 1401 public boolean shouldExtendLifetime = false; 1402 public @Nullable Runnable onCancelLifetimeExtension; 1403 RecordingLifetimeExtender(String name)1404 private RecordingLifetimeExtender(String name) { 1405 mName = name; 1406 } 1407 1408 @Override getName()1409 public String getName() { 1410 return mName; 1411 } 1412 1413 @Override setCallback(OnEndLifetimeExtensionCallback callback)1414 public void setCallback(OnEndLifetimeExtensionCallback callback) { 1415 this.callback = callback; 1416 } 1417 1418 @Override shouldExtendLifetime( NotificationEntry entry, @CancellationReason int reason)1419 public boolean shouldExtendLifetime( 1420 NotificationEntry entry, 1421 @CancellationReason int reason) { 1422 return shouldExtendLifetime; 1423 } 1424 1425 @Override cancelLifetimeExtension(NotificationEntry entry)1426 public void cancelLifetimeExtension(NotificationEntry entry) { 1427 if (onCancelLifetimeExtension != null) { 1428 onCancelLifetimeExtension.run(); 1429 } 1430 } 1431 } 1432 1433 private static class RecordingDismissInterceptor implements NotifDismissInterceptor { 1434 private final String mName; 1435 1436 public @Nullable OnEndDismissInterception onEndInterceptionCallback; 1437 public boolean shouldInterceptDismissal = false; 1438 RecordingDismissInterceptor(String name)1439 private RecordingDismissInterceptor(String name) { 1440 mName = name; 1441 } 1442 1443 @Override getName()1444 public String getName() { 1445 return mName; 1446 } 1447 1448 @Override setCallback(OnEndDismissInterception callback)1449 public void setCallback(OnEndDismissInterception callback) { 1450 this.onEndInterceptionCallback = callback; 1451 } 1452 1453 @Override shouldInterceptDismissal(NotificationEntry entry)1454 public boolean shouldInterceptDismissal(NotificationEntry entry) { 1455 return shouldInterceptDismissal; 1456 } 1457 1458 @Override cancelDismissInterception(NotificationEntry entry)1459 public void cancelDismissInterception(NotificationEntry entry) { 1460 } 1461 } 1462 1463 /** 1464 * Wrapper around {@link NotifEvent} that adds the NotificationEntry that the collection under 1465 * test creates. 1466 */ 1467 private static class CollectionEvent { 1468 public final String key; 1469 public final StatusBarNotification sbn; 1470 public final Ranking ranking; 1471 public final RankingMap rankingMap; 1472 public final NotificationEntry entry; 1473 CollectionEvent(NotifEvent rawEvent, NotificationEntry entry)1474 private CollectionEvent(NotifEvent rawEvent, NotificationEntry entry) { 1475 this.key = rawEvent.key; 1476 this.sbn = rawEvent.sbn; 1477 this.ranking = rawEvent.ranking; 1478 this.rankingMap = rawEvent.rankingMap; 1479 this.entry = entry; 1480 } 1481 } 1482 1483 private static final String TEST_PACKAGE = "com.android.test.collection"; 1484 private static final String TEST_PACKAGE2 = "com.android.test.collection2"; 1485 1486 private static final String GROUP_1 = "group_1"; 1487 } 1488