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