blob: 0837a42ae700629b2e3873d53c604f2325a8612c [file] [log] [blame]
Ned Burnsf098dbf2019-09-13 19:17:53 -04001/*
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
17package com.android.systemui.statusbar.notification.collection;
18
19import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
20import static android.service.notification.NotificationListenerService.REASON_CLICK;
21
Ned Burnsf098dbf2019-09-13 19:17:53 -040022import static com.android.systemui.statusbar.notification.collection.NotifCollection.REASON_UNKNOWN;
23
24import static org.junit.Assert.assertEquals;
25import static org.junit.Assert.assertFalse;
26import static org.junit.Assert.assertNotEquals;
Ned Burnsf098dbf2019-09-13 19:17:53 -040027import static org.junit.Assert.assertTrue;
28import static org.mockito.ArgumentMatchers.anyInt;
29import static org.mockito.ArgumentMatchers.eq;
30import static org.mockito.Mockito.clearInvocations;
31import static org.mockito.Mockito.never;
Ned Burnsa944ea32019-12-19 17:04:19 -050032import static org.mockito.Mockito.times;
Ned Burnsf098dbf2019-09-13 19:17:53 -040033import static org.mockito.Mockito.verify;
34
Ned Burnse6855d62019-12-19 16:38:08 -050035import static java.util.Objects.requireNonNull;
36
Ned Burnsf098dbf2019-09-13 19:17:53 -040037import android.annotation.Nullable;
38import android.os.RemoteException;
39import android.service.notification.NotificationListenerService.Ranking;
Ned Burnsf098dbf2019-09-13 19:17:53 -040040import android.service.notification.NotificationStats;
Ned Burnsf098dbf2019-09-13 19:17:53 -040041import android.testing.AndroidTestingRunner;
42import android.testing.TestableLooper;
43import android.util.ArrayMap;
Ned Burnsa944ea32019-12-19 17:04:19 -050044import android.util.ArraySet;
Ned Burnsf098dbf2019-09-13 19:17:53 -040045
46import androidx.test.filters.SmallTest;
47
48import com.android.internal.statusbar.IStatusBarService;
49import com.android.internal.statusbar.NotificationVisibility;
50import com.android.systemui.SysuiTestCase;
Ned Burnsf098dbf2019-09-13 19:17:53 -040051import com.android.systemui.statusbar.RankingBuilder;
Ned Burnsd7bf7922019-12-19 16:13:01 -050052import com.android.systemui.statusbar.notification.collection.NoManSimulator.NotifEvent;
Ned Burnsf098dbf2019-09-13 19:17:53 -040053import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason;
Ned Burnsa944ea32019-12-19 17:04:19 -050054import com.android.systemui.statusbar.notification.collection.notifcollection.CoalescedEvent;
55import com.android.systemui.statusbar.notification.collection.notifcollection.GroupCoalescer;
56import com.android.systemui.statusbar.notification.collection.notifcollection.GroupCoalescer.BatchableNotificationHandler;
Ned Burnsf098dbf2019-09-13 19:17:53 -040057import com.android.systemui.util.Assert;
58
59import org.junit.Before;
60import org.junit.Test;
61import org.junit.runner.RunWith;
62import org.mockito.ArgumentCaptor;
63import org.mockito.Captor;
64import org.mockito.Mock;
65import org.mockito.MockitoAnnotations;
66import org.mockito.Spy;
67
68import java.util.Arrays;
Ned Burnsa944ea32019-12-19 17:04:19 -050069import java.util.Collection;
70import java.util.List;
Ned Burnsf098dbf2019-09-13 19:17:53 -040071import java.util.Map;
72
73@SmallTest
74@RunWith(AndroidTestingRunner.class)
75@TestableLooper.RunWithLooper
76public class NotifCollectionTest extends SysuiTestCase {
77
78 @Mock private IStatusBarService mStatusBarService;
Ned Burnsa944ea32019-12-19 17:04:19 -050079 @Mock private GroupCoalescer mGroupCoalescer;
Ned Burnsf098dbf2019-09-13 19:17:53 -040080 @Spy private RecordingCollectionListener mCollectionListener;
Ned Burnsa944ea32019-12-19 17:04:19 -050081 @Mock private CollectionReadyForBuildListener mBuildListener;
Ned Burnsf098dbf2019-09-13 19:17:53 -040082
83 @Spy private RecordingLifetimeExtender mExtender1 = new RecordingLifetimeExtender("Extender1");
84 @Spy private RecordingLifetimeExtender mExtender2 = new RecordingLifetimeExtender("Extender2");
85 @Spy private RecordingLifetimeExtender mExtender3 = new RecordingLifetimeExtender("Extender3");
86
Ned Burnsa944ea32019-12-19 17:04:19 -050087 @Captor private ArgumentCaptor<BatchableNotificationHandler> mListenerCaptor;
Ned Burnsf098dbf2019-09-13 19:17:53 -040088 @Captor private ArgumentCaptor<NotificationEntry> mEntryCaptor;
Ned Burnsa944ea32019-12-19 17:04:19 -050089 @Captor private ArgumentCaptor<Collection<NotificationEntry>> mBuildListCaptor;
Ned Burnsf098dbf2019-09-13 19:17:53 -040090
91 private NotifCollection mCollection;
Ned Burnsa944ea32019-12-19 17:04:19 -050092 private BatchableNotificationHandler mNotifHandler;
Ned Burnsf098dbf2019-09-13 19:17:53 -040093
94 private NoManSimulator mNoMan;
95
96 @Before
97 public void setUp() {
98 MockitoAnnotations.initMocks(this);
99 Assert.sMainLooper = TestableLooper.get(this).getLooper();
100
101 mCollection = new NotifCollection(mStatusBarService);
Ned Burnsa944ea32019-12-19 17:04:19 -0500102 mCollection.attach(mGroupCoalescer);
Ned Burnsf098dbf2019-09-13 19:17:53 -0400103 mCollection.addCollectionListener(mCollectionListener);
Ned Burnsa944ea32019-12-19 17:04:19 -0500104 mCollection.setBuildListener(mBuildListener);
Ned Burnsf098dbf2019-09-13 19:17:53 -0400105
106 // Capture the listener object that the collection registers with the listener service so
107 // we can simulate listener service events in tests below
Ned Burnsa944ea32019-12-19 17:04:19 -0500108 verify(mGroupCoalescer).setNotificationHandler(mListenerCaptor.capture());
109 mNotifHandler = requireNonNull(mListenerCaptor.getValue());
Ned Burnsf098dbf2019-09-13 19:17:53 -0400110
Ned Burnsd7bf7922019-12-19 16:13:01 -0500111 mNoMan = new NoManSimulator();
Ned Burnsa944ea32019-12-19 17:04:19 -0500112 mNoMan.addListener(mNotifHandler);
Ned Burnsf098dbf2019-09-13 19:17:53 -0400113 }
114
115 @Test
116 public void testEventDispatchedWhenNotifPosted() {
117 // WHEN a notification is posted
Ned Burnsd7bf7922019-12-19 16:13:01 -0500118 NotifEvent notif1 = mNoMan.postNotif(
Ned Burnsf098dbf2019-09-13 19:17:53 -0400119 buildNotif(TEST_PACKAGE, 3)
120 .setRank(4747));
121
122 // THEN the listener is notified
123 verify(mCollectionListener).onEntryAdded(mEntryCaptor.capture());
124
125 NotificationEntry entry = mEntryCaptor.getValue();
Evan Laird9afe7662019-10-16 17:16:39 -0400126 assertEquals(notif1.key, entry.getKey());
127 assertEquals(notif1.sbn, entry.getSbn());
128 assertEquals(notif1.ranking, entry.getRanking());
Ned Burnsf098dbf2019-09-13 19:17:53 -0400129 }
130
131 @Test
Ned Burnsa944ea32019-12-19 17:04:19 -0500132 public void testEventDispatchedWhenNotifBatchPosted() {
133 // GIVEN a NotifCollection with one notif already posted
134 mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2)
135 .setGroup(mContext, "group_1")
136 .setContentTitle(mContext, "Old version"));
137
138 clearInvocations(mCollectionListener);
139 clearInvocations(mBuildListener);
140
141 // WHEN three notifications from the same group are posted (one of them an update, two of
142 // them new)
143 NotificationEntry entry1 = buildNotif(TEST_PACKAGE, 1)
144 .setGroup(mContext, "group_1")
145 .build();
146 NotificationEntry entry2 = buildNotif(TEST_PACKAGE, 2)
147 .setGroup(mContext, "group_1")
148 .setContentTitle(mContext, "New version")
149 .build();
150 NotificationEntry entry3 = buildNotif(TEST_PACKAGE, 3)
151 .setGroup(mContext, "group_1")
152 .build();
153
154 mNotifHandler.onNotificationBatchPosted(Arrays.asList(
155 new CoalescedEvent(entry1.getKey(), 0, entry1.getSbn(), entry1.getRanking(), null),
156 new CoalescedEvent(entry2.getKey(), 1, entry2.getSbn(), entry2.getRanking(), null),
157 new CoalescedEvent(entry3.getKey(), 2, entry3.getSbn(), entry3.getRanking(), null)
158 ));
159
160 // THEN onEntryAdded is called on the new ones
161 verify(mCollectionListener, times(2)).onEntryAdded(mEntryCaptor.capture());
162
163 List<NotificationEntry> capturedAdds = mEntryCaptor.getAllValues();
164
165 assertEquals(entry1.getSbn(), capturedAdds.get(0).getSbn());
166 assertEquals(entry1.getRanking(), capturedAdds.get(0).getRanking());
167
168 assertEquals(entry3.getSbn(), capturedAdds.get(1).getSbn());
169 assertEquals(entry3.getRanking(), capturedAdds.get(1).getRanking());
170
171 // THEN onEntryUpdated is called on the middle one
172 verify(mCollectionListener).onEntryUpdated(mEntryCaptor.capture());
173 NotificationEntry capturedUpdate = mEntryCaptor.getValue();
174 assertEquals(entry2.getSbn(), capturedUpdate.getSbn());
175 assertEquals(entry2.getRanking(), capturedUpdate.getRanking());
176
177 // THEN onBuildList is called only once
178 verify(mBuildListener).onBuildList(mBuildListCaptor.capture());
179 assertEquals(new ArraySet<>(Arrays.asList(
180 capturedAdds.get(0),
181 capturedAdds.get(1),
182 capturedUpdate
183 )), new ArraySet<>(mBuildListCaptor.getValue()));
184 }
185
186 @Test
Ned Burnsf098dbf2019-09-13 19:17:53 -0400187 public void testEventDispatchedWhenNotifUpdated() {
188 // GIVEN a collection with one notif
189 mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3)
190 .setRank(4747));
191
192 // WHEN the notif is reposted
Ned Burnsd7bf7922019-12-19 16:13:01 -0500193 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3)
Ned Burnsf098dbf2019-09-13 19:17:53 -0400194 .setRank(89));
195
196 // THEN the listener is notified
197 verify(mCollectionListener).onEntryUpdated(mEntryCaptor.capture());
198
199 NotificationEntry entry = mEntryCaptor.getValue();
Evan Laird9afe7662019-10-16 17:16:39 -0400200 assertEquals(notif2.key, entry.getKey());
201 assertEquals(notif2.sbn, entry.getSbn());
202 assertEquals(notif2.ranking, entry.getRanking());
Ned Burnsf098dbf2019-09-13 19:17:53 -0400203 }
204
205 @Test
206 public void testEventDispatchedWhenNotifRemoved() {
207 // GIVEN a collection with one notif
208 mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3));
209 clearInvocations(mCollectionListener);
210
Ned Burnsd7bf7922019-12-19 16:13:01 -0500211 NotifEvent notif = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400212 NotificationEntry entry = mCollectionListener.getEntry(notif.key);
213 clearInvocations(mCollectionListener);
214
215 // WHEN a notif is retracted
216 mNoMan.retractNotif(notif.sbn, REASON_APP_CANCEL);
217
218 // THEN the listener is notified
219 verify(mCollectionListener).onEntryRemoved(entry, REASON_APP_CANCEL, false);
Evan Laird9afe7662019-10-16 17:16:39 -0400220 assertEquals(notif.sbn, entry.getSbn());
221 assertEquals(notif.ranking, entry.getRanking());
Ned Burnsf098dbf2019-09-13 19:17:53 -0400222 }
223
224 @Test
225 public void testRankingsAreUpdatedForOtherNotifs() {
226 // GIVEN a collection with one notif
Ned Burnsd7bf7922019-12-19 16:13:01 -0500227 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3)
Ned Burnsf098dbf2019-09-13 19:17:53 -0400228 .setRank(47));
229 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
230
231 // WHEN a new notif is posted, triggering a rerank
232 mNoMan.setRanking(notif1.sbn.getKey(), new RankingBuilder(notif1.ranking)
233 .setRank(56)
234 .build());
235 mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 77));
236
237 // THEN the ranking is updated on the first entry
Evan Laird9afe7662019-10-16 17:16:39 -0400238 assertEquals(56, entry1.getRanking().getRank());
Ned Burnsf098dbf2019-09-13 19:17:53 -0400239 }
240
241 @Test
242 public void testRankingUpdateIsProperlyIssuedToEveryone() {
243 // GIVEN a collection with a couple notifs
Ned Burnsd7bf7922019-12-19 16:13:01 -0500244 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3)
Ned Burnsf098dbf2019-09-13 19:17:53 -0400245 .setRank(3));
Ned Burnsd7bf7922019-12-19 16:13:01 -0500246 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 8)
Ned Burnsf098dbf2019-09-13 19:17:53 -0400247 .setRank(2));
Ned Burnsd7bf7922019-12-19 16:13:01 -0500248 NotifEvent notif3 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 77)
Ned Burnsf098dbf2019-09-13 19:17:53 -0400249 .setRank(1));
250
251 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
252 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
253 NotificationEntry entry3 = mCollectionListener.getEntry(notif3.key);
254
255 // WHEN a ranking update is delivered
256 Ranking newRanking1 = new RankingBuilder(notif1.ranking)
257 .setRank(4)
258 .setExplanation("Foo bar")
259 .build();
260 Ranking newRanking2 = new RankingBuilder(notif2.ranking)
261 .setRank(5)
262 .setExplanation("baz buzz")
263 .build();
264 Ranking newRanking3 = new RankingBuilder(notif3.ranking)
265 .setRank(6)
266 .setExplanation("Penguin pizza")
267 .build();
268
269 mNoMan.setRanking(notif1.sbn.getKey(), newRanking1);
270 mNoMan.setRanking(notif2.sbn.getKey(), newRanking2);
271 mNoMan.setRanking(notif3.sbn.getKey(), newRanking3);
272 mNoMan.issueRankingUpdate();
273
274 // THEN all of the NotifEntries have their rankings properly updated
Evan Laird9afe7662019-10-16 17:16:39 -0400275 assertEquals(newRanking1, entry1.getRanking());
276 assertEquals(newRanking2, entry2.getRanking());
277 assertEquals(newRanking3, entry3.getRanking());
Ned Burnsf098dbf2019-09-13 19:17:53 -0400278 }
279
280 @Test
281 public void testNotifEntriesAreNotPersistedAcrossRemovalAndReposting() {
282 // GIVEN a notification that has been posted
Ned Burnsd7bf7922019-12-19 16:13:01 -0500283 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400284 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
285
286 // WHEN the notification is retracted and then reposted
287 mNoMan.retractNotif(notif1.sbn, REASON_APP_CANCEL);
288 mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3));
289
290 // THEN the new NotificationEntry is a new object
291 NotificationEntry entry2 = mCollectionListener.getEntry(notif1.key);
292 assertNotEquals(entry2, entry1);
293 }
294
295 @Test
296 public void testDismissNotification() throws RemoteException {
297 // GIVEN a collection with a couple notifications and a lifetime extender
298 mCollection.addNotificationLifetimeExtender(mExtender1);
299
Ned Burnsd7bf7922019-12-19 16:13:01 -0500300 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
301 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag"));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400302 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
303
304 // WHEN a notification is manually dismissed
305 DismissedByUserStats stats = new DismissedByUserStats(
306 NotificationStats.DISMISSAL_SHADE,
307 NotificationStats.DISMISS_SENTIMENT_NEUTRAL,
Evan Laird9afe7662019-10-16 17:16:39 -0400308 NotificationVisibility.obtain(entry2.getKey(), 7, 2, true));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400309
310 mCollection.dismissNotification(entry2, REASON_CLICK, stats);
311
312 // THEN we check for lifetime extension
313 verify(mExtender1).shouldExtendLifetime(entry2, REASON_CLICK);
314
315 // THEN we send the dismissal to system server
316 verify(mStatusBarService).onNotificationClear(
317 notif2.sbn.getPackageName(),
318 notif2.sbn.getTag(),
319 88,
320 notif2.sbn.getUser().getIdentifier(),
321 notif2.sbn.getKey(),
322 stats.dismissalSurface,
323 stats.dismissalSentiment,
324 stats.notificationVisibility);
325
326 // THEN we fire a remove event
327 verify(mCollectionListener).onEntryRemoved(entry2, REASON_CLICK, true);
328 }
329
330 @Test(expected = IllegalStateException.class)
331 public void testDismissingNonExistentNotificationThrows() {
332 // GIVEN a collection that originally had three notifs, but where one was dismissed
Ned Burnsd7bf7922019-12-19 16:13:01 -0500333 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
334 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
335 NotifEvent notif3 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 99));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400336 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
337 mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
338
339 // WHEN we try to dismiss a notification that isn't present
340 mCollection.dismissNotification(
341 entry2,
342 REASON_CLICK,
343 new DismissedByUserStats(0, 0, NotificationVisibility.obtain("foo", 47, 3, true)));
344
345 // THEN an exception is thrown
346 }
347
348 @Test
349 public void testLifetimeExtendersAreQueriedWhenNotifRemoved() {
350 // GIVEN a couple notifications and a few lifetime extenders
351 mExtender1.shouldExtendLifetime = true;
352 mExtender2.shouldExtendLifetime = true;
353
354 mCollection.addNotificationLifetimeExtender(mExtender1);
355 mCollection.addNotificationLifetimeExtender(mExtender2);
356 mCollection.addNotificationLifetimeExtender(mExtender3);
357
Ned Burnsd7bf7922019-12-19 16:13:01 -0500358 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
359 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400360 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
361
362 // WHEN a notification is removed
363 mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
364
365 // THEN each extender is asked whether to extend, even if earlier ones return true
366 verify(mExtender1).shouldExtendLifetime(entry2, REASON_UNKNOWN);
367 verify(mExtender2).shouldExtendLifetime(entry2, REASON_UNKNOWN);
368 verify(mExtender3).shouldExtendLifetime(entry2, REASON_UNKNOWN);
369
370 // THEN the entry is not removed
371 assertTrue(mCollection.getNotifs().contains(entry2));
372
373 // THEN the entry properly records all extenders that returned true
374 assertEquals(Arrays.asList(mExtender1, mExtender2), entry2.mLifetimeExtenders);
375 }
376
377 @Test
378 public void testWhenLastLifetimeExtenderExpiresAllAreReQueried() {
379 // GIVEN a couple notifications and a few lifetime extenders
380 mExtender2.shouldExtendLifetime = true;
381
382 mCollection.addNotificationLifetimeExtender(mExtender1);
383 mCollection.addNotificationLifetimeExtender(mExtender2);
384 mCollection.addNotificationLifetimeExtender(mExtender3);
385
Ned Burnsd7bf7922019-12-19 16:13:01 -0500386 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
387 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400388 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
389
390 // GIVEN a notification gets lifetime-extended by one of them
391 mNoMan.retractNotif(notif2.sbn, REASON_APP_CANCEL);
392 assertTrue(mCollection.getNotifs().contains(entry2));
393 clearInvocations(mExtender1, mExtender2, mExtender3);
394
395 // WHEN the last active extender expires (but new ones become active)
396 mExtender1.shouldExtendLifetime = true;
397 mExtender2.shouldExtendLifetime = false;
398 mExtender3.shouldExtendLifetime = true;
399 mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2);
400
401 // THEN each extender is re-queried
402 verify(mExtender1).shouldExtendLifetime(entry2, REASON_UNKNOWN);
403 verify(mExtender2).shouldExtendLifetime(entry2, REASON_UNKNOWN);
404 verify(mExtender3).shouldExtendLifetime(entry2, REASON_UNKNOWN);
405
406 // THEN the entry is not removed
407 assertTrue(mCollection.getNotifs().contains(entry2));
408
409 // THEN the entry properly records all extenders that returned true
410 assertEquals(Arrays.asList(mExtender1, mExtender3), entry2.mLifetimeExtenders);
411 }
412
413 @Test
414 public void testExtendersAreNotReQueriedUntilFinalActiveExtenderExpires() {
415 // GIVEN a couple notifications and a few lifetime extenders
416 mExtender1.shouldExtendLifetime = true;
417 mExtender2.shouldExtendLifetime = true;
418
419 mCollection.addNotificationLifetimeExtender(mExtender1);
420 mCollection.addNotificationLifetimeExtender(mExtender2);
421 mCollection.addNotificationLifetimeExtender(mExtender3);
422
Ned Burnsd7bf7922019-12-19 16:13:01 -0500423 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
424 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400425 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
426
427 // GIVEN a notification gets lifetime-extended by a couple of them
428 mNoMan.retractNotif(notif2.sbn, REASON_APP_CANCEL);
429 assertTrue(mCollection.getNotifs().contains(entry2));
430 clearInvocations(mExtender1, mExtender2, mExtender3);
431
432 // WHEN one (but not all) of the extenders expires
433 mExtender2.shouldExtendLifetime = false;
434 mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2);
435
436 // THEN the entry is not removed
437 assertTrue(mCollection.getNotifs().contains(entry2));
438
439 // THEN we don't re-query the extenders
440 verify(mExtender1, never()).shouldExtendLifetime(eq(entry2), anyInt());
441 verify(mExtender2, never()).shouldExtendLifetime(eq(entry2), anyInt());
442 verify(mExtender3, never()).shouldExtendLifetime(eq(entry2), anyInt());
443
444 // THEN the entry properly records all extenders that returned true
445 assertEquals(Arrays.asList(mExtender1), entry2.mLifetimeExtenders);
446 }
447
448 @Test
449 public void testNotificationIsRemovedWhenAllLifetimeExtendersExpire() {
450 // GIVEN a couple notifications and a few lifetime extenders
451 mExtender1.shouldExtendLifetime = true;
452 mExtender2.shouldExtendLifetime = true;
453
454 mCollection.addNotificationLifetimeExtender(mExtender1);
455 mCollection.addNotificationLifetimeExtender(mExtender2);
456 mCollection.addNotificationLifetimeExtender(mExtender3);
457
Ned Burnsd7bf7922019-12-19 16:13:01 -0500458 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
459 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400460 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
461
462 // GIVEN a notification gets lifetime-extended by a couple of them
463 mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
464 assertTrue(mCollection.getNotifs().contains(entry2));
465 clearInvocations(mExtender1, mExtender2, mExtender3);
466
467 // WHEN all of the active extenders expire
468 mExtender2.shouldExtendLifetime = false;
469 mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2);
470 mExtender1.shouldExtendLifetime = false;
471 mExtender1.callback.onEndLifetimeExtension(mExtender1, entry2);
472
473 // THEN the entry removed
474 assertFalse(mCollection.getNotifs().contains(entry2));
475 verify(mCollectionListener).onEntryRemoved(entry2, REASON_UNKNOWN, false);
476 }
477
478 @Test
479 public void testLifetimeExtensionIsCanceledWhenNotifIsUpdated() {
480 // GIVEN a few lifetime extenders and a couple notifications
481 mCollection.addNotificationLifetimeExtender(mExtender1);
482 mCollection.addNotificationLifetimeExtender(mExtender2);
483 mCollection.addNotificationLifetimeExtender(mExtender3);
484
485 mExtender1.shouldExtendLifetime = true;
486 mExtender2.shouldExtendLifetime = true;
487
Ned Burnsd7bf7922019-12-19 16:13:01 -0500488 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
489 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400490 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
491
492 // GIVEN a notification gets lifetime-extended by a couple of them
493 mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
494 assertTrue(mCollection.getNotifs().contains(entry2));
495 clearInvocations(mExtender1, mExtender2, mExtender3);
496
497 // WHEN the notification is reposted
498 mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
499
500 // THEN all of the active lifetime extenders are canceled
501 verify(mExtender1).cancelLifetimeExtension(entry2);
502 verify(mExtender2).cancelLifetimeExtension(entry2);
503
504 // THEN the notification is still present
505 assertTrue(mCollection.getNotifs().contains(entry2));
506 }
507
508 @Test(expected = IllegalStateException.class)
509 public void testReentrantCallsToLifetimeExtendersThrow() {
510 // GIVEN a few lifetime extenders and a couple notifications
511 mCollection.addNotificationLifetimeExtender(mExtender1);
512 mCollection.addNotificationLifetimeExtender(mExtender2);
513 mCollection.addNotificationLifetimeExtender(mExtender3);
514
515 mExtender1.shouldExtendLifetime = true;
516 mExtender2.shouldExtendLifetime = true;
517
Ned Burnsd7bf7922019-12-19 16:13:01 -0500518 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
519 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400520 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
521
522 // GIVEN a notification gets lifetime-extended by a couple of them
523 mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
524 assertTrue(mCollection.getNotifs().contains(entry2));
525 clearInvocations(mExtender1, mExtender2, mExtender3);
526
527 // WHEN a lifetime extender makes a reentrant call during cancelLifetimeExtension()
528 mExtender2.onCancelLifetimeExtension = () -> {
529 mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2);
530 };
531 // This triggers the call to cancelLifetimeExtension()
532 mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
533
534 // THEN an exception is thrown
535 }
536
537 @Test
538 public void testRankingIsUpdatedWhenALifetimeExtendedNotifIsReposted() {
539 // GIVEN a few lifetime extenders and a couple notifications
540 mCollection.addNotificationLifetimeExtender(mExtender1);
541 mCollection.addNotificationLifetimeExtender(mExtender2);
542 mCollection.addNotificationLifetimeExtender(mExtender3);
543
544 mExtender1.shouldExtendLifetime = true;
545 mExtender2.shouldExtendLifetime = true;
546
Ned Burnsd7bf7922019-12-19 16:13:01 -0500547 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
548 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400549 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
550
551 // GIVEN a notification gets lifetime-extended by a couple of them
552 mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
553 assertTrue(mCollection.getNotifs().contains(entry2));
554 clearInvocations(mExtender1, mExtender2, mExtender3);
555
556 // WHEN the notification is reposted
Ned Burnsd7bf7922019-12-19 16:13:01 -0500557 NotifEvent notif2a = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88)
Ned Burnsf098dbf2019-09-13 19:17:53 -0400558 .setRank(4747)
559 .setExplanation("Some new explanation"));
560
561 // THEN the notification's ranking is properly updated
Evan Laird9afe7662019-10-16 17:16:39 -0400562 assertEquals(notif2a.ranking, entry2.getRanking());
Ned Burnsf098dbf2019-09-13 19:17:53 -0400563 }
564
565 private static NotificationEntryBuilder buildNotif(String pkg, int id, String tag) {
566 return new NotificationEntryBuilder()
567 .setPkg(pkg)
568 .setId(id)
569 .setTag(tag);
570 }
571
572 private static NotificationEntryBuilder buildNotif(String pkg, int id) {
573 return new NotificationEntryBuilder()
574 .setPkg(pkg)
575 .setId(id);
576 }
577
Ned Burnsf098dbf2019-09-13 19:17:53 -0400578 private static class RecordingCollectionListener implements NotifCollectionListener {
579 private final Map<String, NotificationEntry> mLastSeenEntries = new ArrayMap<>();
580
581 @Override
582 public void onEntryAdded(NotificationEntry entry) {
Evan Laird9afe7662019-10-16 17:16:39 -0400583 mLastSeenEntries.put(entry.getKey(), entry);
Ned Burnsf098dbf2019-09-13 19:17:53 -0400584 }
585
586 @Override
587 public void onEntryUpdated(NotificationEntry entry) {
588 }
589
590 @Override
591 public void onEntryRemoved(NotificationEntry entry, int reason, boolean removedByUser) {
592 }
593
594 public NotificationEntry getEntry(String key) {
595 if (!mLastSeenEntries.containsKey(key)) {
596 throw new RuntimeException("Key not found: " + key);
597 }
598 return mLastSeenEntries.get(key);
599 }
600 }
601
602 private static class RecordingLifetimeExtender implements NotifLifetimeExtender {
603 private final String mName;
604
605 public @Nullable OnEndLifetimeExtensionCallback callback;
606 public boolean shouldExtendLifetime = false;
607 public @Nullable Runnable onCancelLifetimeExtension;
608
609 private RecordingLifetimeExtender(String name) {
610 mName = name;
611 }
612
613 @Override
614 public String getName() {
615 return mName;
616 }
617
618 @Override
619 public void setCallback(OnEndLifetimeExtensionCallback callback) {
620 this.callback = callback;
621 }
622
623 @Override
624 public boolean shouldExtendLifetime(
625 NotificationEntry entry,
626 @CancellationReason int reason) {
627 return shouldExtendLifetime;
628 }
629
630 @Override
631 public void cancelLifetimeExtension(NotificationEntry entry) {
632 if (onCancelLifetimeExtension != null) {
633 onCancelLifetimeExtension.run();
634 }
635 }
636 }
637
638 private static final String TEST_PACKAGE = "com.android.test.collection";
639 private static final String TEST_PACKAGE2 = "com.android.test.collection2";
640}