• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.getUser().getIdentifier(),
381                 notif2.sbn.getKey(),
382                 stats.dismissalSurface,
383                 stats.dismissalSentiment,
384                 stats.notificationVisibility);
385     }
386 
387     @Test
testDismissedNotificationsAreMarkedAsDismissedLocally()388     public void testDismissedNotificationsAreMarkedAsDismissedLocally() {
389         // GIVEN a collection with a notification
390         NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
391         NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
392 
393         // WHEN a notification is manually dismissed
394         mCollection.dismissNotification(entry1, defaultStats(entry1));
395 
396         // THEN the entry is marked as dismissed locally
397         assertEquals(DISMISSED, entry1.getDismissState());
398     }
399 
400     @Test
testDismissedNotificationsCannotBeLifetimeExtended()401     public void testDismissedNotificationsCannotBeLifetimeExtended() {
402         // GIVEN a collection with a notification and a lifetime extender
403         mCollection.addNotificationLifetimeExtender(mExtender1);
404         NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
405         NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
406 
407         // WHEN a notification is manually dismissed
408         mCollection.dismissNotification(entry1, defaultStats(entry1));
409 
410         // THEN lifetime extenders are never queried
411         verify(mExtender1, never()).shouldExtendLifetime(eq(entry1), anyInt());
412     }
413 
414     @Test
testDismissedNotificationsDoNotTriggerRemovalEvents()415     public void testDismissedNotificationsDoNotTriggerRemovalEvents() {
416         // GIVEN a collection with a notification
417         NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
418         NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
419 
420         // WHEN a notification is manually dismissed
421         mCollection.dismissNotification(entry1, defaultStats(entry1));
422 
423         // THEN onEntryRemoved is not called
424         verify(mCollectionListener, never()).onEntryRemoved(eq(entry1), anyInt());
425     }
426 
427     @Test
testDismissedNotificationsStillAppearInNotificationSet()428     public void testDismissedNotificationsStillAppearInNotificationSet() {
429         // GIVEN a collection with a notification
430         NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
431         NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
432 
433         // WHEN a notification is manually dismissed
434         mCollection.dismissNotification(entry1, defaultStats(entry1));
435 
436         // THEN the dismissed entry still appears in the notification set
437         assertEquals(
438                 new ArraySet<>(singletonList(entry1)),
439                 new ArraySet<>(mCollection.getAllNotifs()));
440     }
441 
442     @Test
testRetractingLifetimeExtendedSummaryDoesNotDismissChildren()443     public void testRetractingLifetimeExtendedSummaryDoesNotDismissChildren() {
444         // GIVEN A notif group with one summary and two children
445         mCollection.addNotificationLifetimeExtender(mExtender1);
446         CollectionEvent notif1 = postNotif(
447                 buildNotif(TEST_PACKAGE, 1, "myTag")
448                         .setGroup(mContext, GROUP_1)
449                         .setGroupSummary(mContext, true));
450         CollectionEvent notif2 = postNotif(
451                 buildNotif(TEST_PACKAGE, 2, "myTag")
452                         .setGroup(mContext, GROUP_1));
453         CollectionEvent notif3 = postNotif(
454                 buildNotif(TEST_PACKAGE, 3, "myTag")
455                         .setGroup(mContext, GROUP_1));
456 
457         NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
458         NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
459         NotificationEntry entry3 = mCollectionListener.getEntry(notif3.key);
460 
461         // GIVEN that the summary and one child are retracted by the app, but both are
462         // lifetime-extended
463         mExtender1.shouldExtendLifetime = true;
464         mNoMan.retractNotif(notif1.sbn, REASON_APP_CANCEL);
465         mNoMan.retractNotif(notif2.sbn, REASON_APP_CANCEL);
466         assertEquals(
467                 new ArraySet<>(List.of(entry1, entry2, entry3)),
468                 new ArraySet<>(mCollection.getAllNotifs()));
469 
470         // WHEN the summary is retracted by the app
471         mCollection.dismissNotification(entry1, defaultStats(entry1));
472 
473         // THEN the summary is removed, but both children stick around
474         assertEquals(
475                 new ArraySet<>(List.of(entry2, entry3)),
476                 new ArraySet<>(mCollection.getAllNotifs()));
477         assertEquals(NOT_DISMISSED, entry2.getDismissState());
478         assertEquals(NOT_DISMISSED, entry3.getDismissState());
479     }
480 
481     @Test
testNMSReportsUserDismissalAlwaysRemovesNotif()482     public void testNMSReportsUserDismissalAlwaysRemovesNotif() throws RemoteException {
483         // GIVEN notifications are lifetime extended
484         mExtender1.shouldExtendLifetime = true;
485         CollectionEvent notif = postNotif(buildNotif(TEST_PACKAGE, 1, "myTag"));
486         CollectionEvent notif2 = postNotif(buildNotif(TEST_PACKAGE, 2, "myTag"));
487         NotificationEntry entry = mCollectionListener.getEntry(notif.key);
488         NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
489         assertEquals(
490                 new ArraySet<>(List.of(entry, entry2)),
491                 new ArraySet<>(mCollection.getAllNotifs()));
492 
493         // WHEN the notifications are reported to be dismissed by the user by NMS
494         mNoMan.retractNotif(notif.sbn, REASON_CANCEL);
495         mNoMan.retractNotif(notif2.sbn, REASON_CLICK);
496 
497         // THEN the notifications are removed b/c they were dismissed by the user
498         assertEquals(
499                 new ArraySet<>(List.of()),
500                 new ArraySet<>(mCollection.getAllNotifs()));
501     }
502 
503     @Test
testDismissNotificationCallsDismissInterceptors()504     public void testDismissNotificationCallsDismissInterceptors() throws RemoteException {
505         // GIVEN a collection with notifications with multiple dismiss interceptors
506         mInterceptor1.shouldInterceptDismissal = true;
507         mInterceptor2.shouldInterceptDismissal = true;
508         mInterceptor3.shouldInterceptDismissal = false;
509         mCollection.addNotificationDismissInterceptor(mInterceptor1);
510         mCollection.addNotificationDismissInterceptor(mInterceptor2);
511         mCollection.addNotificationDismissInterceptor(mInterceptor3);
512 
513         NotifEvent notif = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
514         NotificationEntry entry = mCollectionListener.getEntry(notif.key);
515 
516         // WHEN a notification is manually dismissed
517         DismissedByUserStats stats = defaultStats(entry);
518         mCollection.dismissNotification(entry, stats);
519 
520         // THEN all interceptors get checked
521         verify(mInterceptor1).shouldInterceptDismissal(entry);
522         verify(mInterceptor2).shouldInterceptDismissal(entry);
523         verify(mInterceptor3).shouldInterceptDismissal(entry);
524         assertEquals(List.of(mInterceptor1, mInterceptor2), entry.mDismissInterceptors);
525 
526         // THEN we never send the dismissal to system server
527         verify(mStatusBarService, never()).onNotificationClear(
528                 notif.sbn.getPackageName(),
529                 notif.sbn.getUser().getIdentifier(),
530                 notif.sbn.getKey(),
531                 stats.dismissalSurface,
532                 stats.dismissalSentiment,
533                 stats.notificationVisibility);
534     }
535 
536     @Test
testDismissInterceptorsCanceledWhenNotifIsUpdated()537     public void testDismissInterceptorsCanceledWhenNotifIsUpdated() throws RemoteException {
538         // GIVEN a few lifetime extenders and a couple notifications
539         mCollection.addNotificationDismissInterceptor(mInterceptor1);
540         mCollection.addNotificationDismissInterceptor(mInterceptor2);
541 
542         mInterceptor1.shouldInterceptDismissal = true;
543         mInterceptor2.shouldInterceptDismissal = true;
544 
545         NotifEvent notif = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
546         NotificationEntry entry = mCollectionListener.getEntry(notif.key);
547 
548         // WHEN a notification is manually dismissed and intercepted
549         DismissedByUserStats stats = defaultStats(entry);
550         mCollection.dismissNotification(entry, stats);
551         assertEquals(List.of(mInterceptor1, mInterceptor2), entry.mDismissInterceptors);
552         clearInvocations(mInterceptor1, mInterceptor2);
553 
554         // WHEN the notification is reposted
555         mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
556 
557         // THEN all of the active dismissal interceptors are canceled
558         verify(mInterceptor1).cancelDismissInterception(entry);
559         verify(mInterceptor2).cancelDismissInterception(entry);
560         assertEquals(List.of(), entry.mDismissInterceptors);
561 
562         // THEN the notification is never sent to system server to dismiss
563         verify(mStatusBarService, never()).onNotificationClear(
564                 eq(notif.sbn.getPackageName()),
565                 eq(notif.sbn.getUser().getIdentifier()),
566                 eq(notif.sbn.getKey()),
567                 anyInt(),
568                 anyInt(),
569                 eq(stats.notificationVisibility));
570     }
571 
572     @Test
testEndingAllDismissInterceptorsSendsDismiss()573     public void testEndingAllDismissInterceptorsSendsDismiss() throws RemoteException {
574         // GIVEN a collection with notifications a dismiss interceptor
575         mInterceptor1.shouldInterceptDismissal = true;
576         mCollection.addNotificationDismissInterceptor(mInterceptor1);
577 
578         NotifEvent notif = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
579         NotificationEntry entry = mCollectionListener.getEntry(notif.key);
580 
581         // GIVEN a notification is manually dismissed
582         DismissedByUserStats stats = defaultStats(entry);
583         mCollection.dismissNotification(entry, stats);
584 
585         // WHEN all interceptors end their interception dismissal
586         mInterceptor1.shouldInterceptDismissal = false;
587         mInterceptor1.onEndInterceptionCallback.onEndDismissInterception(mInterceptor1, entry,
588                 stats);
589 
590         // THEN we send the dismissal to system server
591         verify(mStatusBarService).onNotificationClear(
592                 eq(notif.sbn.getPackageName()),
593                 eq(notif.sbn.getUser().getIdentifier()),
594                 eq(notif.sbn.getKey()),
595                 anyInt(),
596                 anyInt(),
597                 eq(stats.notificationVisibility));
598     }
599 
600     @Test
testEndDismissInterceptionUpdatesDismissInterceptors()601     public void testEndDismissInterceptionUpdatesDismissInterceptors() {
602         // GIVEN a collection with notifications with multiple dismiss interceptors
603         mInterceptor1.shouldInterceptDismissal = true;
604         mInterceptor2.shouldInterceptDismissal = true;
605         mInterceptor3.shouldInterceptDismissal = false;
606         mCollection.addNotificationDismissInterceptor(mInterceptor1);
607         mCollection.addNotificationDismissInterceptor(mInterceptor2);
608         mCollection.addNotificationDismissInterceptor(mInterceptor3);
609 
610         NotifEvent notif = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
611         NotificationEntry entry = mCollectionListener.getEntry(notif.key);
612 
613         // GIVEN a notification is manually dismissed
614         mCollection.dismissNotification(entry, defaultStats(entry));
615 
616        // WHEN an interceptor ends its interception
617         mInterceptor1.shouldInterceptDismissal = false;
618         mInterceptor1.onEndInterceptionCallback.onEndDismissInterception(mInterceptor1, entry,
619                 defaultStats(entry));
620 
621         // THEN all interceptors get checked
622         verify(mInterceptor1).shouldInterceptDismissal(entry);
623         verify(mInterceptor2).shouldInterceptDismissal(entry);
624         verify(mInterceptor3).shouldInterceptDismissal(entry);
625 
626         // THEN mInterceptor2 is the only dismiss interceptor
627         assertEquals(List.of(mInterceptor2), entry.mDismissInterceptors);
628     }
629 
630 
631     @Test(expected = IllegalStateException.class)
testEndingDismissalOfNonInterceptedThrows()632     public void testEndingDismissalOfNonInterceptedThrows() {
633         // GIVEN a collection with notifications with a dismiss interceptor that hasn't been called
634         mInterceptor1.shouldInterceptDismissal = false;
635         mCollection.addNotificationDismissInterceptor(mInterceptor1);
636 
637         NotifEvent notif = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
638         NotificationEntry entry = mCollectionListener.getEntry(notif.key);
639 
640         // WHEN we try to end the dismissal of an interceptor that didn't intercept the notif
641         mInterceptor1.onEndInterceptionCallback.onEndDismissInterception(mInterceptor1, entry,
642                 defaultStats(entry));
643 
644         // THEN an exception is thrown
645     }
646 
647     @Test(expected = IllegalStateException.class)
testDismissingNonExistentNotificationThrows()648     public void testDismissingNonExistentNotificationThrows() {
649         // GIVEN a collection that originally had three notifs, but where one was dismissed
650         NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
651         NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
652         NotifEvent notif3 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 99));
653         NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
654         mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
655 
656         // WHEN we try to dismiss a notification that isn't present
657         mCollection.dismissNotification(entry2, defaultStats(entry2));
658 
659         // THEN an exception is thrown
660     }
661 
662     @Test
testGroupChildrenAreDismissedLocallyWhenSummaryIsDismissed()663     public void testGroupChildrenAreDismissedLocallyWhenSummaryIsDismissed() {
664         // GIVEN a collection with two grouped notifs in it
665         CollectionEvent notif0 = postNotif(
666                 buildNotif(TEST_PACKAGE, 0)
667                         .setGroup(mContext, GROUP_1)
668                         .setGroupSummary(mContext, true));
669         CollectionEvent notif1 = postNotif(
670                 buildNotif(TEST_PACKAGE, 1)
671                         .setGroup(mContext, GROUP_1));
672         NotificationEntry entry0 = mCollectionListener.getEntry(notif0.key);
673         NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
674 
675         // WHEN the summary is dismissed
676         mCollection.dismissNotification(entry0, defaultStats(entry0));
677 
678         // THEN all members of the group are marked as dismissed locally
679         assertEquals(DISMISSED, entry0.getDismissState());
680         assertEquals(PARENT_DISMISSED, entry1.getDismissState());
681     }
682 
683     @Test
testUpdatingDismissedSummaryBringsChildrenBack()684     public void testUpdatingDismissedSummaryBringsChildrenBack() {
685         // GIVEN a collection with two grouped notifs in it
686         CollectionEvent notif0 = postNotif(
687                 buildNotif(TEST_PACKAGE, 0)
688                         .setGroup(mContext, GROUP_1)
689                         .setGroupSummary(mContext, true));
690         CollectionEvent notif1 = postNotif(
691                 buildNotif(TEST_PACKAGE, 1)
692                         .setGroup(mContext, GROUP_1));
693         NotificationEntry entry0 = mCollectionListener.getEntry(notif0.key);
694         NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
695 
696         // WHEN the summary is dismissed but then reposted without a group
697         mCollection.dismissNotification(entry0, defaultStats(entry0));
698         NotifEvent notif0a = mNoMan.postNotif(
699                 buildNotif(TEST_PACKAGE, 0));
700 
701         // THEN it and all of its previous children are no longer dismissed locally
702         assertEquals(NOT_DISMISSED, entry0.getDismissState());
703         assertEquals(NOT_DISMISSED, entry1.getDismissState());
704     }
705 
706     @Test
testDismissedChildrenAreNotResetByParentUpdate()707     public void testDismissedChildrenAreNotResetByParentUpdate() {
708         // GIVEN a collection with three grouped notifs in it
709         CollectionEvent notif0 = postNotif(
710                 buildNotif(TEST_PACKAGE, 0)
711                         .setGroup(mContext, GROUP_1)
712                         .setGroupSummary(mContext, true));
713         CollectionEvent notif1 = postNotif(
714                 buildNotif(TEST_PACKAGE, 1)
715                         .setGroup(mContext, GROUP_1));
716         CollectionEvent notif2 = postNotif(
717                 buildNotif(TEST_PACKAGE, 2)
718                         .setGroup(mContext, GROUP_1));
719         NotificationEntry entry0 = mCollectionListener.getEntry(notif0.key);
720         NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
721         NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
722 
723         // WHEN a child is dismissed, then the parent is dismissed, then the parent is updated
724         mCollection.dismissNotification(entry1, defaultStats(entry1));
725         mCollection.dismissNotification(entry0, defaultStats(entry0));
726         NotifEvent notif0a = mNoMan.postNotif(
727                 buildNotif(TEST_PACKAGE, 0));
728 
729         // THEN the manually-dismissed child is still marked as dismissed
730         assertEquals(NOT_DISMISSED, entry0.getDismissState());
731         assertEquals(DISMISSED, entry1.getDismissState());
732         assertEquals(NOT_DISMISSED, entry2.getDismissState());
733     }
734 
735     @Test
testUpdatingGroupKeyOfDismissedSummaryBringsChildrenBack()736     public void testUpdatingGroupKeyOfDismissedSummaryBringsChildrenBack() {
737         // GIVEN a collection with two grouped notifs in it
738         CollectionEvent notif0 = postNotif(
739                 buildNotif(TEST_PACKAGE, 0)
740                         .setOverrideGroupKey(GROUP_1)
741                         .setGroupSummary(mContext, true));
742         CollectionEvent notif1 = postNotif(
743                 buildNotif(TEST_PACKAGE, 1)
744                         .setOverrideGroupKey(GROUP_1));
745         NotificationEntry entry0 = mCollectionListener.getEntry(notif0.key);
746         NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
747 
748         // WHEN the summary is dismissed but then reposted AND in the same update one of the
749         // children's ranking loses its override group
750         mCollection.dismissNotification(entry0, defaultStats(entry0));
751         mNoMan.setRanking(entry1.getKey(), new RankingBuilder()
752                 .setKey(entry1.getKey())
753                 .build());
754         mNoMan.postNotif(
755                 buildNotif(TEST_PACKAGE, 0)
756                         .setOverrideGroupKey(GROUP_1)
757                         .setGroupSummary(mContext, true));
758 
759         // THEN it and all of its previous children are no longer dismissed locally, including the
760         // child that is no longer part of the group
761         assertEquals(NOT_DISMISSED, entry0.getDismissState());
762         assertEquals(NOT_DISMISSED, entry1.getDismissState());
763     }
764 
765     @Test
testDismissingSummaryDoesNotDismissForegroundServiceChildren()766     public void testDismissingSummaryDoesNotDismissForegroundServiceChildren() {
767         // GIVEN a collection with three grouped notifs in it
768         CollectionEvent notif0 = postNotif(
769                 buildNotif(TEST_PACKAGE, 0)
770                         .setGroup(mContext, GROUP_1)
771                         .setGroupSummary(mContext, true));
772         CollectionEvent notif1 = postNotif(
773                 buildNotif(TEST_PACKAGE, 1)
774                         .setGroup(mContext, GROUP_1)
775                         .setFlag(mContext, Notification.FLAG_FOREGROUND_SERVICE, true));
776         CollectionEvent notif2 = postNotif(
777                 buildNotif(TEST_PACKAGE, 2)
778                         .setGroup(mContext, GROUP_1));
779 
780         // WHEN the summary is dismissed
781         mCollection.dismissNotification(notif0.entry, defaultStats(notif0.entry));
782 
783         // THEN the foreground service child is not dismissed
784         assertEquals(DISMISSED, notif0.entry.getDismissState());
785         assertEquals(NOT_DISMISSED, notif1.entry.getDismissState());
786         assertEquals(PARENT_DISMISSED, notif2.entry.getDismissState());
787     }
788 
789     @Test
testDismissingSummaryDoesNotDismissBubbledChildren()790     public void testDismissingSummaryDoesNotDismissBubbledChildren() {
791         // GIVEN a collection with three grouped notifs in it
792         CollectionEvent notif0 = postNotif(
793                 buildNotif(TEST_PACKAGE, 0)
794                         .setGroup(mContext, GROUP_1)
795                         .setGroupSummary(mContext, true));
796         CollectionEvent notif1 = postNotif(
797                 buildNotif(TEST_PACKAGE, 1)
798                         .setGroup(mContext, GROUP_1)
799                         .setFlag(mContext, Notification.FLAG_BUBBLE, true));
800         CollectionEvent notif2 = postNotif(
801                 buildNotif(TEST_PACKAGE, 2)
802                         .setGroup(mContext, GROUP_1));
803 
804         // WHEN the summary is dismissed
805         mCollection.dismissNotification(notif0.entry, defaultStats(notif0.entry));
806 
807         // THEN the bubbled child is not dismissed
808         assertEquals(DISMISSED, notif0.entry.getDismissState());
809         assertEquals(NOT_DISMISSED, notif1.entry.getDismissState());
810         assertEquals(PARENT_DISMISSED, notif2.entry.getDismissState());
811     }
812 
813     @Test
testDismissingSummaryDoesNotDismissDuplicateSummaries()814     public void testDismissingSummaryDoesNotDismissDuplicateSummaries() {
815         // GIVEN a group with a two summaries
816         CollectionEvent notif0 = postNotif(
817                 buildNotif(TEST_PACKAGE, 0)
818                         .setGroup(mContext, GROUP_1)
819                         .setGroupSummary(mContext, true));
820         CollectionEvent notif1 = postNotif(
821                 buildNotif(TEST_PACKAGE, 1)
822                         .setGroup(mContext, GROUP_1)
823                         .setGroupSummary(mContext, true));
824         CollectionEvent notif2 = postNotif(
825                 buildNotif(TEST_PACKAGE, 2)
826                         .setGroup(mContext, GROUP_1));
827 
828         // WHEN the first summary is dismissed
829         mCollection.dismissNotification(notif0.entry, defaultStats(notif0.entry));
830 
831         // THEN the second summary is not auto-dismissed (but the child is)
832         assertEquals(DISMISSED, notif0.entry.getDismissState());
833         assertEquals(NOT_DISMISSED, notif1.entry.getDismissState());
834         assertEquals(PARENT_DISMISSED, notif2.entry.getDismissState());
835     }
836 
837     @Test
testLifetimeExtendersAreQueriedWhenNotifRemoved()838     public void testLifetimeExtendersAreQueriedWhenNotifRemoved() {
839         // GIVEN a couple notifications and a few lifetime extenders
840         mExtender1.shouldExtendLifetime = true;
841         mExtender2.shouldExtendLifetime = true;
842 
843         mCollection.addNotificationLifetimeExtender(mExtender1);
844         mCollection.addNotificationLifetimeExtender(mExtender2);
845         mCollection.addNotificationLifetimeExtender(mExtender3);
846 
847         NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
848         NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
849         NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
850 
851         // WHEN a notification is removed by the app
852         mNoMan.retractNotif(notif2.sbn, REASON_APP_CANCEL);
853 
854         // THEN each extender is asked whether to extend, even if earlier ones return true
855         verify(mExtender1).shouldExtendLifetime(entry2, REASON_APP_CANCEL);
856         verify(mExtender2).shouldExtendLifetime(entry2, REASON_APP_CANCEL);
857         verify(mExtender3).shouldExtendLifetime(entry2, REASON_APP_CANCEL);
858 
859         // THEN the entry is not removed
860         assertTrue(mCollection.getAllNotifs().contains(entry2));
861 
862         // THEN the entry properly records all extenders that returned true
863         assertEquals(Arrays.asList(mExtender1, mExtender2), entry2.mLifetimeExtenders);
864     }
865 
866     @Test
testWhenLastLifetimeExtenderExpiresAllAreReQueried()867     public void testWhenLastLifetimeExtenderExpiresAllAreReQueried() {
868         // GIVEN a couple notifications and a few lifetime extenders
869         mExtender2.shouldExtendLifetime = true;
870 
871         mCollection.addNotificationLifetimeExtender(mExtender1);
872         mCollection.addNotificationLifetimeExtender(mExtender2);
873         mCollection.addNotificationLifetimeExtender(mExtender3);
874 
875         NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
876         NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
877         NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
878 
879         // GIVEN a notification gets lifetime-extended by one of them
880         mNoMan.retractNotif(notif2.sbn, REASON_APP_CANCEL);
881         assertTrue(mCollection.getAllNotifs().contains(entry2));
882         clearInvocations(mExtender1, mExtender2, mExtender3);
883 
884         // WHEN the last active extender expires (but new ones become active)
885         mExtender1.shouldExtendLifetime = true;
886         mExtender2.shouldExtendLifetime = false;
887         mExtender3.shouldExtendLifetime = true;
888         mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2);
889 
890         // THEN each extender is re-queried
891         verify(mExtender1).shouldExtendLifetime(entry2, REASON_APP_CANCEL);
892         verify(mExtender2).shouldExtendLifetime(entry2, REASON_APP_CANCEL);
893         verify(mExtender3).shouldExtendLifetime(entry2, REASON_APP_CANCEL);
894 
895         // THEN the entry is not removed
896         assertTrue(mCollection.getAllNotifs().contains(entry2));
897 
898         // THEN the entry properly records all extenders that returned true
899         assertEquals(Arrays.asList(mExtender1, mExtender3), entry2.mLifetimeExtenders);
900     }
901 
902     @Test
testExtendersAreNotReQueriedUntilFinalActiveExtenderExpires()903     public void testExtendersAreNotReQueriedUntilFinalActiveExtenderExpires() {
904         // GIVEN a couple notifications and a few lifetime extenders
905         mExtender1.shouldExtendLifetime = true;
906         mExtender2.shouldExtendLifetime = true;
907 
908         mCollection.addNotificationLifetimeExtender(mExtender1);
909         mCollection.addNotificationLifetimeExtender(mExtender2);
910         mCollection.addNotificationLifetimeExtender(mExtender3);
911 
912         NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
913         NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
914         NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
915 
916         // GIVEN a notification gets lifetime-extended by a couple of them
917         mNoMan.retractNotif(notif2.sbn, REASON_APP_CANCEL);
918         assertTrue(mCollection.getAllNotifs().contains(entry2));
919         clearInvocations(mExtender1, mExtender2, mExtender3);
920 
921         // WHEN one (but not all) of the extenders expires
922         mExtender2.shouldExtendLifetime = false;
923         mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2);
924 
925         // THEN the entry is not removed
926         assertTrue(mCollection.getAllNotifs().contains(entry2));
927 
928         // THEN we don't re-query the extenders
929         verify(mExtender1, never()).shouldExtendLifetime(entry2, REASON_APP_CANCEL);
930         verify(mExtender2, never()).shouldExtendLifetime(entry2, REASON_APP_CANCEL);
931         verify(mExtender3, never()).shouldExtendLifetime(entry2, REASON_APP_CANCEL);
932 
933         // THEN the entry properly records all extenders that returned true
934         assertEquals(singletonList(mExtender1), entry2.mLifetimeExtenders);
935     }
936 
937     @Test
testNotificationIsRemovedWhenAllLifetimeExtendersExpire()938     public void testNotificationIsRemovedWhenAllLifetimeExtendersExpire() {
939         // GIVEN a couple notifications and a few lifetime extenders
940         mExtender1.shouldExtendLifetime = true;
941         mExtender2.shouldExtendLifetime = true;
942 
943         mCollection.addNotificationLifetimeExtender(mExtender1);
944         mCollection.addNotificationLifetimeExtender(mExtender2);
945         mCollection.addNotificationLifetimeExtender(mExtender3);
946 
947         NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
948         NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
949         NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
950 
951         // GIVEN a notification gets lifetime-extended by a couple of them
952         mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
953         assertTrue(mCollection.getAllNotifs().contains(entry2));
954         clearInvocations(mExtender1, mExtender2, mExtender3);
955 
956         // WHEN all of the active extenders expire
957         mExtender2.shouldExtendLifetime = false;
958         mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2);
959         mExtender1.shouldExtendLifetime = false;
960         mExtender1.callback.onEndLifetimeExtension(mExtender1, entry2);
961 
962         // THEN the entry removed
963         assertFalse(mCollection.getAllNotifs().contains(entry2));
964         verify(mCollectionListener).onEntryRemoved(entry2, REASON_UNKNOWN);
965     }
966 
967     @Test
testLifetimeExtensionIsCanceledWhenNotifIsUpdated()968     public void testLifetimeExtensionIsCanceledWhenNotifIsUpdated() {
969         // GIVEN a few lifetime extenders and a couple notifications
970         mCollection.addNotificationLifetimeExtender(mExtender1);
971         mCollection.addNotificationLifetimeExtender(mExtender2);
972         mCollection.addNotificationLifetimeExtender(mExtender3);
973 
974         mExtender1.shouldExtendLifetime = true;
975         mExtender2.shouldExtendLifetime = true;
976 
977         NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
978         NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
979         NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
980 
981         // GIVEN a notification gets lifetime-extended by a couple of them
982         mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
983         assertTrue(mCollection.getAllNotifs().contains(entry2));
984         clearInvocations(mExtender1, mExtender2, mExtender3);
985 
986         // WHEN the notification is reposted
987         mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
988 
989         // THEN all of the active lifetime extenders are canceled
990         verify(mExtender1).cancelLifetimeExtension(entry2);
991         verify(mExtender2).cancelLifetimeExtension(entry2);
992 
993         // THEN the notification is still present
994         assertTrue(mCollection.getAllNotifs().contains(entry2));
995     }
996 
997     @Test(expected = IllegalStateException.class)
testReentrantCallsToLifetimeExtendersThrow()998     public void testReentrantCallsToLifetimeExtendersThrow() {
999         // GIVEN a few lifetime extenders and a couple notifications
1000         mCollection.addNotificationLifetimeExtender(mExtender1);
1001         mCollection.addNotificationLifetimeExtender(mExtender2);
1002         mCollection.addNotificationLifetimeExtender(mExtender3);
1003 
1004         mExtender1.shouldExtendLifetime = true;
1005         mExtender2.shouldExtendLifetime = true;
1006 
1007         NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
1008         NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
1009         NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
1010 
1011         // GIVEN a notification gets lifetime-extended by a couple of them
1012         mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
1013         assertTrue(mCollection.getAllNotifs().contains(entry2));
1014         clearInvocations(mExtender1, mExtender2, mExtender3);
1015 
1016         // WHEN a lifetime extender makes a reentrant call during cancelLifetimeExtension()
1017         mExtender2.onCancelLifetimeExtension = () -> {
1018             mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2);
1019         };
1020         // This triggers the call to cancelLifetimeExtension()
1021         mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
1022 
1023         // THEN an exception is thrown
1024     }
1025 
1026     @Test
testRankingIsUpdatedWhenALifetimeExtendedNotifIsReposted()1027     public void testRankingIsUpdatedWhenALifetimeExtendedNotifIsReposted() {
1028         // GIVEN a few lifetime extenders and a couple notifications
1029         mCollection.addNotificationLifetimeExtender(mExtender1);
1030         mCollection.addNotificationLifetimeExtender(mExtender2);
1031         mCollection.addNotificationLifetimeExtender(mExtender3);
1032 
1033         mExtender1.shouldExtendLifetime = true;
1034         mExtender2.shouldExtendLifetime = true;
1035 
1036         NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
1037         NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
1038         NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
1039 
1040         // GIVEN a notification gets lifetime-extended by a couple of them
1041         mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
1042         assertTrue(mCollection.getAllNotifs().contains(entry2));
1043         clearInvocations(mExtender1, mExtender2, mExtender3);
1044 
1045         // WHEN the notification is reposted
1046         NotifEvent notif2a = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88)
1047                 .setRank(4747)
1048                 .setExplanation("Some new explanation"));
1049 
1050         // THEN the notification's ranking is properly updated
1051         assertEquals(notif2a.ranking, entry2.getRanking());
1052     }
1053 
1054     @Test
testCancellationReasonIsSetWhenNotifIsCancelled()1055     public void testCancellationReasonIsSetWhenNotifIsCancelled() {
1056         // GIVEN a notification
1057         NotifEvent notif0 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3));
1058         NotificationEntry entry0 = mCollectionListener.getEntry(notif0.key);
1059 
1060         // WHEN the notification is retracted
1061         mNoMan.retractNotif(notif0.sbn, REASON_APP_CANCEL);
1062 
1063         // THEN the retraction reason is stored on the notif
1064         assertEquals(REASON_APP_CANCEL, entry0.mCancellationReason);
1065     }
1066 
1067     @Test
testCancellationReasonIsClearedWhenNotifIsUpdated()1068     public void testCancellationReasonIsClearedWhenNotifIsUpdated() {
1069         // GIVEN a notification and a lifetime extender that will preserve it
1070         NotifEvent notif0 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3));
1071         NotificationEntry entry0 = mCollectionListener.getEntry(notif0.key);
1072         mCollection.addNotificationLifetimeExtender(mExtender1);
1073         mExtender1.shouldExtendLifetime = true;
1074 
1075         // WHEN the notification is retracted and subsequently reposted
1076         mNoMan.retractNotif(notif0.sbn, REASON_APP_CANCEL);
1077         assertEquals(REASON_APP_CANCEL, entry0.mCancellationReason);
1078         mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3));
1079 
1080         // THEN the notification has its cancellation reason cleared
1081         assertEquals(REASON_NOT_CANCELED, entry0.mCancellationReason);
1082     }
1083 
1084     @Test
testDismissNotificationsRebuildsOnce()1085     public void testDismissNotificationsRebuildsOnce() {
1086         // GIVEN a collection with a couple notifications
1087         NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
1088         NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag"));
1089         NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
1090         NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
1091         clearInvocations(mBuildListener);
1092 
1093         // WHEN both notifications are manually dismissed together
1094         mCollection.dismissNotifications(
1095                 List.of(new Pair<>(entry1, defaultStats(entry1)),
1096                         new Pair<>(entry2, defaultStats(entry2))));
1097 
1098         // THEN build list is only called one time
1099         verifyBuiltList(List.of(entry1, entry2));
1100     }
1101 
1102     @Test
testDismissNotificationsSentToSystemServer()1103     public void testDismissNotificationsSentToSystemServer() throws RemoteException {
1104         // GIVEN a collection with a couple notifications
1105         NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
1106         NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag"));
1107         NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
1108         NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
1109 
1110         // WHEN both notifications are manually dismissed together
1111         DismissedByUserStats stats1 = defaultStats(entry1);
1112         DismissedByUserStats stats2 = defaultStats(entry2);
1113         mCollection.dismissNotifications(
1114                 List.of(new Pair<>(entry1, defaultStats(entry1)),
1115                         new Pair<>(entry2, defaultStats(entry2))));
1116 
1117         // THEN we send the dismissals to system server
1118         verify(mStatusBarService).onNotificationClear(
1119                 notif1.sbn.getPackageName(),
1120                 notif1.sbn.getUser().getIdentifier(),
1121                 notif1.sbn.getKey(),
1122                 stats1.dismissalSurface,
1123                 stats1.dismissalSentiment,
1124                 stats1.notificationVisibility);
1125 
1126         verify(mStatusBarService).onNotificationClear(
1127                 notif2.sbn.getPackageName(),
1128                 notif2.sbn.getUser().getIdentifier(),
1129                 notif2.sbn.getKey(),
1130                 stats2.dismissalSurface,
1131                 stats2.dismissalSentiment,
1132                 stats2.notificationVisibility);
1133     }
1134 
1135     @Test
testDismissNotificationsMarkedAsDismissed()1136     public void testDismissNotificationsMarkedAsDismissed() {
1137         // GIVEN a collection with a couple notifications
1138         NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
1139         NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag"));
1140         NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
1141         NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
1142 
1143         // WHEN both notifications are manually dismissed together
1144         mCollection.dismissNotifications(
1145                 List.of(new Pair<>(entry1, defaultStats(entry1)),
1146                         new Pair<>(entry2, defaultStats(entry2))));
1147 
1148         // THEN the entries are marked as dismissed
1149         assertEquals(DISMISSED, entry1.getDismissState());
1150         assertEquals(DISMISSED, entry2.getDismissState());
1151     }
1152 
1153     @Test
testDismissNotificationssCallsDismissInterceptors()1154     public void testDismissNotificationssCallsDismissInterceptors() {
1155         // GIVEN a collection with notifications with multiple dismiss interceptors
1156         mInterceptor1.shouldInterceptDismissal = true;
1157         mInterceptor2.shouldInterceptDismissal = true;
1158         mInterceptor3.shouldInterceptDismissal = false;
1159         mCollection.addNotificationDismissInterceptor(mInterceptor1);
1160         mCollection.addNotificationDismissInterceptor(mInterceptor2);
1161         mCollection.addNotificationDismissInterceptor(mInterceptor3);
1162 
1163         NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
1164         NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag"));
1165         NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
1166         NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
1167 
1168         // WHEN both notifications are manually dismissed together
1169         mCollection.dismissNotifications(
1170                 List.of(new Pair<>(entry1, defaultStats(entry1)),
1171                         new Pair<>(entry2, defaultStats(entry2))));
1172 
1173         // THEN all interceptors get checked
1174         verify(mInterceptor1).shouldInterceptDismissal(entry1);
1175         verify(mInterceptor2).shouldInterceptDismissal(entry1);
1176         verify(mInterceptor3).shouldInterceptDismissal(entry1);
1177         verify(mInterceptor1).shouldInterceptDismissal(entry2);
1178         verify(mInterceptor2).shouldInterceptDismissal(entry2);
1179         verify(mInterceptor3).shouldInterceptDismissal(entry2);
1180 
1181         assertEquals(List.of(mInterceptor1, mInterceptor2), entry1.mDismissInterceptors);
1182         assertEquals(List.of(mInterceptor1, mInterceptor2), entry2.mDismissInterceptors);
1183     }
1184 
1185     @Test
testDismissAllNotificationsCallsRebuildOnce()1186     public void testDismissAllNotificationsCallsRebuildOnce() {
1187         // GIVEN a collection with a couple notifications
1188         NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
1189         NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag"));
1190         NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
1191         NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
1192         clearInvocations(mBuildListener);
1193 
1194         // WHEN all notifications are dismissed for the user who posted both notifs
1195         mCollection.dismissAllNotifications(entry1.getSbn().getUser().getIdentifier());
1196 
1197         // THEN build list is only called one time
1198         verifyBuiltList(List.of(entry1, entry2));
1199     }
1200 
1201     @Test
testDismissAllNotificationsSentToSystemServer()1202     public void testDismissAllNotificationsSentToSystemServer() throws RemoteException {
1203         // GIVEN a collection with a couple notifications
1204         NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
1205         NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag"));
1206         NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
1207         NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
1208 
1209         // WHEN all notifications are dismissed for the user who posted both notifs
1210         mCollection.dismissAllNotifications(entry1.getSbn().getUser().getIdentifier());
1211 
1212         // THEN we send the dismissal to system server
1213         verify(mStatusBarService).onClearAllNotifications(
1214                 entry1.getSbn().getUser().getIdentifier());
1215     }
1216 
1217     @Test
testDismissAllNotificationsMarkedAsDismissed()1218     public void testDismissAllNotificationsMarkedAsDismissed() {
1219         // GIVEN a collection with a couple notifications
1220         NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
1221         NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag"));
1222         NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
1223         NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
1224 
1225         // WHEN all notifications are dismissed for the user who posted both notifs
1226         mCollection.dismissAllNotifications(entry1.getSbn().getUser().getIdentifier());
1227 
1228         // THEN the entries are marked as dismissed
1229         assertEquals(DISMISSED, entry1.getDismissState());
1230         assertEquals(DISMISSED, entry2.getDismissState());
1231     }
1232 
1233     @Test
testDismissAllNotificationsDoesNotMarkDismissedUnclearableNotifs()1234     public void testDismissAllNotificationsDoesNotMarkDismissedUnclearableNotifs() {
1235         // GIVEN a collection with one unclearable notification and one clearable notification
1236         NotificationEntryBuilder notifEntryBuilder = buildNotif(TEST_PACKAGE, 47, "myTag");
1237         notifEntryBuilder.modifyNotification(mContext)
1238                 .setFlag(FLAG_NO_CLEAR, true);
1239         NotifEvent unclearabeNotif = mNoMan.postNotif(notifEntryBuilder);
1240         NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag"));
1241         NotificationEntry unclearableEntry = mCollectionListener.getEntry(unclearabeNotif.key);
1242         NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
1243 
1244         // WHEN all notifications are dismissed for the user who posted both notifs
1245         mCollection.dismissAllNotifications(unclearableEntry.getSbn().getUser().getIdentifier());
1246 
1247         // THEN only the clearable entry is marked as dismissed
1248         assertEquals(NOT_DISMISSED, unclearableEntry.getDismissState());
1249         assertEquals(DISMISSED, entry2.getDismissState());
1250     }
1251 
1252     @Test
testDismissAllNotificationsCallsDismissInterceptorsOnlyOnUnclearableNotifs()1253     public void testDismissAllNotificationsCallsDismissInterceptorsOnlyOnUnclearableNotifs() {
1254         // GIVEN a collection with multiple dismiss interceptors
1255         mInterceptor1.shouldInterceptDismissal = true;
1256         mInterceptor2.shouldInterceptDismissal = true;
1257         mInterceptor3.shouldInterceptDismissal = false;
1258         mCollection.addNotificationDismissInterceptor(mInterceptor1);
1259         mCollection.addNotificationDismissInterceptor(mInterceptor2);
1260         mCollection.addNotificationDismissInterceptor(mInterceptor3);
1261 
1262         // GIVEN a collection with one unclearable and one clearable notification
1263         NotifEvent unclearableNotif = mNoMan.postNotif(
1264                 buildNotif(TEST_PACKAGE, 47, "myTag")
1265                         .setFlag(mContext, FLAG_NO_CLEAR, true));
1266         NotificationEntry unclearable = mCollectionListener.getEntry(unclearableNotif.key);
1267         NotifEvent clearableNotif = mNoMan.postNotif(
1268                 buildNotif(TEST_PACKAGE, 88, "myTag")
1269                         .setFlag(mContext, FLAG_NO_CLEAR, false));
1270         NotificationEntry clearable = mCollectionListener.getEntry(clearableNotif.key);
1271 
1272         // WHEN all notifications are dismissed for the user who posted the notif
1273         mCollection.dismissAllNotifications(clearable.getSbn().getUser().getIdentifier());
1274 
1275         // THEN all interceptors get checked for the unclearable notification
1276         verify(mInterceptor1).shouldInterceptDismissal(unclearable);
1277         verify(mInterceptor2).shouldInterceptDismissal(unclearable);
1278         verify(mInterceptor3).shouldInterceptDismissal(unclearable);
1279         assertEquals(List.of(mInterceptor1, mInterceptor2), unclearable.mDismissInterceptors);
1280 
1281         // THEN no interceptors get checked for the clearable notification
1282         verify(mInterceptor1, never()).shouldInterceptDismissal(clearable);
1283         verify(mInterceptor2, never()).shouldInterceptDismissal(clearable);
1284         verify(mInterceptor3, never()).shouldInterceptDismissal(clearable);
1285     }
1286 
1287     @Test
testClearNotificationDoesntThrowIfMissing()1288     public void testClearNotificationDoesntThrowIfMissing() {
1289         // GIVEN that enough time has passed that we're beyond the forgiveness window
1290         mClock.advanceTime(5001);
1291 
1292         // WHEN we get a remove event for a notification we don't know about
1293         final NotificationEntry container = new NotificationEntryBuilder()
1294                 .setPkg(TEST_PACKAGE)
1295                 .setId(47)
1296                 .build();
1297         mNotifHandler.onNotificationRemoved(
1298                 container.getSbn(),
1299                 new RankingMap(new Ranking[]{ container.getRanking() }));
1300 
1301         // THEN the event is ignored
1302         verify(mCollectionListener, never()).onEntryRemoved(any(NotificationEntry.class), anyInt());
1303     }
1304 
1305     @Test
testClearNotificationDoesntThrowIfInForgivenessWindow()1306     public void testClearNotificationDoesntThrowIfInForgivenessWindow() {
1307         // GIVEN that some time has passed but we're still within the initialization forgiveness
1308         // window
1309         mClock.advanceTime(4999);
1310 
1311         // WHEN we get a remove event for a notification we don't know about
1312         final NotificationEntry container = new NotificationEntryBuilder()
1313                 .setPkg(TEST_PACKAGE)
1314                 .setId(47)
1315                 .build();
1316         mNotifHandler.onNotificationRemoved(
1317                 container.getSbn(),
1318                 new RankingMap(new Ranking[]{ container.getRanking() }));
1319 
1320         // THEN no exception is thrown, but no event is fired
1321         verify(mCollectionListener, never()).onEntryRemoved(any(NotificationEntry.class), anyInt());
1322     }
1323 
buildNotif(String pkg, int id, String tag)1324     private static NotificationEntryBuilder buildNotif(String pkg, int id, String tag) {
1325         return new NotificationEntryBuilder()
1326                 .setPkg(pkg)
1327                 .setId(id)
1328                 .setTag(tag);
1329     }
1330 
buildNotif(String pkg, int id)1331     private static NotificationEntryBuilder buildNotif(String pkg, int id) {
1332         return new NotificationEntryBuilder()
1333                 .setPkg(pkg)
1334                 .setId(id);
1335     }
1336 
defaultStats(NotificationEntry entry)1337     private static DismissedByUserStats defaultStats(NotificationEntry entry) {
1338         return new DismissedByUserStats(
1339                 DISMISSAL_SHADE,
1340                 DISMISS_SENTIMENT_NEUTRAL,
1341                 NotificationVisibility.obtain(entry.getKey(), 7, 2, true));
1342     }
1343 
postNotif(NotificationEntryBuilder builder)1344     private CollectionEvent postNotif(NotificationEntryBuilder builder) {
1345         clearInvocations(mCollectionListener);
1346         NotifEvent rawEvent = mNoMan.postNotif(builder);
1347         verify(mCollectionListener).onEntryAdded(mEntryCaptor.capture());
1348         return new CollectionEvent(rawEvent, requireNonNull(mEntryCaptor.getValue()));
1349     }
1350 
verifyBuiltList(Collection<NotificationEntry> list)1351     private void verifyBuiltList(Collection<NotificationEntry> list) {
1352         verify(mBuildListener).onBuildList(mBuildListCaptor.capture());
1353         assertEquals(new ArraySet<>(list), new ArraySet<>(mBuildListCaptor.getValue()));
1354     }
1355 
1356     private static class RecordingCollectionListener implements NotifCollectionListener {
1357         private final Map<String, NotificationEntry> mLastSeenEntries = new ArrayMap<>();
1358 
1359         @Override
onEntryInit(NotificationEntry entry)1360         public void onEntryInit(NotificationEntry entry) {
1361         }
1362 
1363         @Override
onEntryAdded(NotificationEntry entry)1364         public void onEntryAdded(NotificationEntry entry) {
1365             mLastSeenEntries.put(entry.getKey(), entry);
1366         }
1367 
1368         @Override
onEntryUpdated(NotificationEntry entry)1369         public void onEntryUpdated(NotificationEntry entry) {
1370             mLastSeenEntries.put(entry.getKey(), entry);
1371         }
1372 
1373         @Override
onEntryRemoved(NotificationEntry entry, int reason)1374         public void onEntryRemoved(NotificationEntry entry, int reason) {
1375         }
1376 
1377         @Override
onEntryCleanUp(NotificationEntry entry)1378         public void onEntryCleanUp(NotificationEntry entry) {
1379         }
1380 
1381         @Override
onRankingApplied()1382         public void onRankingApplied() {
1383         }
1384 
1385         @Override
onRankingUpdate(RankingMap rankingMap)1386         public void onRankingUpdate(RankingMap rankingMap) {
1387         }
1388 
getEntry(String key)1389         public NotificationEntry getEntry(String key) {
1390             if (!mLastSeenEntries.containsKey(key)) {
1391                 throw new RuntimeException("Key not found: " + key);
1392             }
1393             return mLastSeenEntries.get(key);
1394         }
1395     }
1396 
1397     private static class RecordingLifetimeExtender implements NotifLifetimeExtender {
1398         private final String mName;
1399 
1400         public @Nullable OnEndLifetimeExtensionCallback callback;
1401         public boolean shouldExtendLifetime = false;
1402         public @Nullable Runnable onCancelLifetimeExtension;
1403 
RecordingLifetimeExtender(String name)1404         private RecordingLifetimeExtender(String name) {
1405             mName = name;
1406         }
1407 
1408         @Override
getName()1409         public String getName() {
1410             return mName;
1411         }
1412 
1413         @Override
setCallback(OnEndLifetimeExtensionCallback callback)1414         public void setCallback(OnEndLifetimeExtensionCallback callback) {
1415             this.callback = callback;
1416         }
1417 
1418         @Override
shouldExtendLifetime( NotificationEntry entry, @CancellationReason int reason)1419         public boolean shouldExtendLifetime(
1420                 NotificationEntry entry,
1421                 @CancellationReason int reason) {
1422             return shouldExtendLifetime;
1423         }
1424 
1425         @Override
cancelLifetimeExtension(NotificationEntry entry)1426         public void cancelLifetimeExtension(NotificationEntry entry) {
1427             if (onCancelLifetimeExtension != null) {
1428                 onCancelLifetimeExtension.run();
1429             }
1430         }
1431     }
1432 
1433     private static class RecordingDismissInterceptor implements NotifDismissInterceptor {
1434         private final String mName;
1435 
1436         public @Nullable OnEndDismissInterception onEndInterceptionCallback;
1437         public boolean shouldInterceptDismissal = false;
1438 
RecordingDismissInterceptor(String name)1439         private RecordingDismissInterceptor(String name) {
1440             mName = name;
1441         }
1442 
1443         @Override
getName()1444         public String getName() {
1445             return mName;
1446         }
1447 
1448         @Override
setCallback(OnEndDismissInterception callback)1449         public void setCallback(OnEndDismissInterception callback) {
1450             this.onEndInterceptionCallback = callback;
1451         }
1452 
1453         @Override
shouldInterceptDismissal(NotificationEntry entry)1454         public boolean shouldInterceptDismissal(NotificationEntry entry) {
1455             return shouldInterceptDismissal;
1456         }
1457 
1458         @Override
cancelDismissInterception(NotificationEntry entry)1459         public void cancelDismissInterception(NotificationEntry entry) {
1460         }
1461     }
1462 
1463     /**
1464      * Wrapper around {@link NotifEvent} that adds the NotificationEntry that the collection under
1465      * test creates.
1466      */
1467     private static class CollectionEvent {
1468         public final String key;
1469         public final StatusBarNotification sbn;
1470         public final Ranking ranking;
1471         public final RankingMap rankingMap;
1472         public final NotificationEntry entry;
1473 
CollectionEvent(NotifEvent rawEvent, NotificationEntry entry)1474         private CollectionEvent(NotifEvent rawEvent, NotificationEntry entry) {
1475             this.key = rawEvent.key;
1476             this.sbn = rawEvent.sbn;
1477             this.ranking = rawEvent.ranking;
1478             this.rankingMap = rawEvent.rankingMap;
1479             this.entry = entry;
1480         }
1481     }
1482 
1483     private static final String TEST_PACKAGE = "com.android.test.collection";
1484     private static final String TEST_PACKAGE2 = "com.android.test.collection2";
1485 
1486     private static final String GROUP_1 = "group_1";
1487 }
1488