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