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