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