blob: 5bab0ef39326da071d38afe4fddd36aaafc4531f [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.Bundle;
27import android.os.PowerManager;
28import android.os.RemoteException;
29import android.os.ServiceManager;
Lucas Dupin2c3992b2019-03-11 16:34:08 -070030import android.os.UserHandle;
Gus Prevasec9e1f02018-12-18 15:28:12 -050031import android.provider.Settings;
32import android.service.dreams.DreamService;
33import android.service.dreams.IDreamManager;
34import android.service.notification.StatusBarNotification;
35import android.text.TextUtils;
36import android.util.Log;
37
38import com.android.internal.annotations.VisibleForTesting;
39import com.android.systemui.Dependency;
Beverly8fdb5332019-02-04 14:29:49 -050040import com.android.systemui.plugins.statusbar.StatusBarStateController;
Gus Prevasec9e1f02018-12-18 15:28:12 -050041import com.android.systemui.statusbar.NotificationPresenter;
Selim Cinekc3fec682019-06-06 18:11:07 -070042import com.android.systemui.statusbar.StatusBarState;
Ned Burnsf81c4c42019-01-07 14:10:43 -050043import com.android.systemui.statusbar.notification.collection.NotificationEntry;
Gus Prevasec9e1f02018-12-18 15:28:12 -050044import com.android.systemui.statusbar.phone.ShadeController;
45import com.android.systemui.statusbar.policy.HeadsUpManager;
46
47/**
48 * Provides heads-up and pulsing state for notification entries.
49 */
50public class NotificationInterruptionStateProvider {
51
52 private static final String TAG = "InterruptionStateProvider";
53 private static final boolean DEBUG = false;
54 private static final boolean ENABLE_HEADS_UP = true;
55 private static final String SETTING_HEADS_UP_TICKER = "ticker_gets_heads_up";
56
57 private final StatusBarStateController mStatusBarStateController =
58 Dependency.get(StatusBarStateController.class);
59 private final NotificationFilter mNotificationFilter = Dependency.get(NotificationFilter.class);
Lucas Dupin2c3992b2019-03-11 16:34:08 -070060 private final AmbientDisplayConfiguration mAmbientDisplayConfiguration;
Gus Prevasec9e1f02018-12-18 15:28:12 -050061
62 private final Context mContext;
63 private final PowerManager mPowerManager;
64 private final IDreamManager mDreamManager;
65
66 private NotificationPresenter mPresenter;
67 private ShadeController mShadeController;
68 private HeadsUpManager mHeadsUpManager;
69 private HeadsUpSuppressor mHeadsUpSuppressor;
70
71 private ContentObserver mHeadsUpObserver;
72 @VisibleForTesting
73 protected boolean mUseHeadsUp = false;
74 private boolean mDisableNotificationAlerts;
75
76 public NotificationInterruptionStateProvider(Context context) {
77 this(context,
78 (PowerManager) context.getSystemService(Context.POWER_SERVICE),
79 IDreamManager.Stub.asInterface(
Lucas Dupin2c3992b2019-03-11 16:34:08 -070080 ServiceManager.checkService(DreamService.DREAM_SERVICE)),
81 new AmbientDisplayConfiguration(context));
Gus Prevasec9e1f02018-12-18 15:28:12 -050082 }
83
84 @VisibleForTesting
85 protected NotificationInterruptionStateProvider(
86 Context context,
87 PowerManager powerManager,
Lucas Dupin2c3992b2019-03-11 16:34:08 -070088 IDreamManager dreamManager,
89 AmbientDisplayConfiguration ambientDisplayConfiguration) {
Gus Prevasec9e1f02018-12-18 15:28:12 -050090 mContext = context;
91 mPowerManager = powerManager;
92 mDreamManager = dreamManager;
Lucas Dupin2c3992b2019-03-11 16:34:08 -070093 mAmbientDisplayConfiguration = ambientDisplayConfiguration;
Gus Prevasec9e1f02018-12-18 15:28:12 -050094 }
95
96 /** Sets up late-binding dependencies for this component. */
97 public void setUpWithPresenter(
98 NotificationPresenter notificationPresenter,
99 HeadsUpManager headsUpManager,
100 HeadsUpSuppressor headsUpSuppressor) {
101 mPresenter = notificationPresenter;
102 mHeadsUpManager = headsUpManager;
103 mHeadsUpSuppressor = headsUpSuppressor;
104
105 mHeadsUpObserver = new ContentObserver(Dependency.get(Dependency.MAIN_HANDLER)) {
106 @Override
107 public void onChange(boolean selfChange) {
108 boolean wasUsing = mUseHeadsUp;
109 mUseHeadsUp = ENABLE_HEADS_UP && !mDisableNotificationAlerts
110 && Settings.Global.HEADS_UP_OFF != Settings.Global.getInt(
111 mContext.getContentResolver(),
112 Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED,
113 Settings.Global.HEADS_UP_OFF);
114 Log.d(TAG, "heads up is " + (mUseHeadsUp ? "enabled" : "disabled"));
115 if (wasUsing != mUseHeadsUp) {
116 if (!mUseHeadsUp) {
117 Log.d(TAG,
118 "dismissing any existing heads up notification on disable event");
119 mHeadsUpManager.releaseAllImmediately();
120 }
121 }
122 }
123 };
124
125 if (ENABLE_HEADS_UP) {
126 mContext.getContentResolver().registerContentObserver(
127 Settings.Global.getUriFor(Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED),
128 true,
129 mHeadsUpObserver);
130 mContext.getContentResolver().registerContentObserver(
131 Settings.Global.getUriFor(SETTING_HEADS_UP_TICKER), true,
132 mHeadsUpObserver);
133 }
134 mHeadsUpObserver.onChange(true); // set up
135 }
136
137 private ShadeController getShadeController() {
138 if (mShadeController == null) {
139 mShadeController = Dependency.get(ShadeController.class);
140 }
141 return mShadeController;
142 }
143
144 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800145 * Whether the notification should appear as a bubble with a fly-out on top of the screen.
146 *
147 * @param entry the entry to check
148 * @return true if the entry should bubble up, false otherwise
149 */
150 public boolean shouldBubbleUp(NotificationEntry entry) {
Mady Melloraa8fef22019-04-11 13:36:40 -0700151 final StatusBarNotification sbn = entry.notification;
152 if (!entry.canBubble) {
153 if (DEBUG) {
154 Log.d(TAG, "No bubble up: not allowed to bubble: " + sbn.getKey());
155 }
156 return false;
157 }
158
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800159 if (!entry.isBubble()) {
160 if (DEBUG) {
161 Log.d(TAG, "No bubble up: notification " + sbn.getKey()
162 + " is bubble? " + entry.isBubble());
163 }
164 return false;
165 }
166
Mady Melloraa8fef22019-04-11 13:36:40 -0700167 final Notification n = sbn.getNotification();
168 if (n.getBubbleMetadata() == null || n.getBubbleMetadata().getIntent() == null) {
169 if (DEBUG) {
170 Log.d(TAG, "No bubble up: notification: " + sbn.getKey()
171 + " doesn't have valid metadata");
172 }
173 return false;
174 }
175
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800176 if (!canHeadsUpCommon(entry)) {
177 return false;
178 }
179
180 return true;
181 }
182
183 /**
Gus Prevasec9e1f02018-12-18 15:28:12 -0500184 * Whether the notification should peek in from the top and alert the user.
185 *
186 * @param entry the entry to check
187 * @return true if the entry should heads up, false otherwise
188 */
Ned Burnsf81c4c42019-01-07 14:10:43 -0500189 public boolean shouldHeadsUp(NotificationEntry entry) {
Selim Cinekc3fec682019-06-06 18:11:07 -0700190 if (mStatusBarStateController.isDozing()) {
191 return shouldHeadsUpWhenDozing(entry);
192 } else {
193 return shouldHeadsUpWhenAwake(entry);
Gus Prevasec9e1f02018-12-18 15:28:12 -0500194 }
Selim Cinekc3fec682019-06-06 18:11:07 -0700195 }
196
197 private boolean shouldHeadsUpWhenAwake(NotificationEntry entry) {
198 StatusBarNotification sbn = entry.notification;
Gus Prevasec9e1f02018-12-18 15:28:12 -0500199
Gus Prevasec9e1f02018-12-18 15:28:12 -0500200 boolean inShade = mStatusBarStateController.getState() == SHADE;
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800201 if (entry.isBubble() && inShade) {
202 if (DEBUG) {
203 Log.d(TAG, "No heads up: in unlocked shade where notification is shown as a "
204 + "bubble: " + sbn.getKey());
205 }
Gus Prevasec9e1f02018-12-18 15:28:12 -0500206 return false;
207 }
208
209 if (!canAlertCommon(entry)) {
210 if (DEBUG) {
211 Log.d(TAG, "No heads up: notification shouldn't alert: " + sbn.getKey());
212 }
213 return false;
214 }
215
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800216 if (!canHeadsUpCommon(entry)) {
217 return false;
218 }
219
220 if (entry.importance < NotificationManager.IMPORTANCE_HIGH) {
Gus Prevasec9e1f02018-12-18 15:28:12 -0500221 if (DEBUG) {
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800222 Log.d(TAG, "No heads up: unimportant notification: " + sbn.getKey());
Gus Prevasec9e1f02018-12-18 15:28:12 -0500223 }
224 return false;
225 }
226
227 boolean isDreaming = false;
228 try {
229 isDreaming = mDreamManager.isDreaming();
230 } catch (RemoteException e) {
231 Log.e(TAG, "Failed to query dream manager.", e);
232 }
233 boolean inUse = mPowerManager.isScreenOn() && !isDreaming;
234
235 if (!inUse) {
236 if (DEBUG) {
237 Log.d(TAG, "No heads up: not in use: " + sbn.getKey());
238 }
239 return false;
240 }
241
Gus Prevasec9e1f02018-12-18 15:28:12 -0500242 if (!mHeadsUpSuppressor.canHeadsUp(entry, sbn)) {
243 return false;
244 }
245
246 return true;
247 }
248
249 /**
250 * Whether or not the notification should "pulse" on the user's display when the phone is
251 * dozing. This displays the ambient view of the notification.
252 *
253 * @param entry the entry to check
254 * @return true if the entry should ambient pulse, false otherwise
255 */
Selim Cinekc3fec682019-06-06 18:11:07 -0700256 private boolean shouldHeadsUpWhenDozing(NotificationEntry entry) {
Gus Prevasec9e1f02018-12-18 15:28:12 -0500257 StatusBarNotification sbn = entry.notification;
258
Lucas Dupin2c3992b2019-03-11 16:34:08 -0700259 if (!mAmbientDisplayConfiguration.pulseOnNotificationEnabled(UserHandle.USER_CURRENT)) {
260 if (DEBUG) {
261 Log.d(TAG, "No pulsing: disabled by setting: " + sbn.getKey());
262 }
263 return false;
264 }
265
Gus Prevasec9e1f02018-12-18 15:28:12 -0500266 if (!canAlertCommon(entry)) {
267 if (DEBUG) {
268 Log.d(TAG, "No pulsing: notification shouldn't alert: " + sbn.getKey());
269 }
270 return false;
271 }
272
273 if (entry.shouldSuppressAmbient()) {
274 if (DEBUG) {
275 Log.d(TAG, "No pulsing: ambient effect suppressed: " + sbn.getKey());
276 }
277 return false;
278 }
279
280 if (entry.importance < NotificationManager.IMPORTANCE_DEFAULT) {
281 if (DEBUG) {
282 Log.d(TAG, "No pulsing: not important enough: " + sbn.getKey());
283 }
284 return false;
285 }
Selim Cinekc3fec682019-06-06 18:11:07 -0700286 return true;
Gus Prevasec9e1f02018-12-18 15:28:12 -0500287 }
288
289 /**
Selim Cinekc3fec682019-06-06 18:11:07 -0700290 * Common checks between regular heads up and when pulsing. See
Ned Burnsf81c4c42019-01-07 14:10:43 -0500291 * {@link #shouldHeadsUp(NotificationEntry)} and
Selim Cinekc3fec682019-06-06 18:11:07 -0700292 * {@link #shouldHeadsUpWhenDozing(NotificationEntry)}. Notifications that fail any of these
293 * checks
Gus Prevasec9e1f02018-12-18 15:28:12 -0500294 * should not alert at all.
295 *
296 * @param entry the entry to check
297 * @return true if these checks pass, false if the notification should not alert
298 */
Ned Burnsf81c4c42019-01-07 14:10:43 -0500299 protected boolean canAlertCommon(NotificationEntry entry) {
Gus Prevasec9e1f02018-12-18 15:28:12 -0500300 StatusBarNotification sbn = entry.notification;
301
302 if (mNotificationFilter.shouldFilterOut(entry)) {
303 if (DEBUG) {
304 Log.d(TAG, "No alerting: filtered notification: " + sbn.getKey());
305 }
306 return false;
307 }
308
309 // Don't alert notifications that are suppressed due to group alert behavior
310 if (sbn.isGroup() && sbn.getNotification().suppressAlertingDueToGrouping()) {
311 if (DEBUG) {
312 Log.d(TAG, "No alerting: suppressed due to group alert behavior");
313 }
314 return false;
315 }
316
317 return true;
318 }
319
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800320 /**
321 * Common checks between heads up alerting and bubble fly out alerting. See
322 * {@link #shouldHeadsUp(NotificationEntry)} and
323 * {@link #shouldBubbleUp(NotificationEntry)}. Notifications that fail any of these
324 * checks should not interrupt the user on screen.
325 *
326 * @param entry the entry to check
327 * @return true if these checks pass, false if the notification should not interrupt on screen
328 */
329 public boolean canHeadsUpCommon(NotificationEntry entry) {
330 StatusBarNotification sbn = entry.notification;
331
332 if (!mUseHeadsUp || mPresenter.isDeviceInVrMode()) {
333 if (DEBUG) {
334 Log.d(TAG, "No heads up: no huns or vr mode");
335 }
336 return false;
337 }
338
339 if (entry.shouldSuppressPeek()) {
340 if (DEBUG) {
341 Log.d(TAG, "No heads up: suppressed by DND: " + sbn.getKey());
342 }
343 return false;
344 }
345
346 if (isSnoozedPackage(sbn)) {
347 if (DEBUG) {
348 Log.d(TAG, "No heads up: snoozed package: " + sbn.getKey());
349 }
350 return false;
351 }
352
353 if (entry.hasJustLaunchedFullScreenIntent()) {
354 if (DEBUG) {
355 Log.d(TAG, "No heads up: recent fullscreen: " + sbn.getKey());
356 }
357 return false;
358 }
359
360 return true;
361 }
362
Gus Prevasec9e1f02018-12-18 15:28:12 -0500363 private boolean isSnoozedPackage(StatusBarNotification sbn) {
364 return mHeadsUpManager.isSnoozed(sbn.getPackageName());
365 }
366
367 /** Sets whether to disable all alerts. */
368 public void setDisableNotificationAlerts(boolean disableNotificationAlerts) {
369 mDisableNotificationAlerts = disableNotificationAlerts;
370 mHeadsUpObserver.onChange(true);
371 }
372
373 protected NotificationPresenter getPresenter() {
374 return mPresenter;
375 }
376
Selim Cinekc3fec682019-06-06 18:11:07 -0700377 /**
378 * When an entry was added, should we launch its fullscreen intent? Examples are Alarms or
379 * incoming calls.
380 *
381 * @param entry the entry that was added
382 * @return {@code true} if we should launch the full screen intent
383 */
384 public boolean shouldLaunchFullScreenIntentWhenAdded(NotificationEntry entry) {
385 return entry.notification.getNotification().fullScreenIntent != null
386 && (!shouldHeadsUp(entry)
387 || mStatusBarStateController.getState() == StatusBarState.KEYGUARD);
388 }
389
Gus Prevasec9e1f02018-12-18 15:28:12 -0500390 /** A component which can suppress heads-up notifications due to the overall state of the UI. */
391 public interface HeadsUpSuppressor {
392 /**
393 * Returns false if the provided notification is ineligible for heads-up according to this
394 * component.
395 *
396 * @param entry entry of the notification that might be heads upped
397 * @param sbn notification that might be heads upped
398 * @return false if the notification can not be heads upped
399 */
Ned Burnsf81c4c42019-01-07 14:10:43 -0500400 boolean canHeadsUp(NotificationEntry entry, StatusBarNotification sbn);
Gus Prevasec9e1f02018-12-18 15:28:12 -0500401
402 }
403
404}