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