blob: 4df99be9efd156a944530c34bf4da08c6a71c890 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.notification.collection;
import static android.app.Notification.FLAG_FOREGROUND_SERVICE;
import static android.app.Notification.FLAG_NO_CLEAR;
import static android.app.Notification.FLAG_ONGOING_EVENT;
import static android.service.notification.NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED;
import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
import static android.service.notification.NotificationListenerService.REASON_CANCEL;
import static android.service.notification.NotificationListenerService.REASON_CLICK;
import static android.service.notification.NotificationStats.DISMISSAL_SHADE;
import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL;
import static com.android.systemui.statusbar.notification.collection.NotifCollection.REASON_NOT_CANCELED;
import static com.android.systemui.statusbar.notification.collection.NotifCollection.REASON_UNKNOWN;
import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.DISMISSED;
import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.NOT_DISMISSED;
import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static java.util.Collections.singletonList;
import static java.util.Objects.requireNonNull;
import android.annotation.Nullable;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.os.Handler;
import android.os.RemoteException;
import android.service.notification.NotificationListenerService.Ranking;
import android.service.notification.NotificationListenerService.RankingMap;
import android.service.notification.StatusBarNotification;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.statusbar.NotificationVisibility;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.dump.LogBufferEulogizer;
import com.android.systemui.statusbar.RankingBuilder;
import com.android.systemui.statusbar.notification.NotifPipelineFlags;
import com.android.systemui.statusbar.notification.collection.NoManSimulator.NotifEvent;
import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason;
import com.android.systemui.statusbar.notification.collection.coalescer.CoalescedEvent;
import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer;
import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer.BatchableNotificationHandler;
import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener;
import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats;
import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLogger;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class NotifCollectionTest extends SysuiTestCase {
@Mock private IStatusBarService mStatusBarService;
@Mock private NotifPipelineFlags mNotifPipelineFlags;
@Mock private NotifCollectionLogger mLogger;
@Mock private LogBufferEulogizer mEulogizer;
@Mock private Handler mMainHandler;
@Mock private GroupCoalescer mGroupCoalescer;
@Spy private RecordingCollectionListener mCollectionListener;
@Mock private CollectionReadyForBuildListener mBuildListener;
@Spy private RecordingLifetimeExtender mExtender1 = new RecordingLifetimeExtender("Extender1");
@Spy private RecordingLifetimeExtender mExtender2 = new RecordingLifetimeExtender("Extender2");
@Spy private RecordingLifetimeExtender mExtender3 = new RecordingLifetimeExtender("Extender3");
@Spy private RecordingDismissInterceptor mInterceptor1 = new RecordingDismissInterceptor(
"Interceptor1");
@Spy private RecordingDismissInterceptor mInterceptor2 = new RecordingDismissInterceptor(
"Interceptor2");
@Spy private RecordingDismissInterceptor mInterceptor3 = new RecordingDismissInterceptor(
"Interceptor3");
@Captor private ArgumentCaptor<BatchableNotificationHandler> mListenerCaptor;
@Captor private ArgumentCaptor<NotificationEntry> mEntryCaptor;
@Captor private ArgumentCaptor<Collection<NotificationEntry>> mBuildListCaptor;
private NotifCollection mCollection;
private BatchableNotificationHandler mNotifHandler;
private InOrder mListenerInOrder;
private NoManSimulator mNoMan;
private FakeSystemClock mClock = new FakeSystemClock();
private FakeExecutor mBgExecutor = new FakeExecutor(mClock);
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
allowTestableLooperAsMainThread();
when(mEulogizer.record(any(Exception.class))).thenAnswer(i -> i.getArguments()[0]);
mListenerInOrder = inOrder(mCollectionListener);
mCollection = new NotifCollection(
mStatusBarService,
mClock,
mNotifPipelineFlags,
mLogger,
mMainHandler,
mBgExecutor,
mEulogizer,
mock(DumpManager.class));
mCollection.attach(mGroupCoalescer);
mCollection.addCollectionListener(mCollectionListener);
mCollection.setBuildListener(mBuildListener);
// Capture the listener object that the collection registers with the listener service so
// we can simulate listener service events in tests below
verify(mGroupCoalescer).setNotificationHandler(mListenerCaptor.capture());
mNotifHandler = requireNonNull(mListenerCaptor.getValue());
mNoMan = new NoManSimulator();
mNoMan.addListener(mNotifHandler);
mNotifHandler.onNotificationsInitialized();
}
@Test
public void testGetGroupSummary() {
final NotificationEntryBuilder entryBuilder = buildNotif(TEST_PACKAGE, 0)
.setGroup(mContext, "group")
.setGroupSummary(mContext, true);
final String groupKey = entryBuilder.build().getSbn().getGroupKey();
assertEquals(null, mCollection.getGroupSummary(groupKey));
NotifEvent summary = mNoMan.postNotif(entryBuilder);
final NotificationEntry entry = mCollection.getGroupSummary(groupKey);
assertEquals(summary.key, entry.getKey());
assertEquals(summary.sbn, entry.getSbn());
assertEquals(summary.ranking, entry.getRanking());
}
@Test
public void testIsOnlyChildInGroup() {
final NotificationEntryBuilder entryBuilder = buildNotif(TEST_PACKAGE, 1)
.setGroup(mContext, "group");
NotifEvent notif1 = mNoMan.postNotif(entryBuilder);
final NotificationEntry entry = mCollection.getEntry(notif1.key);
assertTrue(mCollection.isOnlyChildInGroup(entry));
// summaries are not counted
mNoMan.postNotif(
buildNotif(TEST_PACKAGE, 0)
.setGroup(mContext, "group")
.setGroupSummary(mContext, true));
assertTrue(mCollection.isOnlyChildInGroup(entry));
mNoMan.postNotif(
buildNotif(TEST_PACKAGE, 2)
.setGroup(mContext, "group"));
assertFalse(mCollection.isOnlyChildInGroup(entry));
}
@Test
public void testEventDispatchedWhenNotifPosted() {
// WHEN a notification is posted
NotifEvent notif1 = mNoMan.postNotif(
buildNotif(TEST_PACKAGE, 3)
.setRank(4747));
// THEN the listener is notified
final NotificationEntry entry = mCollectionListener.getEntry(notif1.key);
mListenerInOrder.verify(mCollectionListener).onEntryInit(entry);
mListenerInOrder.verify(mCollectionListener).onEntryAdded(entry);
mListenerInOrder.verify(mCollectionListener).onRankingApplied();
assertEquals(notif1.key, entry.getKey());
assertEquals(notif1.sbn, entry.getSbn());
assertEquals(notif1.ranking, entry.getRanking());
}
@Test
public void testCancelNonExistingNotification() {
NotifEvent notif = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag"));
NotificationEntry entry = mCollectionListener.getEntry(notif.key);
mCollection.dismissNotification(entry, defaultStats(entry));
mCollection.dismissNotification(entry, defaultStats(entry));
mCollection.dismissNotification(entry, defaultStats(entry));
}
@Test
public void testEventDispatchedWhenNotifBatchPosted() {
// GIVEN a NotifCollection with one notif already posted
mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2)
.setGroup(mContext, "group_1")
.setContentTitle(mContext, "Old version"));
clearInvocations(mCollectionListener);
clearInvocations(mBuildListener);
// WHEN three notifications from the same group are posted (one of them an update, two of
// them new)
NotificationEntry entry1 = buildNotif(TEST_PACKAGE, 1)
.setGroup(mContext, "group_1")
.build();
NotificationEntry entry2 = buildNotif(TEST_PACKAGE, 2)
.setGroup(mContext, "group_1")
.setContentTitle(mContext, "New version")
.build();
NotificationEntry entry3 = buildNotif(TEST_PACKAGE, 3)
.setGroup(mContext, "group_1")
.build();
mNotifHandler.onNotificationBatchPosted(Arrays.asList(
new CoalescedEvent(entry1.getKey(), 0, entry1.getSbn(), entry1.getRanking(), null),
new CoalescedEvent(entry2.getKey(), 1, entry2.getSbn(), entry2.getRanking(), null),
new CoalescedEvent(entry3.getKey(), 2, entry3.getSbn(), entry3.getRanking(), null)
));
// THEN onEntryAdded is called on the new ones
verify(mCollectionListener, times(2)).onEntryAdded(mEntryCaptor.capture());
List<NotificationEntry> capturedAdds = mEntryCaptor.getAllValues();
assertEquals(entry1.getSbn(), capturedAdds.get(0).getSbn());
assertEquals(entry1.getRanking(), capturedAdds.get(0).getRanking());
assertEquals(entry3.getSbn(), capturedAdds.get(1).getSbn());
assertEquals(entry3.getRanking(), capturedAdds.get(1).getRanking());
// THEN onEntryUpdated is called on the middle one
verify(mCollectionListener).onEntryUpdated(mEntryCaptor.capture());
NotificationEntry capturedUpdate = mEntryCaptor.getValue();
assertEquals(entry2.getSbn(), capturedUpdate.getSbn());
assertEquals(entry2.getRanking(), capturedUpdate.getRanking());
// THEN onBuildList is called only once
verifyBuiltList(
List.of(
capturedAdds.get(0),
capturedAdds.get(1),
capturedUpdate));
}
@Test
public void testEventDispatchedWhenNotifUpdated() {
// GIVEN a collection with one notif
mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3)
.setRank(4747));
// WHEN the notif is reposted
NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3)
.setRank(89));
// THEN the listener is notified
final NotificationEntry entry = mCollectionListener.getEntry(notif2.key);
mListenerInOrder.verify(mCollectionListener).onEntryUpdated(entry);
mListenerInOrder.verify(mCollectionListener).onRankingApplied();
assertEquals(notif2.key, entry.getKey());
assertEquals(notif2.sbn, entry.getSbn());
assertEquals(notif2.ranking, entry.getRanking());
}
@Test
public void testEventDispatchedWhenNotifRemoved() {
// GIVEN a collection with one notif
mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3));
clearInvocations(mCollectionListener);
NotifEvent notif = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
NotificationEntry entry = mCollectionListener.getEntry(notif.key);
clearInvocations(mCollectionListener);
// WHEN a notif is retracted
mNoMan.retractNotif(notif.sbn, REASON_APP_CANCEL);
// THEN the listener is notified
mListenerInOrder.verify(mCollectionListener).onEntryRemoved(entry, REASON_APP_CANCEL);
mListenerInOrder.verify(mCollectionListener).onEntryCleanUp(entry);
mListenerInOrder.verify(mCollectionListener).onRankingApplied();
assertEquals(notif.sbn, entry.getSbn());
assertEquals(notif.ranking, entry.getRanking());
}
@Test
public void testEventDispatchedWhenChannelChanged() {
// GIVEN a collection with one notif that has a channel
NotificationEntryBuilder neb = buildNotif(TEST_PACKAGE, 48);
NotificationChannel channel = new NotificationChannel(
"channelId",
"channelName",
NotificationManager.IMPORTANCE_DEFAULT);
neb.setChannel(channel);
NotifEvent notif = mNoMan.postNotif(neb);
NotificationEntry entry = mCollectionListener.getEntry(notif.key);
clearInvocations(mCollectionListener);
// WHEN a notif channel is modified
channel.setAllowBubbles(true);
mNoMan.issueChannelModification(
TEST_PACKAGE,
entry.getSbn().getUser(),
channel,
NOTIFICATION_CHANNEL_OR_GROUP_UPDATED);
// THEN the listener is notified
mListenerInOrder.verify(mCollectionListener).onNotificationChannelModified(
TEST_PACKAGE,
entry.getSbn().getUser(),
channel,
NOTIFICATION_CHANNEL_OR_GROUP_UPDATED);
}
@Test
public void testRankingsAreUpdatedForOtherNotifs() {
// GIVEN a collection with one notif
NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3)
.setRank(47));
NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
// WHEN a new notif is posted, triggering a rerank
mNoMan.setRanking(notif1.sbn.getKey(), new RankingBuilder(notif1.ranking)
.setRank(56)
.build());
mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 77));
// THEN the ranking is updated on the first entry
assertEquals(56, entry1.getRanking().getRank());
}
@Test
public void testRankingUpdateIsProperlyIssuedToEveryone() {
// GIVEN a collection with a couple notifs
NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3)
.setRank(3));
NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 8)
.setRank(2));
NotifEvent notif3 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 77)
.setRank(1));
NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
NotificationEntry entry3 = mCollectionListener.getEntry(notif3.key);
// WHEN a ranking update is delivered
Ranking newRanking1 = new RankingBuilder(notif1.ranking)
.setRank(4)
.setExplanation("Foo bar")
.build();
Ranking newRanking2 = new RankingBuilder(notif2.ranking)
.setRank(5)
.setExplanation("baz buzz")
.build();
// WHEN entry3's ranking update includes an update to its overrideGroupKey
final String newOverrideGroupKey = "newOverrideGroupKey";
Ranking newRanking3 = new RankingBuilder(notif3.ranking)
.setRank(6)
.setExplanation("Penguin pizza")
.setOverrideGroupKey(newOverrideGroupKey)
.build();
mNoMan.setRanking(notif1.sbn.getKey(), newRanking1);
mNoMan.setRanking(notif2.sbn.getKey(), newRanking2);
mNoMan.setRanking(notif3.sbn.getKey(), newRanking3);
mNoMan.issueRankingUpdate();
// THEN all of the NotifEntries have their rankings properly updated
assertEquals(newRanking1, entry1.getRanking());
assertEquals(newRanking2, entry2.getRanking());
assertEquals(newRanking3, entry3.getRanking());
// THEN the entry3's overrideGroupKey is updated along with its groupKey
assertEquals(newOverrideGroupKey, entry3.getSbn().getOverrideGroupKey());
assertNotNull(entry3.getSbn().getGroupKey());
}
@Test
public void testNotifEntriesAreNotPersistedAcrossRemovalAndReposting() {
// GIVEN a notification that has been posted
NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3));
NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
// WHEN the notification is retracted and then reposted
mNoMan.retractNotif(notif1.sbn, REASON_APP_CANCEL);
mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3));
// THEN the new NotificationEntry is a new object
NotificationEntry entry2 = mCollectionListener.getEntry(notif1.key);
assertNotEquals(entry2, entry1);
}
@Test
public void testDismissNotificationSentToSystemServer() throws RemoteException {
// GIVEN a collection with a couple notifications
NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag"));
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
// WHEN a notification is manually dismissed
DismissedByUserStats stats = defaultStats(entry2);
mCollection.dismissNotification(entry2, defaultStats(entry2));
FakeExecutor.exhaustExecutors(mBgExecutor);
// THEN we send the dismissal to system server
verify(mStatusBarService).onNotificationClear(
notif2.sbn.getPackageName(),
notif2.sbn.getUser().getIdentifier(),
notif2.sbn.getKey(),
stats.dismissalSurface,
stats.dismissalSentiment,
stats.notificationVisibility);
}
@Test
public void testDismissedNotificationsAreMarkedAsDismissedLocally() {
// GIVEN a collection with a notification
NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
// WHEN a notification is manually dismissed
mCollection.dismissNotification(entry1, defaultStats(entry1));
// THEN the entry is marked as dismissed locally
assertEquals(DISMISSED, entry1.getDismissState());
}
@Test
public void testDismissedNotificationsCannotBeLifetimeExtended() {
// GIVEN a collection with a notification and a lifetime extender
mCollection.addNotificationLifetimeExtender(mExtender1);
NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
// WHEN a notification is manually dismissed
mCollection.dismissNotification(entry1, defaultStats(entry1));
// THEN lifetime extenders are never queried
verify(mExtender1, never()).maybeExtendLifetime(eq(entry1), anyInt());
}
@Test
public void testDismissedNotificationsDoNotTriggerRemovalEvents() {
// GIVEN a collection with a notification
NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
// WHEN a notification is manually dismissed
mCollection.dismissNotification(entry1, defaultStats(entry1));
// THEN onEntryRemoved is not called
verify(mCollectionListener, never()).onEntryRemoved(eq(entry1), anyInt());
}
@Test
public void testDismissedNotificationsStillAppearInNotificationSet() {
// GIVEN a collection with a notification
NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
// WHEN a notification is manually dismissed
mCollection.dismissNotification(entry1, defaultStats(entry1));
// THEN the dismissed entry still appears in the notification set
assertEquals(
new ArraySet<>(singletonList(entry1)),
new ArraySet<>(mCollection.getAllNotifs()));
}
@Test
public void testRetractingLifetimeExtendedSummaryDoesNotDismissChildren() {
// GIVEN A notif group with one summary and two children
mCollection.addNotificationLifetimeExtender(mExtender1);
CollectionEvent notif1 = postNotif(
buildNotif(TEST_PACKAGE, 1, "myTag")
.setGroup(mContext, GROUP_1)
.setGroupSummary(mContext, true));
CollectionEvent notif2 = postNotif(
buildNotif(TEST_PACKAGE, 2, "myTag")
.setGroup(mContext, GROUP_1));
CollectionEvent notif3 = postNotif(
buildNotif(TEST_PACKAGE, 3, "myTag")
.setGroup(mContext, GROUP_1));
NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
NotificationEntry entry3 = mCollectionListener.getEntry(notif3.key);
// GIVEN that the summary and one child are retracted by the app, but both are
// lifetime-extended
mExtender1.shouldExtendLifetime = true;
mNoMan.retractNotif(notif1.sbn, REASON_APP_CANCEL);
mNoMan.retractNotif(notif2.sbn, REASON_APP_CANCEL);
assertEquals(
new ArraySet<>(List.of(entry1, entry2, entry3)),
new ArraySet<>(mCollection.getAllNotifs()));
// WHEN the summary is retracted by the app
mCollection.dismissNotification(entry1, defaultStats(entry1));
// THEN the summary is removed, but both children stick around
assertEquals(
new ArraySet<>(List.of(entry2, entry3)),
new ArraySet<>(mCollection.getAllNotifs()));
assertEquals(NOT_DISMISSED, entry2.getDismissState());
assertEquals(NOT_DISMISSED, entry3.getDismissState());
}
@Test
public void testNMSReportsUserDismissalAlwaysRemovesNotif() throws RemoteException {
// GIVEN notifications are lifetime extended
mExtender1.shouldExtendLifetime = true;
CollectionEvent notif = postNotif(buildNotif(TEST_PACKAGE, 1, "myTag"));
CollectionEvent notif2 = postNotif(buildNotif(TEST_PACKAGE, 2, "myTag"));
NotificationEntry entry = mCollectionListener.getEntry(notif.key);
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
assertEquals(
new ArraySet<>(List.of(entry, entry2)),
new ArraySet<>(mCollection.getAllNotifs()));
// WHEN the notifications are reported to be dismissed by the user by NMS
mNoMan.retractNotif(notif.sbn, REASON_CANCEL);
mNoMan.retractNotif(notif2.sbn, REASON_CLICK);
// THEN the notifications are removed b/c they were dismissed by the user
assertEquals(
new ArraySet<>(List.of()),
new ArraySet<>(mCollection.getAllNotifs()));
}
@Test
public void testDismissNotificationCallsDismissInterceptors() throws RemoteException {
// GIVEN a collection with notifications with multiple dismiss interceptors
mInterceptor1.shouldInterceptDismissal = true;
mInterceptor2.shouldInterceptDismissal = true;
mInterceptor3.shouldInterceptDismissal = false;
mCollection.addNotificationDismissInterceptor(mInterceptor1);
mCollection.addNotificationDismissInterceptor(mInterceptor2);
mCollection.addNotificationDismissInterceptor(mInterceptor3);
NotifEvent notif = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
NotificationEntry entry = mCollectionListener.getEntry(notif.key);
// WHEN a notification is manually dismissed
DismissedByUserStats stats = defaultStats(entry);
mCollection.dismissNotification(entry, stats);
// THEN all interceptors get checked
verify(mInterceptor1).shouldInterceptDismissal(entry);
verify(mInterceptor2).shouldInterceptDismissal(entry);
verify(mInterceptor3).shouldInterceptDismissal(entry);
assertEquals(List.of(mInterceptor1, mInterceptor2), entry.mDismissInterceptors);
// THEN we never send the dismissal to system server
verify(mStatusBarService, never()).onNotificationClear(
notif.sbn.getPackageName(),
notif.sbn.getUser().getIdentifier(),
notif.sbn.getKey(),
stats.dismissalSurface,
stats.dismissalSentiment,
stats.notificationVisibility);
}
@Test
public void testDismissInterceptorsCanceledWhenNotifIsUpdated() throws RemoteException {
// GIVEN a few lifetime extenders and a couple notifications
mCollection.addNotificationDismissInterceptor(mInterceptor1);
mCollection.addNotificationDismissInterceptor(mInterceptor2);
mInterceptor1.shouldInterceptDismissal = true;
mInterceptor2.shouldInterceptDismissal = true;
NotifEvent notif = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
NotificationEntry entry = mCollectionListener.getEntry(notif.key);
// WHEN a notification is manually dismissed and intercepted
DismissedByUserStats stats = defaultStats(entry);
mCollection.dismissNotification(entry, stats);
assertEquals(List.of(mInterceptor1, mInterceptor2), entry.mDismissInterceptors);
clearInvocations(mInterceptor1, mInterceptor2);
// WHEN the notification is reposted
mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
// THEN all of the active dismissal interceptors are canceled
verify(mInterceptor1).cancelDismissInterception(entry);
verify(mInterceptor2).cancelDismissInterception(entry);
assertEquals(List.of(), entry.mDismissInterceptors);
// THEN the notification is never sent to system server to dismiss
verify(mStatusBarService, never()).onNotificationClear(
eq(notif.sbn.getPackageName()),
eq(notif.sbn.getUser().getIdentifier()),
eq(notif.sbn.getKey()),
anyInt(),
anyInt(),
eq(stats.notificationVisibility));
}
@Test
public void testEndingAllDismissInterceptorsSendsDismiss() throws RemoteException {
// GIVEN a collection with notifications a dismiss interceptor
mInterceptor1.shouldInterceptDismissal = true;
mCollection.addNotificationDismissInterceptor(mInterceptor1);
NotifEvent notif = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
NotificationEntry entry = mCollectionListener.getEntry(notif.key);
// GIVEN a notification is manually dismissed
DismissedByUserStats stats = defaultStats(entry);
mCollection.dismissNotification(entry, stats);
// WHEN all interceptors end their interception dismissal
mInterceptor1.shouldInterceptDismissal = false;
mInterceptor1.onEndInterceptionCallback.onEndDismissInterception(mInterceptor1, entry,
stats);
FakeExecutor.exhaustExecutors(mBgExecutor);
// THEN we send the dismissal to system server
verify(mStatusBarService).onNotificationClear(
eq(notif.sbn.getPackageName()),
eq(notif.sbn.getUser().getIdentifier()),
eq(notif.sbn.getKey()),
anyInt(),
anyInt(),
eq(stats.notificationVisibility));
}
@Test
public void testEndDismissInterceptionUpdatesDismissInterceptors() {
// GIVEN a collection with notifications with multiple dismiss interceptors
mInterceptor1.shouldInterceptDismissal = true;
mInterceptor2.shouldInterceptDismissal = true;
mInterceptor3.shouldInterceptDismissal = false;
mCollection.addNotificationDismissInterceptor(mInterceptor1);
mCollection.addNotificationDismissInterceptor(mInterceptor2);
mCollection.addNotificationDismissInterceptor(mInterceptor3);
NotifEvent notif = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
NotificationEntry entry = mCollectionListener.getEntry(notif.key);
// GIVEN a notification is manually dismissed
mCollection.dismissNotification(entry, defaultStats(entry));
// WHEN an interceptor ends its interception
mInterceptor1.shouldInterceptDismissal = false;
mInterceptor1.onEndInterceptionCallback.onEndDismissInterception(mInterceptor1, entry,
defaultStats(entry));
// THEN all interceptors get checked
verify(mInterceptor1).shouldInterceptDismissal(entry);
verify(mInterceptor2).shouldInterceptDismissal(entry);
verify(mInterceptor3).shouldInterceptDismissal(entry);
// THEN mInterceptor2 is the only dismiss interceptor
assertEquals(List.of(mInterceptor2), entry.mDismissInterceptors);
}
@Test(expected = IllegalStateException.class)
public void testEndingDismissalOfNonInterceptedThrows() {
// GIVEN a collection with notifications with a dismiss interceptor that hasn't been called
mInterceptor1.shouldInterceptDismissal = false;
mCollection.addNotificationDismissInterceptor(mInterceptor1);
NotifEvent notif = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
NotificationEntry entry = mCollectionListener.getEntry(notif.key);
// WHEN we try to end the dismissal of an interceptor that didn't intercept the notif
mInterceptor1.onEndInterceptionCallback.onEndDismissInterception(mInterceptor1, entry,
defaultStats(entry));
// THEN an exception is thrown
}
@Test
public void testGroupChildrenAreDismissedLocallyWhenSummaryIsDismissed() {
// GIVEN a collection with two grouped notifs in it
CollectionEvent notif0 = postNotif(
buildNotif(TEST_PACKAGE, 0)
.setGroup(mContext, GROUP_1)
.setGroupSummary(mContext, true));
CollectionEvent notif1 = postNotif(
buildNotif(TEST_PACKAGE, 1)
.setGroup(mContext, GROUP_1));
NotificationEntry entry0 = mCollectionListener.getEntry(notif0.key);
NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
// WHEN the summary is dismissed
mCollection.dismissNotification(entry0, defaultStats(entry0));
// THEN all members of the group are marked as dismissed locally
assertEquals(DISMISSED, entry0.getDismissState());
assertEquals(PARENT_DISMISSED, entry1.getDismissState());
}
@Test
public void testUpdatingDismissedSummaryBringsChildrenBack() {
// GIVEN a collection with two grouped notifs in it
CollectionEvent notif0 = postNotif(
buildNotif(TEST_PACKAGE, 0)
.setGroup(mContext, GROUP_1)
.setGroupSummary(mContext, true));
CollectionEvent notif1 = postNotif(
buildNotif(TEST_PACKAGE, 1)
.setGroup(mContext, GROUP_1));
NotificationEntry entry0 = mCollectionListener.getEntry(notif0.key);
NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
// WHEN the summary is dismissed but then reposted without a group
mCollection.dismissNotification(entry0, defaultStats(entry0));
NotifEvent notif0a = mNoMan.postNotif(
buildNotif(TEST_PACKAGE, 0));
// THEN it and all of its previous children are no longer dismissed locally
assertEquals(NOT_DISMISSED, entry0.getDismissState());
assertEquals(NOT_DISMISSED, entry1.getDismissState());
}
@Test
public void testDismissedChildrenAreNotResetByParentUpdate() {
// GIVEN a collection with three grouped notifs in it
CollectionEvent notif0 = postNotif(
buildNotif(TEST_PACKAGE, 0)
.setGroup(mContext, GROUP_1)
.setGroupSummary(mContext, true));
CollectionEvent notif1 = postNotif(
buildNotif(TEST_PACKAGE, 1)
.setGroup(mContext, GROUP_1));
CollectionEvent notif2 = postNotif(
buildNotif(TEST_PACKAGE, 2)
.setGroup(mContext, GROUP_1));
NotificationEntry entry0 = mCollectionListener.getEntry(notif0.key);
NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
// WHEN a child is dismissed, then the parent is dismissed, then the parent is updated
mCollection.dismissNotification(entry1, defaultStats(entry1));
mCollection.dismissNotification(entry0, defaultStats(entry0));
NotifEvent notif0a = mNoMan.postNotif(
buildNotif(TEST_PACKAGE, 0));
// THEN the manually-dismissed child is still marked as dismissed
assertEquals(NOT_DISMISSED, entry0.getDismissState());
assertEquals(DISMISSED, entry1.getDismissState());
assertEquals(NOT_DISMISSED, entry2.getDismissState());
}
@Test
public void testUpdatingGroupKeyOfDismissedSummaryBringsChildrenBack() {
// GIVEN a collection with two grouped notifs in it
CollectionEvent notif0 = postNotif(
buildNotif(TEST_PACKAGE, 0)
.setOverrideGroupKey(GROUP_1)
.setGroupSummary(mContext, true));
CollectionEvent notif1 = postNotif(
buildNotif(TEST_PACKAGE, 1)
.setOverrideGroupKey(GROUP_1));
NotificationEntry entry0 = mCollectionListener.getEntry(notif0.key);
NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
// WHEN the summary is dismissed but then reposted AND in the same update one of the
// children's ranking loses its override group
mCollection.dismissNotification(entry0, defaultStats(entry0));
mNoMan.setRanking(entry1.getKey(), new RankingBuilder()
.setKey(entry1.getKey())
.build());
mNoMan.postNotif(
buildNotif(TEST_PACKAGE, 0)
.setOverrideGroupKey(GROUP_1)
.setGroupSummary(mContext, true));
// THEN it and all of its previous children are no longer dismissed locally, including the
// child that is no longer part of the group
assertEquals(NOT_DISMISSED, entry0.getDismissState());
assertEquals(NOT_DISMISSED, entry1.getDismissState());
}
@Test
public void testDismissingSummaryDoesDismissForegroundServiceChildren() {
// GIVEN a collection with three grouped notifs in it
CollectionEvent notif0 = postNotif(
buildNotif(TEST_PACKAGE, 0)
.setGroup(mContext, GROUP_1)
.setGroupSummary(mContext, true));
CollectionEvent notif1 = postNotif(
buildNotif(TEST_PACKAGE, 1)
.setGroup(mContext, GROUP_1)
.setFlag(mContext, Notification.FLAG_FOREGROUND_SERVICE, true));
CollectionEvent notif2 = postNotif(
buildNotif(TEST_PACKAGE, 2)
.setGroup(mContext, GROUP_1));
// WHEN the summary is dismissed
mCollection.dismissNotification(notif0.entry, defaultStats(notif0.entry));
// THEN the foreground service child is dismissed
assertEquals(DISMISSED, notif0.entry.getDismissState());
assertEquals(PARENT_DISMISSED, notif1.entry.getDismissState());
assertEquals(PARENT_DISMISSED, notif2.entry.getDismissState());
}
@Test
public void testDismissingSummaryDoesNotDismissOngoingChildren() {
// GIVEN a collection with three grouped notifs in it
CollectionEvent notif0 = postNotif(
buildNotif(TEST_PACKAGE, 0)
.setGroup(mContext, GROUP_1)
.setGroupSummary(mContext, true));
CollectionEvent notif1 = postNotif(
buildNotif(TEST_PACKAGE, 1)
.setGroup(mContext, GROUP_1)
.setFlag(mContext, FLAG_ONGOING_EVENT, true));
CollectionEvent notif2 = postNotif(
buildNotif(TEST_PACKAGE, 2)
.setGroup(mContext, GROUP_1));
// WHEN the summary is dismissed
mCollection.dismissNotification(notif0.entry, defaultStats(notif0.entry));
// THEN the ongoing child is not dismissed
assertEquals(DISMISSED, notif0.entry.getDismissState());
assertEquals(NOT_DISMISSED, notif1.entry.getDismissState());
assertEquals(PARENT_DISMISSED, notif2.entry.getDismissState());
}
@Test
public void testDismissingSummaryDoesNotDismissBubbledChildren() {
// GIVEN a collection with three grouped notifs in it
CollectionEvent notif0 = postNotif(
buildNotif(TEST_PACKAGE, 0)
.setGroup(mContext, GROUP_1)
.setGroupSummary(mContext, true));
CollectionEvent notif1 = postNotif(
buildNotif(TEST_PACKAGE, 1)
.setGroup(mContext, GROUP_1)
.setFlag(mContext, Notification.FLAG_BUBBLE, true));
CollectionEvent notif2 = postNotif(
buildNotif(TEST_PACKAGE, 2)
.setGroup(mContext, GROUP_1));
// WHEN the summary is dismissed
mCollection.dismissNotification(notif0.entry, defaultStats(notif0.entry));
// THEN the bubbled child is not dismissed
assertEquals(DISMISSED, notif0.entry.getDismissState());
assertEquals(NOT_DISMISSED, notif1.entry.getDismissState());
assertEquals(PARENT_DISMISSED, notif2.entry.getDismissState());
}
@Test
public void testDismissingSummaryDoesNotDismissDuplicateSummaries() {
// GIVEN a group with a two summaries
CollectionEvent notif0 = postNotif(
buildNotif(TEST_PACKAGE, 0)
.setGroup(mContext, GROUP_1)
.setGroupSummary(mContext, true));
CollectionEvent notif1 = postNotif(
buildNotif(TEST_PACKAGE, 1)
.setGroup(mContext, GROUP_1)
.setGroupSummary(mContext, true));
CollectionEvent notif2 = postNotif(
buildNotif(TEST_PACKAGE, 2)
.setGroup(mContext, GROUP_1));
// WHEN the first summary is dismissed
mCollection.dismissNotification(notif0.entry, defaultStats(notif0.entry));
// THEN the second summary is not auto-dismissed (but the child is)
assertEquals(DISMISSED, notif0.entry.getDismissState());
assertEquals(NOT_DISMISSED, notif1.entry.getDismissState());
assertEquals(PARENT_DISMISSED, notif2.entry.getDismissState());
}
@Test
public void testLifetimeExtendersAreQueriedWhenNotifRemoved() {
// GIVEN a couple notifications and a few lifetime extenders
mExtender1.shouldExtendLifetime = true;
mExtender2.shouldExtendLifetime = true;
mCollection.addNotificationLifetimeExtender(mExtender1);
mCollection.addNotificationLifetimeExtender(mExtender2);
mCollection.addNotificationLifetimeExtender(mExtender3);
NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
// WHEN a notification is removed by the app
mNoMan.retractNotif(notif2.sbn, REASON_APP_CANCEL);
// THEN each extender is asked whether to extend, even if earlier ones return true
verify(mExtender1).maybeExtendLifetime(entry2, REASON_APP_CANCEL);
verify(mExtender2).maybeExtendLifetime(entry2, REASON_APP_CANCEL);
verify(mExtender3).maybeExtendLifetime(entry2, REASON_APP_CANCEL);
// THEN the entry is not removed
assertTrue(mCollection.getAllNotifs().contains(entry2));
// THEN the entry properly records all extenders that returned true
assertEquals(Arrays.asList(mExtender1, mExtender2), entry2.mLifetimeExtenders);
}
@Test
public void testWhenLastLifetimeExtenderExpiresAllAreReQueried() {
// GIVEN a couple notifications and a few lifetime extenders
mExtender2.shouldExtendLifetime = true;
mCollection.addNotificationLifetimeExtender(mExtender1);
mCollection.addNotificationLifetimeExtender(mExtender2);
mCollection.addNotificationLifetimeExtender(mExtender3);
NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
// GIVEN a notification gets lifetime-extended by one of them
mNoMan.retractNotif(notif2.sbn, REASON_APP_CANCEL);
assertTrue(mCollection.getAllNotifs().contains(entry2));
clearInvocations(mExtender1, mExtender2, mExtender3);
// WHEN the last active extender expires (but new ones become active)
mExtender1.shouldExtendLifetime = true;
mExtender2.shouldExtendLifetime = false;
mExtender3.shouldExtendLifetime = true;
mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2);
// THEN each extender is re-queried
verify(mExtender1).maybeExtendLifetime(entry2, REASON_APP_CANCEL);
verify(mExtender2).maybeExtendLifetime(entry2, REASON_APP_CANCEL);
verify(mExtender3).maybeExtendLifetime(entry2, REASON_APP_CANCEL);
// THEN the entry is not removed
assertTrue(mCollection.getAllNotifs().contains(entry2));
// THEN the entry properly records all extenders that returned true
assertEquals(Arrays.asList(mExtender1, mExtender3), entry2.mLifetimeExtenders);
}
@Test
public void testExtendersAreNotReQueriedUntilFinalActiveExtenderExpires() {
// GIVEN a couple notifications and a few lifetime extenders
mExtender1.shouldExtendLifetime = true;
mExtender2.shouldExtendLifetime = true;
mCollection.addNotificationLifetimeExtender(mExtender1);
mCollection.addNotificationLifetimeExtender(mExtender2);
mCollection.addNotificationLifetimeExtender(mExtender3);
NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
// GIVEN a notification gets lifetime-extended by a couple of them
mNoMan.retractNotif(notif2.sbn, REASON_APP_CANCEL);
assertTrue(mCollection.getAllNotifs().contains(entry2));
clearInvocations(mExtender1, mExtender2, mExtender3);
// WHEN one (but not all) of the extenders expires
mExtender2.shouldExtendLifetime = false;
mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2);
// THEN the entry is not removed
assertTrue(mCollection.getAllNotifs().contains(entry2));
// THEN we don't re-query the extenders
verify(mExtender1, never()).maybeExtendLifetime(entry2, REASON_APP_CANCEL);
verify(mExtender2, never()).maybeExtendLifetime(entry2, REASON_APP_CANCEL);
verify(mExtender3, never()).maybeExtendLifetime(entry2, REASON_APP_CANCEL);
// THEN the entry properly records all extenders that returned true
assertEquals(singletonList(mExtender1), entry2.mLifetimeExtenders);
}
@Test
public void testNotificationIsRemovedWhenAllLifetimeExtendersExpire() {
// GIVEN a couple notifications and a few lifetime extenders
mExtender1.shouldExtendLifetime = true;
mExtender2.shouldExtendLifetime = true;
mCollection.addNotificationLifetimeExtender(mExtender1);
mCollection.addNotificationLifetimeExtender(mExtender2);
mCollection.addNotificationLifetimeExtender(mExtender3);
NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
// GIVEN a notification gets lifetime-extended by a couple of them
mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
assertTrue(mCollection.getAllNotifs().contains(entry2));
clearInvocations(mExtender1, mExtender2, mExtender3);
// WHEN all of the active extenders expire
mExtender2.shouldExtendLifetime = false;
mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2);
mExtender1.shouldExtendLifetime = false;
mExtender1.callback.onEndLifetimeExtension(mExtender1, entry2);
// THEN the entry removed
assertFalse(mCollection.getAllNotifs().contains(entry2));
verify(mCollectionListener).onEntryRemoved(entry2, REASON_UNKNOWN);
}
@Test
public void testLifetimeExtensionIsCanceledWhenNotifIsUpdated() {
// GIVEN a few lifetime extenders and a couple notifications
mCollection.addNotificationLifetimeExtender(mExtender1);
mCollection.addNotificationLifetimeExtender(mExtender2);
mCollection.addNotificationLifetimeExtender(mExtender3);
mExtender1.shouldExtendLifetime = true;
mExtender2.shouldExtendLifetime = true;
NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
// GIVEN a notification gets lifetime-extended by a couple of them
mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
assertTrue(mCollection.getAllNotifs().contains(entry2));
clearInvocations(mExtender1, mExtender2, mExtender3);
// WHEN the notification is reposted
mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
// THEN all of the active lifetime extenders are canceled
verify(mExtender1).cancelLifetimeExtension(entry2);
verify(mExtender2).cancelLifetimeExtension(entry2);
// THEN the notification is still present
assertTrue(mCollection.getAllNotifs().contains(entry2));
}
@Test(expected = IllegalStateException.class)
public void testReentrantCallsToLifetimeExtendersThrow() {
// GIVEN a few lifetime extenders and a couple notifications
mCollection.addNotificationLifetimeExtender(mExtender1);
mCollection.addNotificationLifetimeExtender(mExtender2);
mCollection.addNotificationLifetimeExtender(mExtender3);
mExtender1.shouldExtendLifetime = true;
mExtender2.shouldExtendLifetime = true;
NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
// GIVEN a notification gets lifetime-extended by a couple of them
mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
assertTrue(mCollection.getAllNotifs().contains(entry2));
clearInvocations(mExtender1, mExtender2, mExtender3);
// WHEN a lifetime extender makes a reentrant call during cancelLifetimeExtension()
mExtender2.onCancelLifetimeExtension = () -> {
mExtender2.callback.onEndLifetimeExtension(mExtender2, entry2);
};
// This triggers the call to cancelLifetimeExtension()
mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
// THEN an exception is thrown
}
@Test
public void testRankingIsUpdatedWhenALifetimeExtendedNotifIsReposted() {
// GIVEN a few lifetime extenders and a couple notifications
mCollection.addNotificationLifetimeExtender(mExtender1);
mCollection.addNotificationLifetimeExtender(mExtender2);
mCollection.addNotificationLifetimeExtender(mExtender3);
mExtender1.shouldExtendLifetime = true;
mExtender2.shouldExtendLifetime = true;
NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47));
NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88));
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
// GIVEN a notification gets lifetime-extended by a couple of them
mNoMan.retractNotif(notif2.sbn, REASON_UNKNOWN);
assertTrue(mCollection.getAllNotifs().contains(entry2));
clearInvocations(mExtender1, mExtender2, mExtender3);
// WHEN the notification is reposted
NotifEvent notif2a = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88)
.setRank(4747)
.setExplanation("Some new explanation"));
// THEN the notification's ranking is properly updated
assertEquals(notif2a.ranking, entry2.getRanking());
}
@Test
public void testCancellationReasonIsSetWhenNotifIsCancelled() {
// GIVEN a notification
NotifEvent notif0 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3));
NotificationEntry entry0 = mCollectionListener.getEntry(notif0.key);
// WHEN the notification is retracted
mNoMan.retractNotif(notif0.sbn, REASON_APP_CANCEL);
// THEN the retraction reason is stored on the notif
assertEquals(REASON_APP_CANCEL, entry0.mCancellationReason);
}
@Test
public void testCancellationReasonIsClearedWhenNotifIsUpdated() {
// GIVEN a notification and a lifetime extender that will preserve it
NotifEvent notif0 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3));
NotificationEntry entry0 = mCollectionListener.getEntry(notif0.key);
mCollection.addNotificationLifetimeExtender(mExtender1);
mExtender1.shouldExtendLifetime = true;
// WHEN the notification is retracted and subsequently reposted
mNoMan.retractNotif(notif0.sbn, REASON_APP_CANCEL);
assertEquals(REASON_APP_CANCEL, entry0.mCancellationReason);
mNoMan.postNotif(buildNotif(TEST_PACKAGE, 3));
// THEN the notification has its cancellation reason cleared
assertEquals(REASON_NOT_CANCELED, entry0.mCancellationReason);
}
@Test
public void testDismissNotificationsRebuildsOnce() {
// GIVEN a collection with a couple notifications
NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag"));
NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
clearInvocations(mBuildListener);
// WHEN both notifications are manually dismissed together
mCollection.dismissNotifications(
List.of(new Pair<>(entry1, defaultStats(entry1)),
new Pair<>(entry2, defaultStats(entry2))));
// THEN build list is only called one time
verifyBuiltList(List.of(entry1, entry2));
}
@Test
public void testDismissNotificationsSentToSystemServer() throws RemoteException {
// GIVEN a collection with a couple notifications
NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag"));
NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
// WHEN both notifications are manually dismissed together
DismissedByUserStats stats1 = defaultStats(entry1);
DismissedByUserStats stats2 = defaultStats(entry2);
mCollection.dismissNotifications(
List.of(new Pair<>(entry1, defaultStats(entry1)),
new Pair<>(entry2, defaultStats(entry2))));
// THEN we send the dismissals to system server
FakeExecutor.exhaustExecutors(mBgExecutor);
verify(mStatusBarService).onNotificationClear(
notif1.sbn.getPackageName(),
notif1.sbn.getUser().getIdentifier(),
notif1.sbn.getKey(),
stats1.dismissalSurface,
stats1.dismissalSentiment,
stats1.notificationVisibility);
verify(mStatusBarService).onNotificationClear(
notif2.sbn.getPackageName(),
notif2.sbn.getUser().getIdentifier(),
notif2.sbn.getKey(),
stats2.dismissalSurface,
stats2.dismissalSentiment,
stats2.notificationVisibility);
}
@Test
public void testDismissNotificationsMarkedAsDismissed() {
// GIVEN a collection with a couple notifications
NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag"));
NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
// WHEN both notifications are manually dismissed together
mCollection.dismissNotifications(
List.of(new Pair<>(entry1, defaultStats(entry1)),
new Pair<>(entry2, defaultStats(entry2))));
// THEN the entries are marked as dismissed
assertEquals(DISMISSED, entry1.getDismissState());
assertEquals(DISMISSED, entry2.getDismissState());
}
@Test
public void testDismissNotificationssCallsDismissInterceptors() {
// GIVEN a collection with notifications with multiple dismiss interceptors
mInterceptor1.shouldInterceptDismissal = true;
mInterceptor2.shouldInterceptDismissal = true;
mInterceptor3.shouldInterceptDismissal = false;
mCollection.addNotificationDismissInterceptor(mInterceptor1);
mCollection.addNotificationDismissInterceptor(mInterceptor2);
mCollection.addNotificationDismissInterceptor(mInterceptor3);
NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag"));
NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
// WHEN both notifications are manually dismissed together
mCollection.dismissNotifications(
List.of(new Pair<>(entry1, defaultStats(entry1)),
new Pair<>(entry2, defaultStats(entry2))));
// THEN all interceptors get checked
verify(mInterceptor1).shouldInterceptDismissal(entry1);
verify(mInterceptor2).shouldInterceptDismissal(entry1);
verify(mInterceptor3).shouldInterceptDismissal(entry1);
verify(mInterceptor1).shouldInterceptDismissal(entry2);
verify(mInterceptor2).shouldInterceptDismissal(entry2);
verify(mInterceptor3).shouldInterceptDismissal(entry2);
assertEquals(List.of(mInterceptor1, mInterceptor2), entry1.mDismissInterceptors);
assertEquals(List.of(mInterceptor1, mInterceptor2), entry2.mDismissInterceptors);
}
@Test
public void testDismissAllNotificationsCallsRebuildOnce() {
// GIVEN a collection with a couple notifications
NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag"));
NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
clearInvocations(mBuildListener);
// WHEN all notifications are dismissed for the user who posted both notifs
mCollection.dismissAllNotifications(entry1.getSbn().getUser().getIdentifier());
// THEN build list is only called one time
verifyBuiltList(List.of(entry1, entry2));
}
@Test
public void testDismissAllNotificationsSentToSystemServer() throws RemoteException {
// GIVEN a collection with a couple notifications
NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag"));
NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
// WHEN all notifications are dismissed for the user who posted both notifs
mCollection.dismissAllNotifications(entry1.getSbn().getUser().getIdentifier());
// THEN we send the dismissal to system server
verify(mStatusBarService).onClearAllNotifications(
entry1.getSbn().getUser().getIdentifier());
}
@Test
public void testDismissAllNotificationsMarkedAsDismissed() {
// GIVEN a collection with a couple notifications
NotifEvent notif1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag"));
NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
// WHEN all notifications are dismissed for the user who posted both notifs
mCollection.dismissAllNotifications(entry1.getSbn().getUser().getIdentifier());
// THEN the entries are marked as dismissed
assertEquals(DISMISSED, entry1.getDismissState());
assertEquals(DISMISSED, entry2.getDismissState());
}
@Test
public void testDismissAllNotificationsDoesNotMarkDismissedUnclearableNotifs() {
// GIVEN a collection with one unclearable notification and one clearable notification
NotificationEntryBuilder notifEntryBuilder = buildNotif(TEST_PACKAGE, 47, "myTag");
notifEntryBuilder.modifyNotification(mContext)
.setFlag(FLAG_NO_CLEAR, true);
NotifEvent unclearabeNotif = mNoMan.postNotif(notifEntryBuilder);
NotifEvent notif2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE2, 88, "barTag"));
NotificationEntry unclearableEntry = mCollectionListener.getEntry(unclearabeNotif.key);
NotificationEntry entry2 = mCollectionListener.getEntry(notif2.key);
// WHEN all notifications are dismissed for the user who posted both notifs
mCollection.dismissAllNotifications(unclearableEntry.getSbn().getUser().getIdentifier());
// THEN only the clearable entry is marked as dismissed
assertEquals(NOT_DISMISSED, unclearableEntry.getDismissState());
assertEquals(DISMISSED, entry2.getDismissState());
}
@Test
public void testDismissAllNotificationsCallsDismissInterceptorsOnlyOnUnclearableNotifs() {
// GIVEN a collection with multiple dismiss interceptors
mInterceptor1.shouldInterceptDismissal = true;
mInterceptor2.shouldInterceptDismissal = true;
mInterceptor3.shouldInterceptDismissal = false;
mCollection.addNotificationDismissInterceptor(mInterceptor1);
mCollection.addNotificationDismissInterceptor(mInterceptor2);
mCollection.addNotificationDismissInterceptor(mInterceptor3);
// GIVEN a collection with one unclearable and one clearable notification
NotifEvent unclearableNotif = mNoMan.postNotif(
buildNotif(TEST_PACKAGE, 47, "myTag")
.setFlag(mContext, FLAG_NO_CLEAR, true));
NotificationEntry unclearable = mCollectionListener.getEntry(unclearableNotif.key);
NotifEvent clearableNotif = mNoMan.postNotif(
buildNotif(TEST_PACKAGE, 88, "myTag")
.setFlag(mContext, FLAG_NO_CLEAR, false));
NotificationEntry clearable = mCollectionListener.getEntry(clearableNotif.key);
// WHEN all notifications are dismissed for the user who posted the notif
mCollection.dismissAllNotifications(clearable.getSbn().getUser().getIdentifier());
// THEN all interceptors get checked for the unclearable notification
verify(mInterceptor1).shouldInterceptDismissal(unclearable);
verify(mInterceptor2).shouldInterceptDismissal(unclearable);
verify(mInterceptor3).shouldInterceptDismissal(unclearable);
assertEquals(List.of(mInterceptor1, mInterceptor2), unclearable.mDismissInterceptors);
// THEN no interceptors get checked for the clearable notification
verify(mInterceptor1, never()).shouldInterceptDismissal(clearable);
verify(mInterceptor2, never()).shouldInterceptDismissal(clearable);
verify(mInterceptor3, never()).shouldInterceptDismissal(clearable);
}
@Test
public void testClearNotificationDoesntThrowIfMissing() {
// GIVEN that enough time has passed that we're beyond the forgiveness window
mClock.advanceTime(5001);
// WHEN we get a remove event for a notification we don't know about
final NotificationEntry container = new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE)
.setId(47)
.build();
mNotifHandler.onNotificationRemoved(
container.getSbn(),
new RankingMap(new Ranking[]{ container.getRanking() }));
// THEN the event is ignored
verify(mCollectionListener, never()).onEntryRemoved(any(NotificationEntry.class), anyInt());
}
@Test
public void testClearNotificationDoesntThrowIfInForgivenessWindow() {
// GIVEN that some time has passed but we're still within the initialization forgiveness
// window
mClock.advanceTime(4999);
// WHEN we get a remove event for a notification we don't know about
final NotificationEntry container = new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE)
.setId(47)
.build();
mNotifHandler.onNotificationRemoved(
container.getSbn(),
new RankingMap(new Ranking[]{ container.getRanking() }));
// THEN no exception is thrown, but no event is fired
verify(mCollectionListener, never()).onEntryRemoved(any(NotificationEntry.class), anyInt());
}
private Runnable getInternalNotifUpdateRunnable(StatusBarNotification sbn) {
InternalNotifUpdater updater = mCollection.getInternalNotifUpdater("Test");
updater.onInternalNotificationUpdate(sbn, "reason");
ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
verify(mMainHandler).post(runnableCaptor.capture());
return runnableCaptor.getValue();
}
@Test
public void testGetInternalNotifUpdaterPostsToMainHandler() {
InternalNotifUpdater updater = mCollection.getInternalNotifUpdater("Test");
updater.onInternalNotificationUpdate(mock(StatusBarNotification.class), "reason");
verify(mMainHandler).post(any());
}
@Test
public void testSecondPostCallsUpdateWithTrue() {
// GIVEN a pipeline with one notification
NotifEvent notifEvent = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
NotificationEntry entry = mCollectionListener.getEntry(notifEvent.key);
// KNOWING that it already called listener methods once
verify(mCollectionListener).onEntryAdded(eq(entry));
verify(mCollectionListener).onRankingApplied();
// WHEN we update the notification via the system
mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
// THEN entry updated gets called, added does not, and ranking is called again
verify(mCollectionListener).onEntryUpdated(eq(entry));
verify(mCollectionListener).onEntryUpdated(eq(entry), eq(true));
verify(mCollectionListener).onEntryAdded((entry));
verify(mCollectionListener, times(2)).onRankingApplied();
}
@Test
public void testInternalNotifUpdaterCallsUpdate() {
// GIVEN a pipeline with one notification
NotifEvent notifEvent = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
NotificationEntry entry = mCollectionListener.getEntry(notifEvent.key);
// KNOWING that it will call listener methods once
verify(mCollectionListener).onEntryAdded(eq(entry));
verify(mCollectionListener).onRankingApplied();
// WHEN we update that notification internally
StatusBarNotification sbn = notifEvent.sbn;
getInternalNotifUpdateRunnable(sbn).run();
// THEN only entry updated gets called a second time
verify(mCollectionListener).onEntryAdded(eq(entry));
verify(mCollectionListener).onRankingApplied();
verify(mCollectionListener).onEntryUpdated(eq(entry));
verify(mCollectionListener).onEntryUpdated(eq(entry), eq(false));
}
@Test
public void testInternalNotifUpdaterIgnoresNew() {
// GIVEN a pipeline without any notifications
StatusBarNotification sbn = buildNotif(TEST_PACKAGE, 47, "myTag").build().getSbn();
// WHEN we internally update an unknown notification
getInternalNotifUpdateRunnable(sbn).run();
// THEN only entry updated gets called a second time
verify(mCollectionListener, never()).onEntryAdded(any());
verify(mCollectionListener, never()).onRankingUpdate(any());
verify(mCollectionListener, never()).onRankingApplied();
verify(mCollectionListener, never()).onEntryUpdated(any());
verify(mCollectionListener, never()).onEntryUpdated(any(), anyBoolean());
}
@Test
public void testMissingRankingWhenRemovalFeatureIsDisabled() {
// GIVEN a pipeline with one two notifications
when(mNotifPipelineFlags.removeUnrankedNotifs()).thenReturn(false);
String key1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 1, "myTag")).key;
String key2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2, "myTag")).key;
NotificationEntry entry1 = mCollectionListener.getEntry(key1);
NotificationEntry entry2 = mCollectionListener.getEntry(key2);
clearInvocations(mCollectionListener);
// GIVEN the message for removing key1 gets does not reach NotifCollection
Ranking ranking1 = mNoMan.removeRankingWithoutEvent(key1);
// WHEN the message for removing key2 arrives
mNoMan.retractNotif(entry2.getSbn(), REASON_APP_CANCEL);
// THEN only entry2 gets removed
verify(mCollectionListener).onEntryRemoved(eq(entry2), eq(REASON_APP_CANCEL));
verify(mCollectionListener).onEntryCleanUp(eq(entry2));
verify(mCollectionListener).onRankingApplied();
verifyNoMoreInteractions(mCollectionListener);
verify(mLogger).logMissingRankings(eq(List.of(entry1)), eq(1), any());
verify(mLogger, never()).logRecoveredRankings(any());
clearInvocations(mCollectionListener, mLogger);
// WHEN a ranking update includes key1 again
mNoMan.setRanking(key1, ranking1);
mNoMan.issueRankingUpdate();
// VERIFY that we do nothing but log the 'recovery'
verify(mCollectionListener).onRankingUpdate(any());
verify(mCollectionListener).onRankingApplied();
verifyNoMoreInteractions(mCollectionListener);
verify(mLogger, never()).logMissingRankings(any(), anyInt(), any());
verify(mLogger).logRecoveredRankings(eq(List.of(key1)));
}
@Test
public void testMissingRankingWhenRemovalFeatureIsEnabled() {
// GIVEN a pipeline with one two notifications
when(mNotifPipelineFlags.removeUnrankedNotifs()).thenReturn(true);
String key1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 1, "myTag")).key;
String key2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2, "myTag")).key;
NotificationEntry entry1 = mCollectionListener.getEntry(key1);
NotificationEntry entry2 = mCollectionListener.getEntry(key2);
clearInvocations(mCollectionListener);
// GIVEN the message for removing key1 gets does not reach NotifCollection
Ranking ranking1 = mNoMan.removeRankingWithoutEvent(key1);
// WHEN the message for removing key2 arrives
mNoMan.retractNotif(entry2.getSbn(), REASON_APP_CANCEL);
// THEN both entry1 and entry2 get removed
verify(mCollectionListener).onEntryRemoved(eq(entry2), eq(REASON_APP_CANCEL));
verify(mCollectionListener).onEntryRemoved(eq(entry1), eq(REASON_UNKNOWN));
verify(mCollectionListener).onEntryCleanUp(eq(entry2));
verify(mCollectionListener).onEntryCleanUp(eq(entry1));
verify(mCollectionListener).onRankingApplied();
verifyNoMoreInteractions(mCollectionListener);
verify(mLogger).logMissingRankings(eq(List.of(entry1)), eq(1), any());
verify(mLogger, never()).logRecoveredRankings(any());
clearInvocations(mCollectionListener, mLogger);
// WHEN a ranking update includes key1 again
mNoMan.setRanking(key1, ranking1);
mNoMan.issueRankingUpdate();
// VERIFY that we do nothing but log the 'recovery'
verify(mCollectionListener).onRankingUpdate(any());
verify(mCollectionListener).onRankingApplied();
verifyNoMoreInteractions(mCollectionListener);
verify(mLogger, never()).logMissingRankings(any(), anyInt(), any());
verify(mLogger).logRecoveredRankings(eq(List.of(key1)));
}
@Test
public void testRegisterFutureDismissal() throws RemoteException {
// GIVEN a pipeline with one notification
NotifEvent notifEvent = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
NotificationEntry entry = requireNonNull(mCollection.getEntry(notifEvent.key));
clearInvocations(mCollectionListener);
// WHEN registering a future dismissal, nothing happens right away
final Runnable onDismiss = mCollection.registerFutureDismissal(entry, REASON_CLICK,
NotifCollectionTest::defaultStats);
verifyNoMoreInteractions(mCollectionListener);
// WHEN finally dismissing
onDismiss.run();
FakeExecutor.exhaustExecutors(mBgExecutor);
verify(mStatusBarService).onNotificationClear(any(), anyInt(), eq(notifEvent.key),
anyInt(), anyInt(), any());
verifyNoMoreInteractions(mStatusBarService);
verifyNoMoreInteractions(mCollectionListener);
}
@Test
public void testRegisterFutureDismissalWithRetractionAndRepost() {
// GIVEN a pipeline with one notification
NotifEvent notifEvent = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
NotificationEntry entry = requireNonNull(mCollection.getEntry(notifEvent.key));
clearInvocations(mCollectionListener);
// WHEN registering a future dismissal, nothing happens right away
final Runnable onDismiss = mCollection.registerFutureDismissal(entry, REASON_CLICK,
NotifCollectionTest::defaultStats);
verifyNoMoreInteractions(mCollectionListener);
// WHEN retracting the notification, and then reposting
mNoMan.retractNotif(notifEvent.sbn, REASON_CLICK);
mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
clearInvocations(mCollectionListener);
// KNOWING that the entry in the collection is different now
assertThat(mCollection.getEntry(notifEvent.key)).isNotSameInstanceAs(entry);
// WHEN finally dismissing
onDismiss.run();
// VERIFY that nothing happens; the notification should not be removed
verifyNoMoreInteractions(mCollectionListener);
assertThat(mCollection.getEntry(notifEvent.key)).isNotNull();
verifyNoMoreInteractions(mStatusBarService);
}
@Test
public void testCannotDismissOngoingNotificationChildren() {
// GIVEN an ongoing notification
final NotificationEntry container = new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE)
.setId(47)
.setGroup(mContext, "group")
.setFlag(mContext, FLAG_ONGOING_EVENT, true)
.build();
// THEN its children are not dismissible
assertFalse(mCollection.shouldAutoDismissChildren(
container, container.getSbn().getGroupKey()));
}
@Test
public void testCannotDismissNoClearNotifications() {
// GIVEN an no-clear notification
final NotificationEntry container = new NotificationEntryBuilder()
.setFlag(mContext, FLAG_NO_CLEAR, true)
.build();
// THEN its children are not dismissible
assertFalse(mCollection.shouldAutoDismissChildren(
container, container.getSbn().getGroupKey()));
}
@Test
public void testCanDismissFgsNotificationChildren() {
// GIVEN an FGS but not ongoing notification
final NotificationEntry container = new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE)
.setId(47)
.setGroup(mContext, "group")
.setFlag(mContext, FLAG_FOREGROUND_SERVICE, true)
.build();
container.setDismissState(NOT_DISMISSED);
// THEN its children are dismissible
assertTrue(mCollection.shouldAutoDismissChildren(
container, container.getSbn().getGroupKey()));
}
private static NotificationEntryBuilder buildNotif(String pkg, int id, String tag) {
return new NotificationEntryBuilder()
.setPkg(pkg)
.setId(id)
.setTag(tag);
}
private static NotificationEntryBuilder buildNotif(String pkg, int id) {
return new NotificationEntryBuilder()
.setPkg(pkg)
.setId(id);
}
private static DismissedByUserStats defaultStats(NotificationEntry entry) {
return new DismissedByUserStats(
DISMISSAL_SHADE,
DISMISS_SENTIMENT_NEUTRAL,
NotificationVisibility.obtain(entry.getKey(), 7, 2, true));
}
private CollectionEvent postNotif(NotificationEntryBuilder builder) {
clearInvocations(mCollectionListener);
NotifEvent rawEvent = mNoMan.postNotif(builder);
verify(mCollectionListener).onEntryAdded(mEntryCaptor.capture());
return new CollectionEvent(rawEvent, requireNonNull(mEntryCaptor.getValue()));
}
private void verifyBuiltList(Collection<NotificationEntry> expectedList) {
verify(mBuildListener).onBuildList(mBuildListCaptor.capture(), any());
assertThat(mBuildListCaptor.getValue()).containsExactly(expectedList.toArray());
}
private static class RecordingCollectionListener implements NotifCollectionListener {
private final Map<String, NotificationEntry> mLastSeenEntries = new ArrayMap<>();
@Override
public void onEntryInit(NotificationEntry entry) {
}
@Override
public void onEntryAdded(NotificationEntry entry) {
mLastSeenEntries.put(entry.getKey(), entry);
}
@Override
public void onEntryUpdated(NotificationEntry entry) {
mLastSeenEntries.put(entry.getKey(), entry);
}
@Override
public void onEntryUpdated(NotificationEntry entry, boolean fromSystem) {
onEntryUpdated(entry);
}
@Override
public void onEntryRemoved(NotificationEntry entry, int reason) {
}
@Override
public void onEntryCleanUp(NotificationEntry entry) {
}
@Override
public void onRankingApplied() {
}
@Override
public void onRankingUpdate(RankingMap rankingMap) {
}
public NotificationEntry getEntry(String key) {
if (!mLastSeenEntries.containsKey(key)) {
throw new RuntimeException("Key not found: " + key);
}
return mLastSeenEntries.get(key);
}
}
private static class RecordingLifetimeExtender implements NotifLifetimeExtender {
private final String mName;
public @Nullable OnEndLifetimeExtensionCallback callback;
public boolean shouldExtendLifetime = false;
public @Nullable Runnable onCancelLifetimeExtension;
private RecordingLifetimeExtender(String name) {
mName = name;
}
@NonNull
@Override
public String getName() {
return mName;
}
@Override
public void setCallback(@NonNull OnEndLifetimeExtensionCallback callback) {
this.callback = callback;
}
@Override
public boolean maybeExtendLifetime(
@NonNull NotificationEntry entry,
@CancellationReason int reason) {
return shouldExtendLifetime;
}
@Override
public void cancelLifetimeExtension(@NonNull NotificationEntry entry) {
if (onCancelLifetimeExtension != null) {
onCancelLifetimeExtension.run();
}
}
}
private static class RecordingDismissInterceptor implements NotifDismissInterceptor {
private final String mName;
public @Nullable OnEndDismissInterception onEndInterceptionCallback;
public boolean shouldInterceptDismissal = false;
private RecordingDismissInterceptor(String name) {
mName = name;
}
@Override
public String getName() {
return mName;
}
@Override
public void setCallback(OnEndDismissInterception callback) {
this.onEndInterceptionCallback = callback;
}
@Override
public boolean shouldInterceptDismissal(NotificationEntry entry) {
return shouldInterceptDismissal;
}
@Override
public void cancelDismissInterception(NotificationEntry entry) {
}
}
/**
* Wrapper around {@link NotifEvent} that adds the NotificationEntry that the collection under
* test creates.
*/
private static class CollectionEvent {
public final String key;
public final StatusBarNotification sbn;
public final Ranking ranking;
public final RankingMap rankingMap;
public final NotificationEntry entry;
private CollectionEvent(NotifEvent rawEvent, NotificationEntry entry) {
this.key = rawEvent.key;
this.sbn = rawEvent.sbn;
this.ranking = rawEvent.ranking;
this.rankingMap = rawEvent.rankingMap;
this.entry = entry;
}
}
private static final String TEST_PACKAGE = "com.android.test.collection";
private static final String TEST_PACKAGE2 = "com.android.test.collection2";
private static final String GROUP_1 = "group_1";
}