blob: 0db1f681a7ac85759cf9d739827cad80ed3c3972 [file] [log] [blame]
Mady Mellorc55b4122019-06-07 18:14:02 -07001/*
2 * Copyright (C) 2019 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.systemui.statusbar;
17
18
19import static android.app.Notification.FLAG_BUBBLE;
20import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
21import static android.app.NotificationManager.IMPORTANCE_HIGH;
22import static android.app.NotificationManager.IMPORTANCE_LOW;
23import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT;
24import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
25
26import static com.android.systemui.statusbar.StatusBarState.SHADE;
27
28import static com.google.common.truth.Truth.assertThat;
29
30import static org.mockito.ArgumentMatchers.any;
31import static org.mockito.ArgumentMatchers.anyInt;
32import static org.mockito.Mockito.verify;
33import static org.mockito.Mockito.when;
34
35import android.app.Notification;
36import android.app.PendingIntent;
37import android.content.Context;
38import android.content.Intent;
39import android.graphics.drawable.Icon;
40import android.hardware.display.AmbientDisplayConfiguration;
41import android.os.PowerManager;
42import android.os.RemoteException;
43import android.os.UserHandle;
44import android.service.dreams.IDreamManager;
45import android.service.notification.StatusBarNotification;
46import android.testing.AndroidTestingRunner;
47
48import androidx.test.filters.SmallTest;
49
50import com.android.systemui.R;
51import com.android.systemui.SysuiTestCase;
52import com.android.systemui.plugins.statusbar.StatusBarStateController;
53import com.android.systemui.statusbar.notification.NotificationFilter;
54import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider;
55import com.android.systemui.statusbar.notification.collection.NotificationEntry;
Lucas Dupin6edeb182019-09-25 13:39:21 -070056import com.android.systemui.statusbar.policy.BatteryController;
Mady Mellorc55b4122019-06-07 18:14:02 -070057import com.android.systemui.statusbar.policy.HeadsUpManager;
58
59import org.junit.Before;
60import org.junit.Test;
61import org.junit.runner.RunWith;
62import org.mockito.Mock;
63import org.mockito.MockitoAnnotations;
64
65/**
66 * Tests for the interruption state provider which understands whether the system & notification
67 * is in a state allowing a particular notification to hun, pulse, or bubble.
68 */
69@RunWith(AndroidTestingRunner.class)
70@SmallTest
71public class NotificationInterruptionStateProviderTest extends SysuiTestCase {
72
73 @Mock
74 PowerManager mPowerManager;
75 @Mock
76 IDreamManager mDreamManager;
77 @Mock
78 AmbientDisplayConfiguration mAmbientDisplayConfiguration;
79 @Mock
80 NotificationFilter mNotificationFilter;
81 @Mock
82 StatusBarStateController mStatusBarStateController;
83 @Mock
84 NotificationPresenter mPresenter;
85 @Mock
86 HeadsUpManager mHeadsUpManager;
87 @Mock
88 NotificationInterruptionStateProvider.HeadsUpSuppressor mHeadsUpSuppressor;
Lucas Dupin6edeb182019-09-25 13:39:21 -070089 @Mock
90 BatteryController mBatteryController;
Mady Mellorc55b4122019-06-07 18:14:02 -070091
92 private NotificationInterruptionStateProvider mNotifInterruptionStateProvider;
93
94 @Before
95 public void setup() {
96 MockitoAnnotations.initMocks(this);
97
98 mNotifInterruptionStateProvider =
99 new TestableNotificationInterruptionStateProvider(mContext,
100 mPowerManager,
101 mDreamManager,
102 mAmbientDisplayConfiguration,
103 mNotificationFilter,
Lucas Dupin6edeb182019-09-25 13:39:21 -0700104 mStatusBarStateController,
105 mBatteryController);
Mady Mellorc55b4122019-06-07 18:14:02 -0700106
107 mNotifInterruptionStateProvider.setUpWithPresenter(
108 mPresenter,
109 mHeadsUpManager,
110 mHeadsUpSuppressor);
111 }
112
113 /**
114 * Sets up the state such that any requests to
115 * {@link NotificationInterruptionStateProvider#canAlertCommon(NotificationEntry)} will
116 * pass as long its provided NotificationEntry fulfills group suppression check.
117 */
118 private void ensureStateForAlertCommon() {
119 when(mNotificationFilter.shouldFilterOut(any())).thenReturn(false);
120 }
121
122 /**
123 * Sets up the state such that any requests to
124 * {@link NotificationInterruptionStateProvider#canAlertAwakeCommon(NotificationEntry)} will
125 * pass as long its provided NotificationEntry fulfills launch fullscreen check.
126 */
127 private void ensureStateForAlertAwakeCommon() {
128 when(mPresenter.isDeviceInVrMode()).thenReturn(false);
129 when(mHeadsUpManager.isSnoozed(any())).thenReturn(false);
130 }
131
132 /**
133 * Sets up the state such that any requests to
134 * {@link NotificationInterruptionStateProvider#shouldHeadsUp(NotificationEntry)} will
135 * pass as long its provided NotificationEntry fulfills importance & DND checks.
136 */
137 private void ensureStateForHeadsUpWhenAwake() throws RemoteException {
138 ensureStateForAlertCommon();
139 ensureStateForAlertAwakeCommon();
140
141 when(mStatusBarStateController.isDozing()).thenReturn(false);
142 when(mDreamManager.isDreaming()).thenReturn(false);
143 when(mPowerManager.isScreenOn()).thenReturn(true);
144 when(mHeadsUpSuppressor.canHeadsUp(any(), any())).thenReturn(true);
145 }
146
147 /**
148 * Sets up the state such that any requests to
149 * {@link NotificationInterruptionStateProvider#shouldHeadsUp(NotificationEntry)} will
150 * pass as long its provided NotificationEntry fulfills importance & DND checks.
151 */
152 private void ensureStateForHeadsUpWhenDozing() {
153 ensureStateForAlertCommon();
154
155 when(mStatusBarStateController.isDozing()).thenReturn(true);
156 when(mAmbientDisplayConfiguration.pulseOnNotificationEnabled(anyInt())).thenReturn(true);
157 }
158
159 /**
160 * Sets up the state such that any requests to
161 * {@link NotificationInterruptionStateProvider#shouldBubbleUp(NotificationEntry)} will
162 * pass as long its provided NotificationEntry fulfills importance & bubble checks.
163 */
164 private void ensureStateForBubbleUp() {
165 ensureStateForAlertCommon();
166 ensureStateForAlertAwakeCommon();
167 }
168
169 /**
170 * Ensure that the disabled state is set correctly.
171 */
172 @Test
173 public void testDisableNotificationAlerts() {
174 // Enabled by default
175 assertThat(mNotifInterruptionStateProvider.areNotificationAlertsDisabled()).isFalse();
176
177 // Disable alerts
178 mNotifInterruptionStateProvider.setDisableNotificationAlerts(true);
179 assertThat(mNotifInterruptionStateProvider.areNotificationAlertsDisabled()).isTrue();
180
181 // Enable alerts
182 mNotifInterruptionStateProvider.setDisableNotificationAlerts(false);
183 assertThat(mNotifInterruptionStateProvider.areNotificationAlertsDisabled()).isFalse();
184 }
185
186 /**
187 * Ensure that the disabled alert state effects whether HUNs are enabled.
188 */
189 @Test
190 public void testHunSettingsChange_enabled_butAlertsDisabled() {
191 // Set up but without a mock change observer
192 mNotifInterruptionStateProvider.setUpWithPresenter(
193 mPresenter,
194 mHeadsUpManager,
195 mHeadsUpSuppressor);
196
197 // HUNs enabled by default
198 assertThat(mNotifInterruptionStateProvider.getUseHeadsUp()).isTrue();
199
200 // Set alerts disabled
201 mNotifInterruptionStateProvider.setDisableNotificationAlerts(true);
202
203 // No more HUNs
204 assertThat(mNotifInterruptionStateProvider.getUseHeadsUp()).isFalse();
205 }
206
207 /**
208 * Alerts can happen.
209 */
210 @Test
211 public void testCanAlertCommon_true() {
212 ensureStateForAlertCommon();
213
214 NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT);
215 assertThat(mNotifInterruptionStateProvider.canAlertCommon(entry)).isTrue();
216 }
217
218 /**
219 * Filtered out notifications don't alert.
220 */
221 @Test
222 public void testCanAlertCommon_false_filteredOut() {
223 ensureStateForAlertCommon();
224 when(mNotificationFilter.shouldFilterOut(any())).thenReturn(true);
225
226 NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT);
227 assertThat(mNotifInterruptionStateProvider.canAlertCommon(entry)).isFalse();
228 }
229
230 /**
231 * Grouped notifications have different alerting behaviours, sometimes the alert for a
232 * grouped notification may be suppressed {@link android.app.Notification#GROUP_ALERT_CHILDREN}.
233 */
234 @Test
235 public void testCanAlertCommon_false_suppressedForGroups() {
236 ensureStateForAlertCommon();
237
238 Notification n = new Notification.Builder(getContext(), "a")
239 .setGroup("a")
240 .setGroupSummary(true)
241 .setGroupAlertBehavior(Notification.GROUP_ALERT_CHILDREN)
242 .build();
243 StatusBarNotification sbn = new StatusBarNotification("a", "a", 0, "a", 0, 0, n,
244 UserHandle.of(0), null, 0);
245 NotificationEntry entry = new NotificationEntry(sbn);
246 entry.importance = IMPORTANCE_DEFAULT;
247
248 assertThat(mNotifInterruptionStateProvider.canAlertCommon(entry)).isFalse();
249 }
250
251 /**
252 * HUNs while dozing can happen.
253 */
254 @Test
255 public void testShouldHeadsUpWhenDozing_true() {
256 ensureStateForHeadsUpWhenDozing();
257
258 NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT);
259 assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue();
260 }
261
262 /**
263 * Ambient display can show HUNs for new notifications, this may be disabled.
264 */
265 @Test
266 public void testShouldHeadsUpWhenDozing_false_pulseDisabled() {
267 ensureStateForHeadsUpWhenDozing();
268 when(mAmbientDisplayConfiguration.pulseOnNotificationEnabled(anyInt())).thenReturn(false);
269
270 NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT);
271 assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse();
272 }
273
274 /**
275 * If the device is not in ambient display or sleeping then we don't HUN.
276 */
277 @Test
278 public void testShouldHeadsUpWhenDozing_false_notDozing() {
279 ensureStateForHeadsUpWhenDozing();
280 when(mStatusBarStateController.isDozing()).thenReturn(false);
281
282 NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT);
283 assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse();
284 }
285
286 /**
287 * In DND ambient effects can be suppressed
288 * {@link android.app.NotificationManager.Policy#SUPPRESSED_EFFECT_AMBIENT}.
289 */
290 @Test
291 public void testShouldHeadsUpWhenDozing_false_suppressingAmbient() {
292 ensureStateForHeadsUpWhenDozing();
293
294 NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT);
295 entry.suppressedVisualEffects = SUPPRESSED_EFFECT_AMBIENT;
296
297 assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse();
298 }
299
300 /**
301 * Notifications that are < {@link android.app.NotificationManager#IMPORTANCE_DEFAULT} don't
302 * get to pulse.
303 */
304 @Test
305 public void testShouldHeadsUpWhenDozing_false_lessImportant() {
306 ensureStateForHeadsUpWhenDozing();
307
308 NotificationEntry entry = createNotification(IMPORTANCE_LOW);
309 assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse();
310 }
311
312 /**
313 * Heads up can happen.
314 */
315 @Test
316 public void testShouldHeadsUp_true() throws RemoteException {
317 ensureStateForHeadsUpWhenAwake();
318
319 NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
320 assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isTrue();
321 }
322
323 /**
324 * Heads up notifications can be disabled in general.
325 */
326 @Test
327 public void testShouldHeadsUp_false_noHunsAllowed() throws RemoteException {
328 ensureStateForHeadsUpWhenAwake();
329
330 // Set alerts disabled, this should cause heads up to be false
331 mNotifInterruptionStateProvider.setDisableNotificationAlerts(true);
332 assertThat(mNotifInterruptionStateProvider.getUseHeadsUp()).isFalse();
333
334 NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
335 assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse();
336 }
337
338 /**
339 * If the device is dozing, we don't show as heads up.
340 */
341 @Test
342 public void testShouldHeadsUp_false_dozing() throws RemoteException {
343 ensureStateForHeadsUpWhenAwake();
344 when(mStatusBarStateController.isDozing()).thenReturn(true);
345
346 NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
347 assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse();
348 }
349
350 /**
351 * If the notification is a bubble, and the user is not on AOD / lockscreen, then
352 * the bubble is shown rather than the heads up.
353 */
354 @Test
355 public void testShouldHeadsUp_false_bubble() throws RemoteException {
356 ensureStateForHeadsUpWhenAwake();
357
358 // Bubble bit only applies to interruption when we're in the shade
359 when(mStatusBarStateController.getState()).thenReturn(SHADE);
360
361 assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(createBubble())).isFalse();
362 }
363
364 /**
365 * If we're not allowed to alert in general, we shouldn't be shown as heads up.
366 */
367 @Test
368 public void testShouldHeadsUp_false_alertCommonFalse() throws RemoteException {
369 ensureStateForHeadsUpWhenAwake();
370 // Make canAlertCommon false by saying it's filtered out
371 when(mNotificationFilter.shouldFilterOut(any())).thenReturn(true);
372
373 NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
374 assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse();
375 }
376
377 /**
378 * In DND HUN peek effects can be suppressed
379 * {@link android.app.NotificationManager.Policy#SUPPRESSED_EFFECT_PEEK}.
380 */
381 @Test
382 public void testShouldHeadsUp_false_suppressPeek() throws RemoteException {
383 ensureStateForHeadsUpWhenAwake();
384
385 NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
386 entry.suppressedVisualEffects = SUPPRESSED_EFFECT_PEEK;
387
388 assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse();
389 }
390
391 /**
392 * Notifications that are < {@link android.app.NotificationManager#IMPORTANCE_HIGH} don't get
393 * to show as a heads up.
394 */
395 @Test
396 public void testShouldHeadsUp_false_lessImportant() throws RemoteException {
397 ensureStateForHeadsUpWhenAwake();
398
399 NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT);
400 assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse();
401 }
402
403 /**
404 * If the device is not in use then we shouldn't be shown as heads up.
405 */
406 @Test
407 public void testShouldHeadsUp_false_deviceNotInUse() throws RemoteException {
408 ensureStateForHeadsUpWhenAwake();
409 NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
410
411 // Device is not in use if screen is not on
412 when(mPowerManager.isScreenOn()).thenReturn(false);
413 assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse();
414
415 // Also not in use if screen is on but we're showing screen saver / "dreaming"
416 when(mPowerManager.isDeviceIdleMode()).thenReturn(true);
417 when(mDreamManager.isDreaming()).thenReturn(true);
418 assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse();
419 }
420
421 /**
422 * If something wants to suppress this heads up, then it shouldn't be shown as a heads up.
423 */
424 @Test
425 public void testShouldHeadsUp_false_suppressed() throws RemoteException {
426 ensureStateForHeadsUpWhenAwake();
427 when(mHeadsUpSuppressor.canHeadsUp(any(), any())).thenReturn(false);
428
429 NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
430 assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(entry)).isFalse();
431 verify(mHeadsUpSuppressor).canHeadsUp(any(), any());
432 }
433
434 /**
435 * On screen alerts don't happen when the device is in VR Mode.
436 */
437 @Test
438 public void testCanAlertAwakeCommon__false_vrMode() {
439 ensureStateForAlertAwakeCommon();
440 when(mPresenter.isDeviceInVrMode()).thenReturn(true);
441
442 NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT);
443 assertThat(mNotifInterruptionStateProvider.canAlertAwakeCommon(entry)).isFalse();
444 }
445
446 /**
447 * On screen alerts don't happen when the notification is snoozed.
448 */
449 @Test
450 public void testCanAlertAwakeCommon_false_snoozedPackage() {
451 ensureStateForAlertAwakeCommon();
452 when(mHeadsUpManager.isSnoozed(any())).thenReturn(true);
453
454 NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT);
455 assertThat(mNotifInterruptionStateProvider.canAlertAwakeCommon(entry)).isFalse();
456 }
457
458 /**
459 * On screen alerts don't happen when that package has just launched fullscreen.
460 */
461 @Test
462 public void testCanAlertAwakeCommon_false_justLaunchedFullscreen() {
463 ensureStateForAlertAwakeCommon();
464
465 NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT);
466 entry.notifyFullScreenIntentLaunched();
467
468 assertThat(mNotifInterruptionStateProvider.canAlertAwakeCommon(entry)).isFalse();
469 }
470
471 /**
472 * Bubbles can happen.
473 */
474 @Test
475 public void testShouldBubbleUp_true() {
476 ensureStateForBubbleUp();
477 assertThat(mNotifInterruptionStateProvider.shouldBubbleUp(createBubble())).isTrue();
478 }
479
480 /**
481 * If the notification doesn't have permission to bubble, it shouldn't bubble.
482 */
483 @Test
484 public void shouldBubbleUp_false_notAllowedToBubble() {
485 ensureStateForBubbleUp();
486
487 NotificationEntry entry = createBubble();
488 entry.canBubble = false;
489
490 assertThat(mNotifInterruptionStateProvider.shouldBubbleUp(entry)).isFalse();
491 }
492
493 /**
494 * If the notification isn't a bubble, it should definitely not show as a bubble.
495 */
496 @Test
497 public void shouldBubbleUp_false_notABubble() {
498 ensureStateForBubbleUp();
499
500 NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
501 entry.canBubble = true;
502
503 assertThat(mNotifInterruptionStateProvider.shouldBubbleUp(entry)).isFalse();
504 }
505
506 /**
507 * If the notification doesn't have bubble metadata, it shouldn't bubble.
508 */
509 @Test
510 public void shouldBubbleUp_false_invalidMetadata() {
511 ensureStateForBubbleUp();
512
513 NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
514 entry.canBubble = true;
515 entry.notification.getNotification().flags |= FLAG_BUBBLE;
516
517 assertThat(mNotifInterruptionStateProvider.shouldBubbleUp(entry)).isFalse();
518 }
519
520 /**
521 * If the notification can't heads up in general, it shouldn't bubble.
522 */
523 @Test
524 public void shouldBubbleUp_false_alertAwakeCommonFalse() {
525 ensureStateForBubbleUp();
526
527 // Make alert common return false by pretending we're in VR mode
528 when(mPresenter.isDeviceInVrMode()).thenReturn(true);
529
530 assertThat(mNotifInterruptionStateProvider.shouldBubbleUp(createBubble())).isFalse();
531 }
532
533 /**
534 * If the notification can't heads up in general, it shouldn't bubble.
535 */
536 @Test
537 public void shouldBubbleUp_false_alertCommonFalse() {
538 ensureStateForBubbleUp();
539
540 // Make canAlertCommon false by saying it's filtered out
541 when(mNotificationFilter.shouldFilterOut(any())).thenReturn(true);
542
543 assertThat(mNotifInterruptionStateProvider.shouldBubbleUp(createBubble())).isFalse();
544 }
545
546 private NotificationEntry createBubble() {
547 Notification.BubbleMetadata data = new Notification.BubbleMetadata.Builder()
548 .setIntent(PendingIntent.getActivity(mContext, 0, new Intent(), 0))
549 .setIcon(Icon.createWithResource(mContext.getResources(), R.drawable.android))
550 .build();
551 Notification n = new Notification.Builder(getContext(), "a")
552 .setContentTitle("title")
553 .setContentText("content text")
554 .setBubbleMetadata(data)
555 .build();
556 StatusBarNotification sbn = new StatusBarNotification("a", "a", 0, "a", 0, 0, n,
557 UserHandle.of(0), null, 0);
558 NotificationEntry entry = new NotificationEntry(sbn);
559 entry.notification.getNotification().flags |= FLAG_BUBBLE;
560 entry.importance = IMPORTANCE_HIGH;
561 entry.canBubble = true;
562 return entry;
563 }
564
565 private NotificationEntry createNotification(int importance) {
566 Notification n = new Notification.Builder(getContext(), "a")
567 .setContentTitle("title")
568 .setContentText("content text")
569 .build();
570 StatusBarNotification sbn = new StatusBarNotification("a", "a", 0, "a", 0, 0, n,
571 UserHandle.of(0), null, 0);
572 NotificationEntry entry = new NotificationEntry(sbn);
573 entry.importance = importance;
574 return entry;
575 }
576
577 /**
578 * Testable class overriding constructor.
579 */
Lucas Dupin6edeb182019-09-25 13:39:21 -0700580 public static class TestableNotificationInterruptionStateProvider extends
Mady Mellorc55b4122019-06-07 18:14:02 -0700581 NotificationInterruptionStateProvider {
582
583 TestableNotificationInterruptionStateProvider(Context context,
584 PowerManager powerManager, IDreamManager dreamManager,
585 AmbientDisplayConfiguration ambientDisplayConfiguration,
586 NotificationFilter notificationFilter,
Lucas Dupin6edeb182019-09-25 13:39:21 -0700587 StatusBarStateController statusBarStateController,
588 BatteryController batteryController) {
Mady Mellorc55b4122019-06-07 18:14:02 -0700589 super(context, powerManager, dreamManager, ambientDisplayConfiguration,
Lucas Dupin6edeb182019-09-25 13:39:21 -0700590 notificationFilter, batteryController, statusBarStateController);
Mady Mellorc55b4122019-06-07 18:14:02 -0700591 }
592 }
593}