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