blob: ac9a57022f5360595bb1c18475ad6cf48b9a8b75 [file] [log] [blame]
/*
* Copyright (C) 2020 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.coalescer;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static java.util.Objects.requireNonNull;
import android.service.notification.NotificationListenerService.Ranking;
import android.service.notification.NotificationListenerService.RankingMap;
import android.service.notification.StatusBarNotification;
import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.NotificationListener;
import com.android.systemui.statusbar.NotificationListener.NotificationHandler;
import com.android.systemui.statusbar.RankingBuilder;
import com.android.systemui.statusbar.notification.collection.NoManSimulator;
import com.android.systemui.statusbar.notification.collection.NoManSimulator.NotifEvent;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
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 java.util.Arrays;
import java.util.Collections;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class GroupCoalescerTest extends SysuiTestCase {
private GroupCoalescer mCoalescer;
@Mock private NotificationListener mListenerService;
@Mock private GroupCoalescer.BatchableNotificationHandler mListener;
@Mock private GroupCoalescerLogger mLogger;
@Captor private ArgumentCaptor<NotificationHandler> mListenerCaptor;
private final NoManSimulator mNoMan = new NoManSimulator();
private final FakeSystemClock mClock = new FakeSystemClock();
private final FakeExecutor mExecutor = new FakeExecutor(mClock);
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mCoalescer =
new GroupCoalescer(
mExecutor,
mClock,
mLogger,
MIN_LINGER_DURATION,
MAX_LINGER_DURATION);
mCoalescer.setNotificationHandler(mListener);
mCoalescer.attach(mListenerService);
verify(mListenerService).addNotificationHandler(mListenerCaptor.capture());
NotificationHandler serviceListener = requireNonNull(mListenerCaptor.getValue());
mNoMan.addListener(serviceListener);
}
@Test
public void testUngroupedNotificationsAreNotCoalesced() {
// WHEN a notification that doesn't have a group key is posted
NotifEvent notif1 = mNoMan.postNotif(
new NotificationEntryBuilder()
.setId(0)
.setPkg(TEST_PACKAGE_A));
mClock.advanceTime(MIN_LINGER_DURATION);
// THEN the event is passed through to the handler
verify(mListener).onNotificationPosted(notif1.sbn, notif1.rankingMap);
// Then the event isn't emitted in a batch
verify(mListener, never()).onNotificationBatchPosted(anyList());
}
@Test
public void testGroupedNotificationsAreCoalesced() {
// WHEN a notification that has a group key is posted
NotifEvent notif1 = mNoMan.postNotif(
new NotificationEntryBuilder()
.setId(0)
.setPkg(TEST_PACKAGE_A)
.setGroup(mContext, GROUP_1));
// THEN the event is not passed on to the handler
verify(mListener, never()).onNotificationPosted(
any(StatusBarNotification.class),
any(RankingMap.class));
// Then the event isn't (yet) emitted in a batch
verify(mListener, never()).onNotificationBatchPosted(anyList());
}
@Test
public void testCoalescedNotificationsStillPassThroughRankingUpdate() {
// WHEN a notification that has a group key is posted
NotifEvent notif1 = mNoMan.postNotif(
new NotificationEntryBuilder()
.setId(0)
.setPkg(TEST_PACKAGE_A)
.setGroup(mContext, GROUP_1));
// THEN the listener receives a ranking update instead of an add
verify(mListener).onNotificationRankingUpdate(notif1.rankingMap);
}
@Test
public void testCoalescedNotificationsArePosted() {
// GIVEN three notifs are posted that are part of the same group
NotifEvent notif1 = mNoMan.postNotif(new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_A)
.setId(1)
.setGroup(mContext, GROUP_1));
mClock.advanceTime(2);
NotifEvent notif2 = mNoMan.postNotif(new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_A)
.setId(2)
.setGroup(mContext, GROUP_1)
.setGroupSummary(mContext, true));
mClock.advanceTime(3);
NotifEvent notif3 = mNoMan.postNotif(new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_A)
.setId(3)
.setGroup(mContext, GROUP_1));
verify(mListener, never()).onNotificationPosted(
any(StatusBarNotification.class),
any(RankingMap.class));
verify(mListener, never()).onNotificationBatchPosted(anyList());
// WHEN enough time passes
mClock.advanceTime(MIN_LINGER_DURATION);
// THEN the coalesced notifs are applied. The summary is sorted to the front.
verify(mListener).onNotificationBatchPosted(Arrays.asList(
new CoalescedEvent(notif2.key, 1, notif2.sbn, notif2.ranking, null),
new CoalescedEvent(notif1.key, 0, notif1.sbn, notif1.ranking, null),
new CoalescedEvent(notif3.key, 2, notif3.sbn, notif3.ranking, null)
));
}
@Test
public void testCoalescedEventsThatAreLaterUngroupedAreEmittedImmediatelyAndNotLater() {
// GIVEN a few newly posted notifications in the same group
NotifEvent notif1a = mNoMan.postNotif(new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_A)
.setId(1)
.setContentTitle(mContext, "Grouped message")
.setGroup(mContext, GROUP_1));
NotifEvent notif2 = mNoMan.postNotif(new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_A)
.setId(2)
.setGroup(mContext, GROUP_1));
NotifEvent notif3 = mNoMan.postNotif(new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_A)
.setId(3)
.setGroup(mContext, GROUP_1));
verify(mListener, never()).onNotificationPosted(
any(StatusBarNotification.class),
any(RankingMap.class));
verify(mListener, never()).onNotificationBatchPosted(anyList());
// WHEN one of them is updated to no longer be in the group
NotifEvent notif1b = mNoMan.postNotif(new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_A)
.setId(1)
.setContentTitle(mContext, "Oops no longer grouped"));
// THEN the pre-existing batch is first emitted
InOrder inOrder = inOrder(mListener);
inOrder.verify(mListener).onNotificationBatchPosted(Arrays.asList(
new CoalescedEvent(notif1a.key, 0, notif1a.sbn, notif1a.ranking, null),
new CoalescedEvent(notif2.key, 1, notif2.sbn, notif2.ranking, null),
new CoalescedEvent(notif3.key, 2, notif3.sbn, notif3.ranking, null)
));
// THEN the updated notif is emitted
inOrder.verify(mListener).onNotificationPosted(notif1b.sbn, notif1b.rankingMap);
// WHEN the time runs out on the remainder of the group
clearInvocations(mListener);
mClock.advanceTime(MIN_LINGER_DURATION);
// THEN no lingering batch is applied
verify(mListener, never()).onNotificationBatchPosted(anyList());
}
@Test
public void testUpdatingCoalescedNotifTriggersBatchEmit() {
// GIVEN two grouped, coalesced notifications
NotifEvent notif1 = mNoMan.postNotif(new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_A)
.setId(1)
.setGroup(mContext, GROUP_1));
mClock.advanceTime(2);
NotifEvent notif2a = mNoMan.postNotif(new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_A)
.setId(2)
.setContentTitle(mContext, "Version 1")
.setGroup(mContext, GROUP_1));
mClock.advanceTime(4);
// WHEN one of them gets updated
NotifEvent notif2b = mNoMan.postNotif(new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_A)
.setId(2)
.setContentTitle(mContext, "Version 2")
.setGroup(mContext, GROUP_1));
// THEN first, the coalesced group is emitted
verify(mListener).onNotificationBatchPosted(Arrays.asList(
new CoalescedEvent(notif1.key, 0, notif1.sbn, notif1.ranking, null),
new CoalescedEvent(notif2a.key, 1, notif2a.sbn, notif2a.ranking, null)
));
verify(mListener, never()).onNotificationPosted(
any(StatusBarNotification.class),
any(RankingMap.class));
// THEN second, the update is emitted
mClock.advanceTime(MIN_LINGER_DURATION);
verify(mListener).onNotificationBatchPosted(Collections.singletonList(
new CoalescedEvent(notif2b.key, 0, notif2b.sbn, notif2b.ranking, null)
));
}
@Test
public void testRemovingCoalescedNotifTriggersBatchEmit() {
// GIVEN two grouped, coalesced notifications
NotifEvent notif1 = mNoMan.postNotif(new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_A)
.setId(1)
.setGroup(mContext, GROUP_1));
NotifEvent notif2a = mNoMan.postNotif(new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_A)
.setId(2)
.setGroup(mContext, GROUP_1));
// WHEN one of them gets retracted
NotifEvent notif2b = mNoMan.retractNotif(notif2a.sbn, 0);
// THEN first, the coalesced group is emitted
InOrder inOrder = inOrder(mListener);
inOrder.verify(mListener).onNotificationBatchPosted(Arrays.asList(
new CoalescedEvent(notif1.key, 0, notif1.sbn, notif1.ranking, null),
new CoalescedEvent(notif2a.key, 1, notif2a.sbn, notif2a.ranking, null)
));
// THEN second, the removal is emitted
inOrder.verify(mListener).onNotificationRemoved(notif2b.sbn, notif2b.rankingMap, 0);
}
@Test
public void testRankingsAreUpdated() {
// GIVEN a couple coalesced notifications
NotifEvent notif1 = mNoMan.postNotif(new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_A)
.setId(1)
.setGroup(mContext, GROUP_1));
NotifEvent notif2 = mNoMan.postNotif(new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_A)
.setId(2)
.setGroup(mContext, GROUP_1));
// WHEN an update to an unrelated notification comes in that updates their rankings
Ranking ranking1b = new RankingBuilder()
.setKey(notif1.key)
.setLastAudiblyAlertedMs(4747)
.build();
Ranking ranking2b = new RankingBuilder()
.setKey(notif2.key)
.setLastAudiblyAlertedMs(3333)
.build();
mNoMan.setRanking(notif1.key, ranking1b);
mNoMan.setRanking(notif2.key, ranking2b);
mNoMan.postNotif(new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_B)
.setId(17));
// THEN they have the new rankings when they are eventually emitted
mClock.advanceTime(MIN_LINGER_DURATION);
verify(mListener).onNotificationBatchPosted(Arrays.asList(
new CoalescedEvent(notif1.key, 0, notif1.sbn, ranking1b, null),
new CoalescedEvent(notif2.key, 1, notif2.sbn, ranking2b, null)
));
}
@Test
public void testMaxLingerDuration() {
// GIVEN five coalesced notifications that have collectively taken 20ms to arrive, 2ms
// longer than the max linger duration
NotifEvent notif1 = mNoMan.postNotif(new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_A)
.setId(1)
.setGroup(mContext, GROUP_1));
mClock.advanceTime(4);
NotifEvent notif2 = mNoMan.postNotif(new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_A)
.setId(2)
.setGroup(mContext, GROUP_1));
mClock.advanceTime(4);
NotifEvent notif3 = mNoMan.postNotif(new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_A)
.setId(3)
.setGroup(mContext, GROUP_1));
mClock.advanceTime(4);
NotifEvent notif4 = mNoMan.postNotif(new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_A)
.setId(4)
.setGroup(mContext, GROUP_1));
mClock.advanceTime(4);
NotifEvent notif5 = mNoMan.postNotif(new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_A)
.setId(5)
.setGroup(mContext, GROUP_1));
mClock.advanceTime(4);
// WHEN a sixth notification arrives
NotifEvent notif6 = mNoMan.postNotif(new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_A)
.setId(6)
.setGroup(mContext, GROUP_1));
// THEN the first five notifications are emitted in a batch
verify(mListener).onNotificationBatchPosted(Arrays.asList(
new CoalescedEvent(notif1.key, 0, notif1.sbn, notif1.ranking, null),
new CoalescedEvent(notif2.key, 1, notif2.sbn, notif2.ranking, null),
new CoalescedEvent(notif3.key, 2, notif3.sbn, notif3.ranking, null),
new CoalescedEvent(notif4.key, 3, notif4.sbn, notif4.ranking, null),
new CoalescedEvent(notif5.key, 4, notif5.sbn, notif5.ranking, null)
));
}
private static final long MIN_LINGER_DURATION = 5;
private static final long MAX_LINGER_DURATION = 18;
private static final String TEST_PACKAGE_A = "com.test.package_a";
private static final String TEST_PACKAGE_B = "com.test.package_b";
private static final String GROUP_1 = "group_1";
}