| /* |
| * 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"; |
| } |