blob: 150667b86828e43d7350955b91500f860e2fff54 [file] [log] [blame]
Gus Prevasec9e1f02018-12-18 15:28:12 -05001/*
2 * Copyright (C) 2018 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 */
16
17package com.android.systemui.statusbar.notification;
18
19import static com.android.systemui.statusbar.StatusBarState.SHADE;
20
21import android.app.Notification;
22import android.app.NotificationManager;
23import android.content.Context;
24import android.database.ContentObserver;
Lucas Dupin2c3992b2019-03-11 16:34:08 -070025import android.hardware.display.AmbientDisplayConfiguration;
Gus Prevasec9e1f02018-12-18 15:28:12 -050026import android.os.PowerManager;
27import android.os.RemoteException;
28import android.os.ServiceManager;
Lucas Dupin2c3992b2019-03-11 16:34:08 -070029import android.os.UserHandle;
Gus Prevasec9e1f02018-12-18 15:28:12 -050030import android.provider.Settings;
31import android.service.dreams.DreamService;
32import android.service.dreams.IDreamManager;
33import android.service.notification.StatusBarNotification;
Gus Prevasec9e1f02018-12-18 15:28:12 -050034import android.util.Log;
35
36import com.android.internal.annotations.VisibleForTesting;
37import com.android.systemui.Dependency;
Beverly8fdb5332019-02-04 14:29:49 -050038import com.android.systemui.plugins.statusbar.StatusBarStateController;
Gus Prevasec9e1f02018-12-18 15:28:12 -050039import com.android.systemui.statusbar.NotificationPresenter;
Selim Cinekc3fec682019-06-06 18:11:07 -070040import com.android.systemui.statusbar.StatusBarState;
Ned Burnsf81c4c42019-01-07 14:10:43 -050041import com.android.systemui.statusbar.notification.collection.NotificationEntry;
Gus Prevasec9e1f02018-12-18 15:28:12 -050042import com.android.systemui.statusbar.policy.HeadsUpManager;
43
Govinda Wasserman3e7ce552019-08-13 11:35:44 -040044import javax.inject.Inject;
45import javax.inject.Singleton;
46
Gus Prevasec9e1f02018-12-18 15:28:12 -050047/**
48 * Provides heads-up and pulsing state for notification entries.
49 */
Govinda Wasserman3e7ce552019-08-13 11:35:44 -040050@Singleton
Gus Prevasec9e1f02018-12-18 15:28:12 -050051public class NotificationInterruptionStateProvider {
52
53 private static final String TAG = "InterruptionStateProvider";
54 private static final boolean DEBUG = false;
Lucas Dupinc0b81112019-07-25 18:56:12 -070055 private static final boolean DEBUG_HEADS_UP = true;
Gus Prevasec9e1f02018-12-18 15:28:12 -050056 private static final boolean ENABLE_HEADS_UP = true;
57 private static final String SETTING_HEADS_UP_TICKER = "ticker_gets_heads_up";
58
Mady Mellorc55b4122019-06-07 18:14:02 -070059 private final StatusBarStateController mStatusBarStateController;
60 private final NotificationFilter mNotificationFilter;
Lucas Dupin2c3992b2019-03-11 16:34:08 -070061 private final AmbientDisplayConfiguration mAmbientDisplayConfiguration;
Gus Prevasec9e1f02018-12-18 15:28:12 -050062
63 private final Context mContext;
64 private final PowerManager mPowerManager;
65 private final IDreamManager mDreamManager;
66
67 private NotificationPresenter mPresenter;
Gus Prevasec9e1f02018-12-18 15:28:12 -050068 private HeadsUpManager mHeadsUpManager;
69 private HeadsUpSuppressor mHeadsUpSuppressor;
70
71 private ContentObserver mHeadsUpObserver;
72 @VisibleForTesting
73 protected boolean mUseHeadsUp = false;
74 private boolean mDisableNotificationAlerts;
75
Govinda Wasserman3e7ce552019-08-13 11:35:44 -040076 @Inject
Mady Mellorc55b4122019-06-07 18:14:02 -070077 public NotificationInterruptionStateProvider(Context context, NotificationFilter filter,
78 StatusBarStateController stateController) {
Gus Prevasec9e1f02018-12-18 15:28:12 -050079 this(context,
80 (PowerManager) context.getSystemService(Context.POWER_SERVICE),
81 IDreamManager.Stub.asInterface(
Lucas Dupin2c3992b2019-03-11 16:34:08 -070082 ServiceManager.checkService(DreamService.DREAM_SERVICE)),
Mady Mellorc55b4122019-06-07 18:14:02 -070083 new AmbientDisplayConfiguration(context),
84 filter,
85 stateController);
Gus Prevasec9e1f02018-12-18 15:28:12 -050086 }
87
88 @VisibleForTesting
89 protected NotificationInterruptionStateProvider(
90 Context context,
91 PowerManager powerManager,
Lucas Dupin2c3992b2019-03-11 16:34:08 -070092 IDreamManager dreamManager,
Mady Mellorc55b4122019-06-07 18:14:02 -070093 AmbientDisplayConfiguration ambientDisplayConfiguration,
94 NotificationFilter notificationFilter,
95 StatusBarStateController statusBarStateController) {
Gus Prevasec9e1f02018-12-18 15:28:12 -050096 mContext = context;
97 mPowerManager = powerManager;
98 mDreamManager = dreamManager;
Lucas Dupin2c3992b2019-03-11 16:34:08 -070099 mAmbientDisplayConfiguration = ambientDisplayConfiguration;
Mady Mellorc55b4122019-06-07 18:14:02 -0700100 mNotificationFilter = notificationFilter;
101 mStatusBarStateController = statusBarStateController;
Gus Prevasec9e1f02018-12-18 15:28:12 -0500102 }
103
104 /** Sets up late-binding dependencies for this component. */
105 public void setUpWithPresenter(
106 NotificationPresenter notificationPresenter,
107 HeadsUpManager headsUpManager,
108 HeadsUpSuppressor headsUpSuppressor) {
Mady Mellorc55b4122019-06-07 18:14:02 -0700109 setUpWithPresenter(notificationPresenter, headsUpManager, headsUpSuppressor,
110 new ContentObserver(Dependency.get(Dependency.MAIN_HANDLER)) {
111 @Override
112 public void onChange(boolean selfChange) {
113 boolean wasUsing = mUseHeadsUp;
114 mUseHeadsUp = ENABLE_HEADS_UP && !mDisableNotificationAlerts
115 && Settings.Global.HEADS_UP_OFF != Settings.Global.getInt(
116 mContext.getContentResolver(),
117 Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED,
118 Settings.Global.HEADS_UP_OFF);
119 Log.d(TAG, "heads up is " + (mUseHeadsUp ? "enabled" : "disabled"));
120 if (wasUsing != mUseHeadsUp) {
121 if (!mUseHeadsUp) {
122 Log.d(TAG,
123 "dismissing any existing heads up notification on disable"
124 + " event");
125 mHeadsUpManager.releaseAllImmediately();
126 }
127 }
128 }
129 });
130 }
131
132 /** Sets up late-binding dependencies for this component. */
133 public void setUpWithPresenter(
134 NotificationPresenter notificationPresenter,
135 HeadsUpManager headsUpManager,
136 HeadsUpSuppressor headsUpSuppressor,
137 ContentObserver observer) {
Gus Prevasec9e1f02018-12-18 15:28:12 -0500138 mPresenter = notificationPresenter;
139 mHeadsUpManager = headsUpManager;
140 mHeadsUpSuppressor = headsUpSuppressor;
Mady Mellorc55b4122019-06-07 18:14:02 -0700141 mHeadsUpObserver = observer;
Gus Prevasec9e1f02018-12-18 15:28:12 -0500142
143 if (ENABLE_HEADS_UP) {
144 mContext.getContentResolver().registerContentObserver(
145 Settings.Global.getUriFor(Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED),
146 true,
147 mHeadsUpObserver);
148 mContext.getContentResolver().registerContentObserver(
149 Settings.Global.getUriFor(SETTING_HEADS_UP_TICKER), true,
150 mHeadsUpObserver);
151 }
152 mHeadsUpObserver.onChange(true); // set up
153 }
154
Gus Prevasec9e1f02018-12-18 15:28:12 -0500155 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800156 * Whether the notification should appear as a bubble with a fly-out on top of the screen.
157 *
158 * @param entry the entry to check
159 * @return true if the entry should bubble up, false otherwise
160 */
161 public boolean shouldBubbleUp(NotificationEntry entry) {
Mady Melloraa8fef22019-04-11 13:36:40 -0700162 final StatusBarNotification sbn = entry.notification;
Mady Mellorc55b4122019-06-07 18:14:02 -0700163
164 if (!canAlertCommon(entry)) {
165 return false;
166 }
167
168 if (!canAlertAwakeCommon(entry)) {
169 return false;
170 }
171
Mady Melloraa8fef22019-04-11 13:36:40 -0700172 if (!entry.canBubble) {
173 if (DEBUG) {
174 Log.d(TAG, "No bubble up: not allowed to bubble: " + sbn.getKey());
175 }
176 return false;
177 }
178
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800179 if (!entry.isBubble()) {
180 if (DEBUG) {
181 Log.d(TAG, "No bubble up: notification " + sbn.getKey()
182 + " is bubble? " + entry.isBubble());
183 }
184 return false;
185 }
186
Mady Melloraa8fef22019-04-11 13:36:40 -0700187 final Notification n = sbn.getNotification();
188 if (n.getBubbleMetadata() == null || n.getBubbleMetadata().getIntent() == null) {
189 if (DEBUG) {
190 Log.d(TAG, "No bubble up: notification: " + sbn.getKey()
191 + " doesn't have valid metadata");
192 }
193 return false;
194 }
195
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800196 return true;
197 }
198
199 /**
Gus Prevasec9e1f02018-12-18 15:28:12 -0500200 * Whether the notification should peek in from the top and alert the user.
201 *
202 * @param entry the entry to check
203 * @return true if the entry should heads up, false otherwise
204 */
Ned Burnsf81c4c42019-01-07 14:10:43 -0500205 public boolean shouldHeadsUp(NotificationEntry entry) {
Selim Cinekc3fec682019-06-06 18:11:07 -0700206 if (mStatusBarStateController.isDozing()) {
207 return shouldHeadsUpWhenDozing(entry);
208 } else {
209 return shouldHeadsUpWhenAwake(entry);
Gus Prevasec9e1f02018-12-18 15:28:12 -0500210 }
Selim Cinekc3fec682019-06-06 18:11:07 -0700211 }
212
213 private boolean shouldHeadsUpWhenAwake(NotificationEntry entry) {
214 StatusBarNotification sbn = entry.notification;
Gus Prevasec9e1f02018-12-18 15:28:12 -0500215
Mady Mellorc55b4122019-06-07 18:14:02 -0700216 if (!mUseHeadsUp) {
Mady Mellor9defe412019-08-02 10:06:47 -0700217 if (DEBUG_HEADS_UP) {
Mady Mellorc55b4122019-06-07 18:14:02 -0700218 Log.d(TAG, "No heads up: no huns");
219 }
220 return false;
221 }
222
223 if (!canAlertCommon(entry)) {
224 return false;
225 }
226
227 if (!canAlertAwakeCommon(entry)) {
228 return false;
229 }
230
Gus Prevasec9e1f02018-12-18 15:28:12 -0500231 boolean inShade = mStatusBarStateController.getState() == SHADE;
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800232 if (entry.isBubble() && inShade) {
Lucas Dupinc0b81112019-07-25 18:56:12 -0700233 if (DEBUG_HEADS_UP) {
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800234 Log.d(TAG, "No heads up: in unlocked shade where notification is shown as a "
235 + "bubble: " + sbn.getKey());
236 }
Gus Prevasec9e1f02018-12-18 15:28:12 -0500237 return false;
238 }
239
Mady Mellorc55b4122019-06-07 18:14:02 -0700240 if (entry.shouldSuppressPeek()) {
Lucas Dupinc0b81112019-07-25 18:56:12 -0700241 if (DEBUG_HEADS_UP) {
Mady Mellorc55b4122019-06-07 18:14:02 -0700242 Log.d(TAG, "No heads up: suppressed by DND: " + sbn.getKey());
Gus Prevasec9e1f02018-12-18 15:28:12 -0500243 }
244 return false;
245 }
246
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800247 if (entry.importance < NotificationManager.IMPORTANCE_HIGH) {
Lucas Dupinc0b81112019-07-25 18:56:12 -0700248 if (DEBUG_HEADS_UP) {
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800249 Log.d(TAG, "No heads up: unimportant notification: " + sbn.getKey());
Gus Prevasec9e1f02018-12-18 15:28:12 -0500250 }
251 return false;
252 }
253
254 boolean isDreaming = false;
255 try {
256 isDreaming = mDreamManager.isDreaming();
257 } catch (RemoteException e) {
258 Log.e(TAG, "Failed to query dream manager.", e);
259 }
260 boolean inUse = mPowerManager.isScreenOn() && !isDreaming;
261
262 if (!inUse) {
Lucas Dupinc0b81112019-07-25 18:56:12 -0700263 if (DEBUG_HEADS_UP) {
Gus Prevasec9e1f02018-12-18 15:28:12 -0500264 Log.d(TAG, "No heads up: not in use: " + sbn.getKey());
265 }
266 return false;
267 }
268
Gus Prevasec9e1f02018-12-18 15:28:12 -0500269 if (!mHeadsUpSuppressor.canHeadsUp(entry, sbn)) {
Lucas Dupinc0b81112019-07-25 18:56:12 -0700270 if (DEBUG_HEADS_UP) {
271 Log.d(TAG, "No heads up: aborted by suppressor: " + sbn.getKey());
272 }
Gus Prevasec9e1f02018-12-18 15:28:12 -0500273 return false;
274 }
275
276 return true;
277 }
278
279 /**
280 * Whether or not the notification should "pulse" on the user's display when the phone is
281 * dozing. This displays the ambient view of the notification.
282 *
283 * @param entry the entry to check
284 * @return true if the entry should ambient pulse, false otherwise
285 */
Selim Cinekc3fec682019-06-06 18:11:07 -0700286 private boolean shouldHeadsUpWhenDozing(NotificationEntry entry) {
Gus Prevasec9e1f02018-12-18 15:28:12 -0500287 StatusBarNotification sbn = entry.notification;
288
Lucas Dupin2c3992b2019-03-11 16:34:08 -0700289 if (!mAmbientDisplayConfiguration.pulseOnNotificationEnabled(UserHandle.USER_CURRENT)) {
Lucas Dupinc0b81112019-07-25 18:56:12 -0700290 if (DEBUG_HEADS_UP) {
Lucas Dupin2c3992b2019-03-11 16:34:08 -0700291 Log.d(TAG, "No pulsing: disabled by setting: " + sbn.getKey());
292 }
293 return false;
294 }
295
Gus Prevasec9e1f02018-12-18 15:28:12 -0500296 if (!canAlertCommon(entry)) {
Lucas Dupinc0b81112019-07-25 18:56:12 -0700297 if (DEBUG_HEADS_UP) {
Gus Prevasec9e1f02018-12-18 15:28:12 -0500298 Log.d(TAG, "No pulsing: notification shouldn't alert: " + sbn.getKey());
299 }
300 return false;
301 }
302
303 if (entry.shouldSuppressAmbient()) {
Lucas Dupinc0b81112019-07-25 18:56:12 -0700304 if (DEBUG_HEADS_UP) {
Gus Prevasec9e1f02018-12-18 15:28:12 -0500305 Log.d(TAG, "No pulsing: ambient effect suppressed: " + sbn.getKey());
306 }
307 return false;
308 }
309
310 if (entry.importance < NotificationManager.IMPORTANCE_DEFAULT) {
Lucas Dupinc0b81112019-07-25 18:56:12 -0700311 if (DEBUG_HEADS_UP) {
Gus Prevasec9e1f02018-12-18 15:28:12 -0500312 Log.d(TAG, "No pulsing: not important enough: " + sbn.getKey());
313 }
314 return false;
315 }
Selim Cinekc3fec682019-06-06 18:11:07 -0700316 return true;
Gus Prevasec9e1f02018-12-18 15:28:12 -0500317 }
318
319 /**
Mady Mellorc55b4122019-06-07 18:14:02 -0700320 * Common checks between regular & AOD heads up and bubbles.
Gus Prevasec9e1f02018-12-18 15:28:12 -0500321 *
322 * @param entry the entry to check
323 * @return true if these checks pass, false if the notification should not alert
324 */
Mady Mellorc55b4122019-06-07 18:14:02 -0700325 @VisibleForTesting
326 public boolean canAlertCommon(NotificationEntry entry) {
Gus Prevasec9e1f02018-12-18 15:28:12 -0500327 StatusBarNotification sbn = entry.notification;
328
329 if (mNotificationFilter.shouldFilterOut(entry)) {
Lucas Dupinc0b81112019-07-25 18:56:12 -0700330 if (DEBUG || DEBUG_HEADS_UP) {
Gus Prevasec9e1f02018-12-18 15:28:12 -0500331 Log.d(TAG, "No alerting: filtered notification: " + sbn.getKey());
332 }
333 return false;
334 }
335
336 // Don't alert notifications that are suppressed due to group alert behavior
337 if (sbn.isGroup() && sbn.getNotification().suppressAlertingDueToGrouping()) {
Lucas Dupinc0b81112019-07-25 18:56:12 -0700338 if (DEBUG || DEBUG_HEADS_UP) {
Gus Prevasec9e1f02018-12-18 15:28:12 -0500339 Log.d(TAG, "No alerting: suppressed due to group alert behavior");
340 }
341 return false;
342 }
Gus Prevasec9e1f02018-12-18 15:28:12 -0500343 return true;
344 }
345
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800346 /**
Mady Mellorc55b4122019-06-07 18:14:02 -0700347 * Common checks between alerts that occur while the device is awake (heads up & bubbles).
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800348 *
349 * @param entry the entry to check
Mady Mellorc55b4122019-06-07 18:14:02 -0700350 * @return true if these checks pass, false if the notification should not alert
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800351 */
Mady Mellorc55b4122019-06-07 18:14:02 -0700352 @VisibleForTesting
353 public boolean canAlertAwakeCommon(NotificationEntry entry) {
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800354 StatusBarNotification sbn = entry.notification;
355
Mady Mellorc55b4122019-06-07 18:14:02 -0700356 if (mPresenter.isDeviceInVrMode()) {
Lucas Dupinc0b81112019-07-25 18:56:12 -0700357 if (DEBUG_HEADS_UP) {
Mady Mellorc55b4122019-06-07 18:14:02 -0700358 Log.d(TAG, "No alerting: no huns or vr mode");
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800359 }
360 return false;
361 }
362
363 if (isSnoozedPackage(sbn)) {
Lucas Dupinc0b81112019-07-25 18:56:12 -0700364 if (DEBUG_HEADS_UP) {
Mady Mellorc55b4122019-06-07 18:14:02 -0700365 Log.d(TAG, "No alerting: snoozed package: " + sbn.getKey());
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800366 }
367 return false;
368 }
369
370 if (entry.hasJustLaunchedFullScreenIntent()) {
Lucas Dupinc0b81112019-07-25 18:56:12 -0700371 if (DEBUG_HEADS_UP) {
Mady Mellorc55b4122019-06-07 18:14:02 -0700372 Log.d(TAG, "No alerting: recent fullscreen: " + sbn.getKey());
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800373 }
374 return false;
375 }
376
377 return true;
378 }
379
Gus Prevasec9e1f02018-12-18 15:28:12 -0500380 private boolean isSnoozedPackage(StatusBarNotification sbn) {
381 return mHeadsUpManager.isSnoozed(sbn.getPackageName());
382 }
383
384 /** Sets whether to disable all alerts. */
385 public void setDisableNotificationAlerts(boolean disableNotificationAlerts) {
386 mDisableNotificationAlerts = disableNotificationAlerts;
387 mHeadsUpObserver.onChange(true);
388 }
389
Mady Mellorc55b4122019-06-07 18:14:02 -0700390 /** Whether all alerts are disabled. */
391 @VisibleForTesting
392 public boolean areNotificationAlertsDisabled() {
393 return mDisableNotificationAlerts;
394 }
395
396 /** Whether HUNs should be used. */
397 @VisibleForTesting
398 public boolean getUseHeadsUp() {
399 return mUseHeadsUp;
400 }
401
Gus Prevasec9e1f02018-12-18 15:28:12 -0500402 protected NotificationPresenter getPresenter() {
403 return mPresenter;
404 }
405
Selim Cinekc3fec682019-06-06 18:11:07 -0700406 /**
407 * When an entry was added, should we launch its fullscreen intent? Examples are Alarms or
408 * incoming calls.
409 *
410 * @param entry the entry that was added
411 * @return {@code true} if we should launch the full screen intent
412 */
413 public boolean shouldLaunchFullScreenIntentWhenAdded(NotificationEntry entry) {
414 return entry.notification.getNotification().fullScreenIntent != null
415 && (!shouldHeadsUp(entry)
416 || mStatusBarStateController.getState() == StatusBarState.KEYGUARD);
417 }
418
Gus Prevasec9e1f02018-12-18 15:28:12 -0500419 /** A component which can suppress heads-up notifications due to the overall state of the UI. */
420 public interface HeadsUpSuppressor {
421 /**
422 * Returns false if the provided notification is ineligible for heads-up according to this
423 * component.
424 *
425 * @param entry entry of the notification that might be heads upped
426 * @param sbn notification that might be heads upped
427 * @return false if the notification can not be heads upped
428 */
Ned Burnsf81c4c42019-01-07 14:10:43 -0500429 boolean canHeadsUp(NotificationEntry entry, StatusBarNotification sbn);
Gus Prevasec9e1f02018-12-18 15:28:12 -0500430
431 }
432
433}