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