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