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