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