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