blob: f6a7abdbe1f9669db594d4631878eabc538b38ac [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;
32import static org.mockito.Mockito.verify;
33
34import android.annotation.Nullable;
35import android.os.RemoteException;
36import android.service.notification.NotificationListenerService.Ranking;
Ned Burnsf098dbf2019-09-13 19:17:53 -040037import android.service.notification.NotificationStats;
Ned Burnsf098dbf2019-09-13 19:17:53 -040038import android.testing.AndroidTestingRunner;
39import android.testing.TestableLooper;
40import android.util.ArrayMap;
41
42import androidx.test.filters.SmallTest;
43
44import com.android.internal.statusbar.IStatusBarService;
45import com.android.internal.statusbar.NotificationVisibility;
46import com.android.systemui.SysuiTestCase;
Ned Burnsf098dbf2019-09-13 19:17:53 -040047import com.android.systemui.statusbar.NotificationListener;
48import com.android.systemui.statusbar.NotificationListener.NotifServiceListener;
49import com.android.systemui.statusbar.RankingBuilder;
Ned Burnsd7bf7922019-12-19 16:13:01 -050050import com.android.systemui.statusbar.notification.collection.NoManSimulator.NotifEvent;
Ned Burnsf098dbf2019-09-13 19:17:53 -040051import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason;
52import com.android.systemui.util.Assert;
53
54import org.junit.Before;
55import org.junit.Test;
56import org.junit.runner.RunWith;
57import org.mockito.ArgumentCaptor;
58import org.mockito.Captor;
59import org.mockito.Mock;
60import org.mockito.MockitoAnnotations;
61import org.mockito.Spy;
62
63import java.util.Arrays;
64import java.util.Map;
Daulet Zhanguzind0549ae2020-01-03 11:08:54 +000065import java.util.Objects;
Ned Burnsf098dbf2019-09-13 19:17:53 -040066
67@SmallTest
68@RunWith(AndroidTestingRunner.class)
69@TestableLooper.RunWithLooper
70public class NotifCollectionTest extends SysuiTestCase {
71
72 @Mock private IStatusBarService mStatusBarService;
73 @Mock private NotificationListener mListenerService;
74 @Spy private RecordingCollectionListener mCollectionListener;
75
76 @Spy private RecordingLifetimeExtender mExtender1 = new RecordingLifetimeExtender("Extender1");
77 @Spy private RecordingLifetimeExtender mExtender2 = new RecordingLifetimeExtender("Extender2");
78 @Spy private RecordingLifetimeExtender mExtender3 = new RecordingLifetimeExtender("Extender3");
79
80 @Captor private ArgumentCaptor<NotifServiceListener> mListenerCaptor;
81 @Captor private ArgumentCaptor<NotificationEntry> mEntryCaptor;
82
83 private NotifCollection mCollection;
84 private NotifServiceListener mServiceListener;
85
86 private NoManSimulator mNoMan;
87
88 @Before
89 public void setUp() {
90 MockitoAnnotations.initMocks(this);
91 Assert.sMainLooper = TestableLooper.get(this).getLooper();
92
93 mCollection = new NotifCollection(mStatusBarService);
94 mCollection.attach(mListenerService);
95 mCollection.addCollectionListener(mCollectionListener);
96
97 // Capture the listener object that the collection registers with the listener service so
98 // we can simulate listener service events in tests below
Ned Burns2246b332019-12-12 14:39:13 -050099 verify(mListenerService).addNotificationListener(mListenerCaptor.capture());
Daulet Zhanguzind0549ae2020-01-03 11:08:54 +0000100 mServiceListener = Objects.requireNonNull(mListenerCaptor.getValue());
Ned Burnsf098dbf2019-09-13 19:17:53 -0400101
Ned Burnsd7bf7922019-12-19 16:13:01 -0500102 mNoMan = new NoManSimulator();
103 mNoMan.addListener(mServiceListener);
Ned Burnsf098dbf2019-09-13 19:17:53 -0400104 }
105
106 @Test
107 public void testEventDispatchedWhenNotifPosted() {
108 // WHEN a notification is posted
Ned Burnsd7bf7922019-12-19 16:13:01 -0500109 NotifEvent notif1 = mNoMan.postNotif(
Ned Burnsf098dbf2019-09-13 19:17:53 -0400110 buildNotif(TEST_PACKAGE, 3)
111 .setRank(4747));
112
113 // THEN the listener is notified
114 verify(mCollectionListener).onEntryAdded(mEntryCaptor.capture());
115
116 NotificationEntry entry = mEntryCaptor.getValue();
Evan Laird9afe7662019-10-16 17:16:39 -0400117 assertEquals(notif1.key, entry.getKey());
118 assertEquals(notif1.sbn, entry.getSbn());
119 assertEquals(notif1.ranking, entry.getRanking());
Ned Burnsf098dbf2019-09-13 19:17:53 -0400120 }
121
122 @Test
123 public void testEventDispatchedWhenNotifUpdated() {
124 // GIVEN a collection with one notif
125 mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3)
126 .setRank(4747));
127
128 // WHEN the notif is reposted
Ned Burnsd7bf7922019-12-19 16:13:01 -0500129 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3)
Ned Burnsf098dbf2019-09-13 19:17:53 -0400130 .setRank(89));
131
132 // THEN the listener is notified
133 verify(mCollectionListener).onEntryUpdated(mEntryCaptor.capture());
134
135 NotificationEntry entry = mEntryCaptor.getValue();
Evan Laird9afe7662019-10-16 17:16:39 -0400136 assertEquals(notif2.key, entry.getKey());
137 assertEquals(notif2.sbn, entry.getSbn());
138 assertEquals(notif2.ranking, entry.getRanking());
Ned Burnsf098dbf2019-09-13 19:17:53 -0400139 }
140
141 @Test
142 public void testEventDispatchedWhenNotifRemoved() {
143 // GIVEN a collection with one notif
144 mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3));
145 clearInvocations(mCollectionListener);
146
Ned Burnsd7bf7922019-12-19 16:13:01 -0500147 NotifEvent notif = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400148 NotificationEntry entry = mCollectionListener.getEntry(notif.key);
149 clearInvocations(mCollectionListener);
150
151 // WHEN a notif is retracted
152 mNoMan.retractNotif(notif.sbn, REASON_APP_CANCEL);
153
154 // THEN the listener is notified
155 verify(mCollectionListener).onEntryRemoved(entry, REASON_APP_CANCEL, false);
Evan Laird9afe7662019-10-16 17:16:39 -0400156 assertEquals(notif.sbn, entry.getSbn());
157 assertEquals(notif.ranking, entry.getRanking());
Ned Burnsf098dbf2019-09-13 19:17:53 -0400158 }
159
160 @Test
161 public void testRankingsAreUpdatedForOtherNotifs() {
162 // GIVEN a collection with one notif
Ned Burnsd7bf7922019-12-19 16:13:01 -0500163 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3)
Ned Burnsf098dbf2019-09-13 19:17:53 -0400164 .setRank(47));
165 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
166
167 // WHEN a new notif is posted, triggering a rerank
168 mNoMan.setRanking(notif1.sbn.getKey(), new RankingBuilder(notif1.ranking)
169 .setRank(56)
170 .build());
171 mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 77));
172
173 // THEN the ranking is updated on the first entry
Evan Laird9afe7662019-10-16 17:16:39 -0400174 assertEquals(56, entry1.getRanking().getRank());
Ned Burnsf098dbf2019-09-13 19:17:53 -0400175 }
176
177 @Test
178 public void testRankingUpdateIsProperlyIssuedToEveryone() {
179 // GIVEN a collection with a couple notifs
Ned Burnsd7bf7922019-12-19 16:13:01 -0500180 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3)
Ned Burnsf098dbf2019-09-13 19:17:53 -0400181 .setRank(3));
Ned Burnsd7bf7922019-12-19 16:13:01 -0500182 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 8)
Ned Burnsf098dbf2019-09-13 19:17:53 -0400183 .setRank(2));
Ned Burnsd7bf7922019-12-19 16:13:01 -0500184 NotifEvent notif3 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 77)
Ned Burnsf098dbf2019-09-13 19:17:53 -0400185 .setRank(1));
186
187 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
188 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
189 NotificationEntry entry3 = mCollectionListener.getEntry(notif3.key);
190
191 // WHEN a ranking update is delivered
192 Ranking newRanking1 = new RankingBuilder(notif1.ranking)
193 .setRank(4)
194 .setExplanation("Foo bar")
195 .build();
196 Ranking newRanking2 = new RankingBuilder(notif2.ranking)
197 .setRank(5)
198 .setExplanation("baz buzz")
199 .build();
200 Ranking newRanking3 = new RankingBuilder(notif3.ranking)
201 .setRank(6)
202 .setExplanation("Penguin pizza")
203 .build();
204
205 mNoMan.setRanking(notif1.sbn.getKey(), newRanking1);
206 mNoMan.setRanking(notif2.sbn.getKey(), newRanking2);
207 mNoMan.setRanking(notif3.sbn.getKey(), newRanking3);
208 mNoMan.issueRankingUpdate();
209
210 // THEN all of the NotifEntries have their rankings properly updated
Evan Laird9afe7662019-10-16 17:16:39 -0400211 assertEquals(newRanking1, entry1.getRanking());
212 assertEquals(newRanking2, entry2.getRanking());
213 assertEquals(newRanking3, entry3.getRanking());
Ned Burnsf098dbf2019-09-13 19:17:53 -0400214 }
215
216 @Test
217 public void testNotifEntriesAreNotPersistedAcrossRemovalAndReposting() {
218 // GIVEN a notification that has been posted
Ned Burnsd7bf7922019-12-19 16:13:01 -0500219 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400220 NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
221
222 // WHEN the notification is retracted and then reposted
223 mNoMan.retractNotif(notif1.sbn, REASON_APP_CANCEL);
224 mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3));
225
226 // THEN the new NotificationEntry is a new object
227 NotificationEntry entry2 = mCollectionListener.getEntry(notif1.key);
228 assertNotEquals(entry2, entry1);
229 }
230
231 @Test
232 public void testDismissNotification() throws RemoteException {
233 // GIVEN a collection with a couple notifications and a lifetime extender
234 mCollection.addNotificationLifetimeExtender(mExtender1);
235
Ned Burnsd7bf7922019-12-19 16:13:01 -0500236 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
237 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag"));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400238 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
239
240 // WHEN a notification is manually dismissed
241 DismissedByUserStats stats = new DismissedByUserStats(
242 NotificationStats.DISMISSAL_SHADE,
243 NotificationStats.DISMISS_SENTIMENT_NEUTRAL,
Evan Laird9afe7662019-10-16 17:16:39 -0400244 NotificationVisibility.obtain(entry2.getKey(), 7, 2, true));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400245
246 mCollection.dismissNotification(entry2, REASON_CLICK, stats);
247
248 // THEN we check for lifetime extension
249 verify(mExtender1).shouldExtendLifetime(entry2, REASON_CLICK);
250
251 // THEN we send the dismissal to system server
252 verify(mStatusBarService).onNotificationClear(
253 notif2.sbn.getPackageName(),
254 notif2.sbn.getTag(),
255 88,
256 notif2.sbn.getUser().getIdentifier(),
257 notif2.sbn.getKey(),
258 stats.dismissalSurface,
259 stats.dismissalSentiment,
260 stats.notificationVisibility);
261
262 // THEN we fire a remove event
263 verify(mCollectionListener).onEntryRemoved(entry2, REASON_CLICK, true);
264 }
265
266 @Test(expected = IllegalStateException.class)
267 public void testDismissingNonExistentNotificationThrows() {
268 // GIVEN a collection that originally had three notifs, but where one was dismissed
Ned Burnsd7bf7922019-12-19 16:13:01 -0500269 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
270 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
271 NotifEvent notif3 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 99));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400272 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
273 mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
274
275 // WHEN we try to dismiss a notification that isn't present
276 mCollection.dismissNotification(
277 entry2,
278 REASON_CLICK,
279 new DismissedByUserStats(0, 0, NotificationVisibility.obtain("foo", 47, 3, true)));
280
281 // THEN an exception is thrown
282 }
283
284 @Test
285 public void testLifetimeExtendersAreQueriedWhenNotifRemoved() {
286 // GIVEN a couple notifications and a few lifetime extenders
287 mExtender1.shouldExtendLifetime = true;
288 mExtender2.shouldExtendLifetime = true;
289
290 mCollection.addNotificationLifetimeExtender(mExtender1);
291 mCollection.addNotificationLifetimeExtender(mExtender2);
292 mCollection.addNotificationLifetimeExtender(mExtender3);
293
Ned Burnsd7bf7922019-12-19 16:13:01 -0500294 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
295 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400296 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
297
298 // WHEN a notification is removed
299 mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
300
301 // THEN each extender is asked whether to extend, even if earlier ones return true
302 verify(mExtender1).shouldExtendLifetime(entry2, REASON_UNKNOWN);
303 verify(mExtender2).shouldExtendLifetime(entry2, REASON_UNKNOWN);
304 verify(mExtender3).shouldExtendLifetime(entry2, REASON_UNKNOWN);
305
306 // THEN the entry is not removed
307 assertTrue(mCollection.getNotifs().contains(entry2));
308
309 // THEN the entry properly records all extenders that returned true
310 assertEquals(Arrays.asList(mExtender1, mExtender2), entry2.mLifetimeExtenders);
311 }
312
313 @Test
314 public void testWhenLastLifetimeExtenderExpiresAllAreReQueried() {
315 // GIVEN a couple notifications and a few lifetime extenders
316 mExtender2.shouldExtendLifetime = true;
317
318 mCollection.addNotificationLifetimeExtender(mExtender1);
319 mCollection.addNotificationLifetimeExtender(mExtender2);
320 mCollection.addNotificationLifetimeExtender(mExtender3);
321
Ned Burnsd7bf7922019-12-19 16:13:01 -0500322 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
323 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400324 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
325
326 // GIVEN a notification gets lifetime-extended by one of them
327 mNoMan.retractNotif(notif2.sbn, REASON_APP_CANCEL);
328 assertTrue(mCollection.getNotifs().contains(entry2));
329 clearInvocations(mExtender1, mExtender2, mExtender3);
330
331 // WHEN the last active extender expires (but new ones become active)
332 mExtender1.shouldExtendLifetime = true;
333 mExtender2.shouldExtendLifetime = false;
334 mExtender3.shouldExtendLifetime = true;
335 mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2);
336
337 // THEN each extender is re-queried
338 verify(mExtender1).shouldExtendLifetime(entry2, REASON_UNKNOWN);
339 verify(mExtender2).shouldExtendLifetime(entry2, REASON_UNKNOWN);
340 verify(mExtender3).shouldExtendLifetime(entry2, REASON_UNKNOWN);
341
342 // THEN the entry is not removed
343 assertTrue(mCollection.getNotifs().contains(entry2));
344
345 // THEN the entry properly records all extenders that returned true
346 assertEquals(Arrays.asList(mExtender1, mExtender3), entry2.mLifetimeExtenders);
347 }
348
349 @Test
350 public void testExtendersAreNotReQueriedUntilFinalActiveExtenderExpires() {
351 // GIVEN a couple notifications and a few lifetime extenders
352 mExtender1.shouldExtendLifetime = true;
353 mExtender2.shouldExtendLifetime = true;
354
355 mCollection.addNotificationLifetimeExtender(mExtender1);
356 mCollection.addNotificationLifetimeExtender(mExtender2);
357 mCollection.addNotificationLifetimeExtender(mExtender3);
358
Ned Burnsd7bf7922019-12-19 16:13:01 -0500359 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
360 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400361 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
362
363 // GIVEN a notification gets lifetime-extended by a couple of them
364 mNoMan.retractNotif(notif2.sbn, REASON_APP_CANCEL);
365 assertTrue(mCollection.getNotifs().contains(entry2));
366 clearInvocations(mExtender1, mExtender2, mExtender3);
367
368 // WHEN one (but not all) of the extenders expires
369 mExtender2.shouldExtendLifetime = false;
370 mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2);
371
372 // THEN the entry is not removed
373 assertTrue(mCollection.getNotifs().contains(entry2));
374
375 // THEN we don't re-query the extenders
376 verify(mExtender1, never()).shouldExtendLifetime(eq(entry2), anyInt());
377 verify(mExtender2, never()).shouldExtendLifetime(eq(entry2), anyInt());
378 verify(mExtender3, never()).shouldExtendLifetime(eq(entry2), anyInt());
379
380 // THEN the entry properly records all extenders that returned true
381 assertEquals(Arrays.asList(mExtender1), entry2.mLifetimeExtenders);
382 }
383
384 @Test
385 public void testNotificationIsRemovedWhenAllLifetimeExtendersExpire() {
386 // GIVEN a couple notifications and a few lifetime extenders
387 mExtender1.shouldExtendLifetime = true;
388 mExtender2.shouldExtendLifetime = true;
389
390 mCollection.addNotificationLifetimeExtender(mExtender1);
391 mCollection.addNotificationLifetimeExtender(mExtender2);
392 mCollection.addNotificationLifetimeExtender(mExtender3);
393
Ned Burnsd7bf7922019-12-19 16:13:01 -0500394 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
395 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400396 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
397
398 // GIVEN a notification gets lifetime-extended by a couple of them
399 mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
400 assertTrue(mCollection.getNotifs().contains(entry2));
401 clearInvocations(mExtender1, mExtender2, mExtender3);
402
403 // WHEN all of the active extenders expire
404 mExtender2.shouldExtendLifetime = false;
405 mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2);
406 mExtender1.shouldExtendLifetime = false;
407 mExtender1.callback.onEndLifetimeExtension(mExtender1, entry2);
408
409 // THEN the entry removed
410 assertFalse(mCollection.getNotifs().contains(entry2));
411 verify(mCollectionListener).onEntryRemoved(entry2, REASON_UNKNOWN, false);
412 }
413
414 @Test
415 public void testLifetimeExtensionIsCanceledWhenNotifIsUpdated() {
416 // GIVEN a few lifetime extenders and a couple notifications
417 mCollection.addNotificationLifetimeExtender(mExtender1);
418 mCollection.addNotificationLifetimeExtender(mExtender2);
419 mCollection.addNotificationLifetimeExtender(mExtender3);
420
421 mExtender1.shouldExtendLifetime = true;
422 mExtender2.shouldExtendLifetime = true;
423
Ned Burnsd7bf7922019-12-19 16:13:01 -0500424 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
425 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400426 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
427
428 // GIVEN a notification gets lifetime-extended by a couple of them
429 mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
430 assertTrue(mCollection.getNotifs().contains(entry2));
431 clearInvocations(mExtender1, mExtender2, mExtender3);
432
433 // WHEN the notification is reposted
434 mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
435
436 // THEN all of the active lifetime extenders are canceled
437 verify(mExtender1).cancelLifetimeExtension(entry2);
438 verify(mExtender2).cancelLifetimeExtension(entry2);
439
440 // THEN the notification is still present
441 assertTrue(mCollection.getNotifs().contains(entry2));
442 }
443
444 @Test(expected = IllegalStateException.class)
445 public void testReentrantCallsToLifetimeExtendersThrow() {
446 // GIVEN a few lifetime extenders and a couple notifications
447 mCollection.addNotificationLifetimeExtender(mExtender1);
448 mCollection.addNotificationLifetimeExtender(mExtender2);
449 mCollection.addNotificationLifetimeExtender(mExtender3);
450
451 mExtender1.shouldExtendLifetime = true;
452 mExtender2.shouldExtendLifetime = true;
453
Ned Burnsd7bf7922019-12-19 16:13:01 -0500454 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
455 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400456 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
457
458 // GIVEN a notification gets lifetime-extended by a couple of them
459 mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
460 assertTrue(mCollection.getNotifs().contains(entry2));
461 clearInvocations(mExtender1, mExtender2, mExtender3);
462
463 // WHEN a lifetime extender makes a reentrant call during cancelLifetimeExtension()
464 mExtender2.onCancelLifetimeExtension = () -> {
465 mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2);
466 };
467 // This triggers the call to cancelLifetimeExtension()
468 mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
469
470 // THEN an exception is thrown
471 }
472
473 @Test
474 public void testRankingIsUpdatedWhenALifetimeExtendedNotifIsReposted() {
475 // GIVEN a few lifetime extenders and a couple notifications
476 mCollection.addNotificationLifetimeExtender(mExtender1);
477 mCollection.addNotificationLifetimeExtender(mExtender2);
478 mCollection.addNotificationLifetimeExtender(mExtender3);
479
480 mExtender1.shouldExtendLifetime = true;
481 mExtender2.shouldExtendLifetime = true;
482
Ned Burnsd7bf7922019-12-19 16:13:01 -0500483 NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
484 NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
Ned Burnsf098dbf2019-09-13 19:17:53 -0400485 NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
486
487 // GIVEN a notification gets lifetime-extended by a couple of them
488 mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
489 assertTrue(mCollection.getNotifs().contains(entry2));
490 clearInvocations(mExtender1, mExtender2, mExtender3);
491
492 // WHEN the notification is reposted
Ned Burnsd7bf7922019-12-19 16:13:01 -0500493 NotifEvent notif2a = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88)
Ned Burnsf098dbf2019-09-13 19:17:53 -0400494 .setRank(4747)
495 .setExplanation("Some new explanation"));
496
497 // THEN the notification's ranking is properly updated
Evan Laird9afe7662019-10-16 17:16:39 -0400498 assertEquals(notif2a.ranking, entry2.getRanking());
Ned Burnsf098dbf2019-09-13 19:17:53 -0400499 }
500
501 private static NotificationEntryBuilder buildNotif(String pkg, int id, String tag) {
502 return new NotificationEntryBuilder()
503 .setPkg(pkg)
504 .setId(id)
505 .setTag(tag);
506 }
507
508 private static NotificationEntryBuilder buildNotif(String pkg, int id) {
509 return new NotificationEntryBuilder()
510 .setPkg(pkg)
511 .setId(id);
512 }
513
Ned Burnsf098dbf2019-09-13 19:17:53 -0400514 private static class RecordingCollectionListener implements NotifCollectionListener {
515 private final Map<String, NotificationEntry> mLastSeenEntries = new ArrayMap<>();
516
517 @Override
518 public void onEntryAdded(NotificationEntry entry) {
Evan Laird9afe7662019-10-16 17:16:39 -0400519 mLastSeenEntries.put(entry.getKey(), entry);
Ned Burnsf098dbf2019-09-13 19:17:53 -0400520 }
521
522 @Override
523 public void onEntryUpdated(NotificationEntry entry) {
524 }
525
526 @Override
527 public void onEntryRemoved(NotificationEntry entry, int reason, boolean removedByUser) {
528 }
529
530 public NotificationEntry getEntry(String key) {
531 if (!mLastSeenEntries.containsKey(key)) {
532 throw new RuntimeException("Key not found: " + key);
533 }
534 return mLastSeenEntries.get(key);
535 }
536 }
537
538 private static class RecordingLifetimeExtender implements NotifLifetimeExtender {
539 private final String mName;
540
541 public @Nullable OnEndLifetimeExtensionCallback callback;
542 public boolean shouldExtendLifetime = false;
543 public @Nullable Runnable onCancelLifetimeExtension;
544
545 private RecordingLifetimeExtender(String name) {
546 mName = name;
547 }
548
549 @Override
550 public String getName() {
551 return mName;
552 }
553
554 @Override
555 public void setCallback(OnEndLifetimeExtensionCallback callback) {
556 this.callback = callback;
557 }
558
559 @Override
560 public boolean shouldExtendLifetime(
561 NotificationEntry entry,
562 @CancellationReason int reason) {
563 return shouldExtendLifetime;
564 }
565
566 @Override
567 public void cancelLifetimeExtension(NotificationEntry entry) {
568 if (onCancelLifetimeExtension != null) {
569 onCancelLifetimeExtension.run();
570 }
571 }
572 }
573
574 private static final String TEST_PACKAGE = "com.android.test.collection";
575 private static final String TEST_PACKAGE2 = "com.android.test.collection2";
576}