| /* |
| * 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; |
| |
| |
| import static android.app.Notification.FLAG_BUBBLE; |
| import static android.app.NotificationManager.IMPORTANCE_DEFAULT; |
| import static android.app.NotificationManager.IMPORTANCE_HIGH; |
| import static android.app.NotificationManager.IMPORTANCE_LOW; |
| import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT; |
| import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; |
| |
| import static com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking; |
| import static com.android.systemui.statusbar.StatusBarState.SHADE; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| |
| import static org.mockito.ArgumentMatchers.any; |
| import static org.mockito.ArgumentMatchers.anyInt; |
| import static org.mockito.Mockito.verify; |
| import static org.mockito.Mockito.when; |
| |
| import android.app.Notification; |
| import android.app.PendingIntent; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.graphics.drawable.Icon; |
| import android.hardware.display.AmbientDisplayConfiguration; |
| import android.os.PowerManager; |
| import android.os.RemoteException; |
| import android.service.dreams.IDreamManager; |
| import android.testing.AndroidTestingRunner; |
| |
| import androidx.test.filters.SmallTest; |
| |
| import com.android.systemui.R; |
| import com.android.systemui.SysuiTestCase; |
| import com.android.systemui.plugins.statusbar.StatusBarStateController; |
| import com.android.systemui.statusbar.notification.NotificationFilter; |
| import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; |
| import com.android.systemui.statusbar.notification.collection.NotificationEntry; |
| import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; |
| import com.android.systemui.statusbar.policy.BatteryController; |
| import com.android.systemui.statusbar.policy.HeadsUpManager; |
| |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.Mock; |
| import org.mockito.MockitoAnnotations; |
| |
| /** |
| * Tests for the interruption state provider which understands whether the system & notification |
| * is in a state allowing a particular notification to hun, pulse, or bubble. |
| */ |
| @RunWith(AndroidTestingRunner.class) |
| @SmallTest |
| public class NotificationInterruptionStateProviderTest extends SysuiTestCase { |
| |
| @Mock |
| PowerManager mPowerManager; |
| @Mock |
| IDreamManager mDreamManager; |
| @Mock |
| AmbientDisplayConfiguration mAmbientDisplayConfiguration; |
| @Mock |
| NotificationFilter mNotificationFilter; |
| @Mock |
| StatusBarStateController mStatusBarStateController; |
| @Mock |
| NotificationPresenter mPresenter; |
| @Mock |
| HeadsUpManager mHeadsUpManager; |
| @Mock |
| NotificationInterruptionStateProvider.HeadsUpSuppressor mHeadsUpSuppressor; |
| @Mock |
| BatteryController mBatteryController; |
| |
| private NotificationInterruptionStateProvider mNotifInterruptionStateProvider; |
| |
| @Before |
| public void setup() { |
| MockitoAnnotations.initMocks(this); |
| |
| mNotifInterruptionStateProvider = |
| new TestableNotificationInterruptionStateProvider(mContext, |
| mPowerManager, |
| mDreamManager, |
| mAmbientDisplayConfiguration, |
| mNotificationFilter, |
| mStatusBarStateController, |
| mBatteryController); |
| |
| mNotifInterruptionStateProvider.setUpWithPresenter( |
| mPresenter, |
| mHeadsUpManager, |
| mHeadsUpSuppressor); |
| } |
| |
| /** |
| * Sets up the state such that any requests to |
| * {@link NotificationInterruptionStateProvider#canAlertCommon(NotificationEntry)} will |
| * pass as long its provided NotificationEntry fulfills group suppression check. |
| */ |
| private void ensureStateForAlertCommon() { |
| when(mNotificationFilter.shouldFilterOut(any())).thenReturn(false); |
| } |
| |
| /** |
| * Sets up the state such that any requests to |
| * {@link NotificationInterruptionStateProvider#canAlertAwakeCommon(NotificationEntry)} will |
| * pass as long its provided NotificationEntry fulfills launch fullscreen check. |
| */ |
| private void ensureStateForAlertAwakeCommon() { |
| when(mPresenter.isDeviceInVrMode()).thenReturn(false); |
| when(mHeadsUpManager.isSnoozed(any())).thenReturn(false); |
| } |
| |
| /** |
| * Sets up the state such that any requests to |
| * {@link NotificationInterruptionStateProvider#shouldHeadsUp(NotificationEntry)} will |
| * pass as long its provided NotificationEntry fulfills importance & DND checks. |
| */ |
| private void ensureStateForHeadsUpWhenAwake() throws RemoteException { |
| ensureStateForAlertCommon(); |
| ensureStateForAlertAwakeCommon(); |
| |
| when(mStatusBarStateController.isDozing()).thenReturn(false); |
| when(mDreamManager.isDreaming()).thenReturn(false); |
| when(mPowerManager.isScreenOn()).thenReturn(true); |
| when(mHeadsUpSuppressor.canHeadsUp(any(), any())).thenReturn(true); |
| } |
| |
| /** |
| * Sets up the state such that any requests to |
| * {@link NotificationInterruptionStateProvider#shouldHeadsUp(NotificationEntry)} will |
| * pass as long its provided NotificationEntry fulfills importance & DND checks. |
| */ |
| private void ensureStateForHeadsUpWhenDozing() { |
| ensureStateForAlertCommon(); |
| |
| when(mStatusBarStateController.isDozing()).thenReturn(true); |
| when(mAmbientDisplayConfiguration.pulseOnNotificationEnabled(anyInt())).thenReturn(true); |
| } |
| |
| /** |
| * Sets up the state such that any requests to |
| * {@link NotificationInterruptionStateProvider#shouldBubbleUp(NotificationEntry)} will |
| * pass as long its provided NotificationEntry fulfills importance & bubble checks. |
| */ |
| private void ensureStateForBubbleUp() { |
| ensureStateForAlertCommon(); |
| ensureStateForAlertAwakeCommon(); |
| } |
| |
| /** |
| * Ensure that the disabled state is set correctly. |
| */ |
| @Test |
| public void testDisableNotificationAlerts() { |
| // Enabled by default |
| assertThat(mNotifInterruptionStateProvider.areNotificationAlertsDisabled()).isFalse(); |
| |
| // Disable alerts |
| mNotifInterruptionStateProvider.setDisableNotificationAlerts(true); |
| assertThat(mNotifInterruptionStateProvider.areNotificationAlertsDisabled()).isTrue(); |
| |
| // Enable alerts |
| mNotifInterruptionStateProvider.setDisableNotificationAlerts(false); |
| assertThat(mNotifInterruptionStateProvider.areNotificationAlertsDisabled()).isFalse(); |
| } |
| |
| /** |
| * Ensure that the disabled alert state effects whether HUNs are enabled. |
| */ |
| @Test |
| public void testHunSettingsChange_enabled_butAlertsDisabled() { |
| // Set up but without a mock change observer |
| mNotifInterruptionStateProvider.setUpWithPresenter( |
| mPresenter, |
| mHeadsUpManager, |
| mHeadsUpSuppressor); |
| |
| // HUNs enabled by default |
| assertThat(mNotifInterruptionStateProvider.getUseHeadsUp()).isTrue(); |
| |
| // Set alerts disabled |
| mNotifInterruptionStateProvider.setDisableNotificationAlerts(true); |
| |
| // No more HUNs |
| assertThat(mNotifInterruptionStateProvider.getUseHeadsUp()).isFalse(); |
| } |
| |
| /** |
| * Alerts can happen. |
| */ |
| @Test |
| public void testCanAlertCommon_true() { |
| ensureStateForAlertCommon(); |
| |
| NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT); |
| assertThat(mNotifInterruptionStateProvider.canAlertCommon(entry)).isTrue(); |
| } |
| |
| /** |
| * Filtered out notifications don't alert. |
| */ |
| @Test |
| public void testCanAlertCommon_false_filteredOut() { |
| ensureStateForAlertCommon(); |
| when(mNotificationFilter.shouldFilterOut(any())).thenReturn(true); |
| |
| NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT); |
| assertThat(mNotifInterruptionStateProvider.canAlertCommon(entry)).isFalse(); |
| } |
| |
| /** |
| * Grouped notifications have different alerting behaviours, sometimes the alert for a |
| * grouped notification may be suppressed {@link android.app.Notification#GROUP_ALERT_CHILDREN}. |
| */ |
| @Test |
| public void testCanAlertCommon_false_suppressedForGroups() { |
| ensureStateForAlertCommon(); |
| |
| NotificationEntry entry = new NotificationEntryBuilder() |
| .setPkg("a") |
| .setOpPkg("a") |
| .setTag("a") |
| .setNotification(new Notification.Builder(getContext(), "a") |
| .setGroup("a") |
| .setGroupSummary(true) |
| .setGroupAlertBehavior(Notification.GROUP_ALERT_CHILDREN) |
| .build()) |
| .setImportance(IMPORTANCE_DEFAULT) |
| .build(); |
| |
| assertThat(mNotifInterruptionStateProvider.canAlertCommon(entry)).isFalse(); |
| } |
| |
| /** |
| * HUNs while dozing can happen. |
| */ |
| @Test |
| public void testShouldHeadsUpWhenDozing_true() { |
| ensureStateForHeadsUpWhenDozing(); |
| |
| NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT); |
| assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue(); |
| } |
| |
| /** |
| * Ambient display can show HUNs for new notifications, this may be disabled. |
| */ |
| @Test |
| public void testShouldHeadsUpWhenDozing_false_pulseDisabled() { |
| ensureStateForHeadsUpWhenDozing(); |
| when(mAmbientDisplayConfiguration.pulseOnNotificationEnabled(anyInt())).thenReturn(false); |
| |
| NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT); |
| assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); |
| } |
| |
| /** |
| * If the device is not in ambient display or sleeping then we don't HUN. |
| */ |
| @Test |
| public void testShouldHeadsUpWhenDozing_false_notDozing() { |
| ensureStateForHeadsUpWhenDozing(); |
| when(mStatusBarStateController.isDozing()).thenReturn(false); |
| |
| NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT); |
| assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); |
| } |
| |
| /** |
| * In DND ambient effects can be suppressed |
| * {@link android.app.NotificationManager.Policy#SUPPRESSED_EFFECT_AMBIENT}. |
| */ |
| @Test |
| public void testShouldHeadsUpWhenDozing_false_suppressingAmbient() { |
| ensureStateForHeadsUpWhenDozing(); |
| |
| NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT); |
| modifyRanking(entry) |
| .setSuppressedVisualEffects(SUPPRESSED_EFFECT_AMBIENT) |
| .build(); |
| |
| assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); |
| } |
| |
| /** |
| * Notifications that are < {@link android.app.NotificationManager#IMPORTANCE_DEFAULT} don't |
| * get to pulse. |
| */ |
| @Test |
| public void testShouldHeadsUpWhenDozing_false_lessImportant() { |
| ensureStateForHeadsUpWhenDozing(); |
| |
| NotificationEntry entry = createNotification(IMPORTANCE_LOW); |
| assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); |
| } |
| |
| /** |
| * Heads up can happen. |
| */ |
| @Test |
| public void testShouldHeadsUp_true() throws RemoteException { |
| ensureStateForHeadsUpWhenAwake(); |
| |
| NotificationEntry entry = createNotification(IMPORTANCE_HIGH); |
| assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue(); |
| } |
| |
| /** |
| * Heads up notifications can be disabled in general. |
| */ |
| @Test |
| public void testShouldHeadsUp_false_noHunsAllowed() throws RemoteException { |
| ensureStateForHeadsUpWhenAwake(); |
| |
| // Set alerts disabled, this should cause heads up to be false |
| mNotifInterruptionStateProvider.setDisableNotificationAlerts(true); |
| assertThat(mNotifInterruptionStateProvider.getUseHeadsUp()).isFalse(); |
| |
| NotificationEntry entry = createNotification(IMPORTANCE_HIGH); |
| assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); |
| } |
| |
| /** |
| * If the device is dozing, we don't show as heads up. |
| */ |
| @Test |
| public void testShouldHeadsUp_false_dozing() throws RemoteException { |
| ensureStateForHeadsUpWhenAwake(); |
| when(mStatusBarStateController.isDozing()).thenReturn(true); |
| |
| NotificationEntry entry = createNotification(IMPORTANCE_HIGH); |
| assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); |
| } |
| |
| /** |
| * If the notification is a bubble, and the user is not on AOD / lockscreen, then |
| * the bubble is shown rather than the heads up. |
| */ |
| @Test |
| public void testShouldHeadsUp_false_bubble() throws RemoteException { |
| ensureStateForHeadsUpWhenAwake(); |
| |
| // Bubble bit only applies to interruption when we're in the shade |
| when(mStatusBarStateController.getState()).thenReturn(SHADE); |
| |
| assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(createBubble())).isFalse(); |
| } |
| |
| /** |
| * If we're not allowed to alert in general, we shouldn't be shown as heads up. |
| */ |
| @Test |
| public void testShouldHeadsUp_false_alertCommonFalse() throws RemoteException { |
| ensureStateForHeadsUpWhenAwake(); |
| // Make canAlertCommon false by saying it's filtered out |
| when(mNotificationFilter.shouldFilterOut(any())).thenReturn(true); |
| |
| NotificationEntry entry = createNotification(IMPORTANCE_HIGH); |
| assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); |
| } |
| |
| /** |
| * In DND HUN peek effects can be suppressed |
| * {@link android.app.NotificationManager.Policy#SUPPRESSED_EFFECT_PEEK}. |
| */ |
| @Test |
| public void testShouldHeadsUp_false_suppressPeek() throws RemoteException { |
| ensureStateForHeadsUpWhenAwake(); |
| |
| NotificationEntry entry = createNotification(IMPORTANCE_HIGH); |
| modifyRanking(entry) |
| .setSuppressedVisualEffects(SUPPRESSED_EFFECT_PEEK) |
| .build(); |
| |
| assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); |
| } |
| |
| /** |
| * Notifications that are < {@link android.app.NotificationManager#IMPORTANCE_HIGH} don't get |
| * to show as a heads up. |
| */ |
| @Test |
| public void testShouldHeadsUp_false_lessImportant() throws RemoteException { |
| ensureStateForHeadsUpWhenAwake(); |
| |
| NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT); |
| assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); |
| } |
| |
| /** |
| * If the device is not in use then we shouldn't be shown as heads up. |
| */ |
| @Test |
| public void testShouldHeadsUp_false_deviceNotInUse() throws RemoteException { |
| ensureStateForHeadsUpWhenAwake(); |
| NotificationEntry entry = createNotification(IMPORTANCE_HIGH); |
| |
| // Device is not in use if screen is not on |
| when(mPowerManager.isScreenOn()).thenReturn(false); |
| assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); |
| |
| // Also not in use if screen is on but we're showing screen saver / "dreaming" |
| when(mPowerManager.isDeviceIdleMode()).thenReturn(true); |
| when(mDreamManager.isDreaming()).thenReturn(true); |
| assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); |
| } |
| |
| /** |
| * If something wants to suppress this heads up, then it shouldn't be shown as a heads up. |
| */ |
| @Test |
| public void testShouldHeadsUp_false_suppressed() throws RemoteException { |
| ensureStateForHeadsUpWhenAwake(); |
| when(mHeadsUpSuppressor.canHeadsUp(any(), any())).thenReturn(false); |
| |
| NotificationEntry entry = createNotification(IMPORTANCE_HIGH); |
| assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse(); |
| verify(mHeadsUpSuppressor).canHeadsUp(any(), any()); |
| } |
| |
| /** |
| * On screen alerts don't happen when the device is in VR Mode. |
| */ |
| @Test |
| public void testCanAlertAwakeCommon__false_vrMode() { |
| ensureStateForAlertAwakeCommon(); |
| when(mPresenter.isDeviceInVrMode()).thenReturn(true); |
| |
| NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT); |
| assertThat(mNotifInterruptionStateProvider.canAlertAwakeCommon(entry)).isFalse(); |
| } |
| |
| /** |
| * On screen alerts don't happen when the notification is snoozed. |
| */ |
| @Test |
| public void testCanAlertAwakeCommon_false_snoozedPackage() { |
| ensureStateForAlertAwakeCommon(); |
| when(mHeadsUpManager.isSnoozed(any())).thenReturn(true); |
| |
| NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT); |
| assertThat(mNotifInterruptionStateProvider.canAlertAwakeCommon(entry)).isFalse(); |
| } |
| |
| /** |
| * On screen alerts don't happen when that package has just launched fullscreen. |
| */ |
| @Test |
| public void testCanAlertAwakeCommon_false_justLaunchedFullscreen() { |
| ensureStateForAlertAwakeCommon(); |
| |
| NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT); |
| entry.notifyFullScreenIntentLaunched(); |
| |
| assertThat(mNotifInterruptionStateProvider.canAlertAwakeCommon(entry)).isFalse(); |
| } |
| |
| /** |
| * Bubbles can happen. |
| */ |
| @Test |
| public void testShouldBubbleUp_true() { |
| ensureStateForBubbleUp(); |
| assertThat(mNotifInterruptionStateProvider.shouldBubbleUp(createBubble())).isTrue(); |
| } |
| |
| /** |
| * If the notification doesn't have permission to bubble, it shouldn't bubble. |
| */ |
| @Test |
| public void shouldBubbleUp_false_notAllowedToBubble() { |
| ensureStateForBubbleUp(); |
| |
| NotificationEntry entry = createBubble(); |
| modifyRanking(entry) |
| .setCanBubble(false) |
| .build(); |
| |
| assertThat(mNotifInterruptionStateProvider.shouldBubbleUp(entry)).isFalse(); |
| } |
| |
| /** |
| * If the notification isn't a bubble, it should definitely not show as a bubble. |
| */ |
| @Test |
| public void shouldBubbleUp_false_notABubble() { |
| ensureStateForBubbleUp(); |
| |
| NotificationEntry entry = createNotification(IMPORTANCE_HIGH); |
| modifyRanking(entry) |
| .setCanBubble(true) |
| .build(); |
| |
| assertThat(mNotifInterruptionStateProvider.shouldBubbleUp(entry)).isFalse(); |
| } |
| |
| /** |
| * If the notification doesn't have bubble metadata, it shouldn't bubble. |
| */ |
| @Test |
| public void shouldBubbleUp_false_invalidMetadata() { |
| ensureStateForBubbleUp(); |
| |
| NotificationEntry entry = createNotification(IMPORTANCE_HIGH); |
| modifyRanking(entry) |
| .setCanBubble(true) |
| .build(); |
| entry.getSbn().getNotification().flags |= FLAG_BUBBLE; |
| |
| assertThat(mNotifInterruptionStateProvider.shouldBubbleUp(entry)).isFalse(); |
| } |
| |
| /** |
| * If the notification can't heads up in general, it shouldn't bubble. |
| */ |
| @Test |
| public void shouldBubbleUp_false_alertAwakeCommonFalse() { |
| ensureStateForBubbleUp(); |
| |
| // Make alert common return false by pretending we're in VR mode |
| when(mPresenter.isDeviceInVrMode()).thenReturn(true); |
| |
| assertThat(mNotifInterruptionStateProvider.shouldBubbleUp(createBubble())).isFalse(); |
| } |
| |
| /** |
| * If the notification can't heads up in general, it shouldn't bubble. |
| */ |
| @Test |
| public void shouldBubbleUp_false_alertCommonFalse() { |
| ensureStateForBubbleUp(); |
| |
| // Make canAlertCommon false by saying it's filtered out |
| when(mNotificationFilter.shouldFilterOut(any())).thenReturn(true); |
| |
| assertThat(mNotifInterruptionStateProvider.shouldBubbleUp(createBubble())).isFalse(); |
| } |
| |
| private NotificationEntry createBubble() { |
| Notification.BubbleMetadata data = new Notification.BubbleMetadata.Builder() |
| .createIntentBubble(PendingIntent.getActivity(mContext, 0, new Intent(), 0), |
| Icon.createWithResource(mContext.getResources(), R.drawable.android)) |
| .build(); |
| Notification n = new Notification.Builder(getContext(), "a") |
| .setContentTitle("title") |
| .setContentText("content text") |
| .setBubbleMetadata(data) |
| .build(); |
| n.flags |= FLAG_BUBBLE; |
| |
| return new NotificationEntryBuilder() |
| .setPkg("a") |
| .setOpPkg("a") |
| .setTag("a") |
| .setNotification(n) |
| .setImportance(IMPORTANCE_HIGH) |
| .setCanBubble(true) |
| .build(); |
| } |
| |
| private NotificationEntry createNotification(int importance) { |
| Notification n = new Notification.Builder(getContext(), "a") |
| .setContentTitle("title") |
| .setContentText("content text") |
| .build(); |
| |
| return new NotificationEntryBuilder() |
| .setPkg("a") |
| .setOpPkg("a") |
| .setTag("a") |
| .setNotification(n) |
| .setImportance(importance) |
| .build(); |
| } |
| |
| /** |
| * Testable class overriding constructor. |
| */ |
| public static class TestableNotificationInterruptionStateProvider extends |
| NotificationInterruptionStateProvider { |
| |
| TestableNotificationInterruptionStateProvider(Context context, |
| PowerManager powerManager, IDreamManager dreamManager, |
| AmbientDisplayConfiguration ambientDisplayConfiguration, |
| NotificationFilter notificationFilter, |
| StatusBarStateController statusBarStateController, |
| BatteryController batteryController) { |
| super(context, powerManager, dreamManager, ambientDisplayConfiguration, |
| notificationFilter, batteryController, statusBarStateController); |
| } |
| } |
| } |