1 /* 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 17 package com.android.systemui.statusbar.notification; 18 19 import static com.android.systemui.statusbar.StatusBarState.SHADE; 20 21 import android.app.Notification; 22 import android.app.NotificationManager; 23 import android.content.Context; 24 import android.database.ContentObserver; 25 import android.hardware.display.AmbientDisplayConfiguration; 26 import android.os.Build; 27 import android.os.PowerManager; 28 import android.os.RemoteException; 29 import android.os.ServiceManager; 30 import android.os.UserHandle; 31 import android.provider.Settings; 32 import android.service.dreams.DreamService; 33 import android.service.dreams.IDreamManager; 34 import android.service.notification.StatusBarNotification; 35 import android.util.Log; 36 37 import com.android.internal.annotations.VisibleForTesting; 38 import com.android.systemui.Dependency; 39 import com.android.systemui.plugins.statusbar.StatusBarStateController; 40 import com.android.systemui.statusbar.NotificationPresenter; 41 import com.android.systemui.statusbar.StatusBarState; 42 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 43 import com.android.systemui.statusbar.policy.BatteryController; 44 import com.android.systemui.statusbar.policy.HeadsUpManager; 45 46 import javax.inject.Inject; 47 import javax.inject.Singleton; 48 49 /** 50 * Provides heads-up and pulsing state for notification entries. 51 */ 52 @Singleton 53 public class NotificationInterruptionStateProvider { 54 55 private static final String TAG = "InterruptionStateProvider"; 56 private static final boolean DEBUG = false; 57 private static final boolean DEBUG_HEADS_UP = Build.IS_DEBUGGABLE; 58 private static final boolean ENABLE_HEADS_UP = true; 59 private static final String SETTING_HEADS_UP_TICKER = "ticker_gets_heads_up"; 60 61 private final StatusBarStateController mStatusBarStateController; 62 private final NotificationFilter mNotificationFilter; 63 private final AmbientDisplayConfiguration mAmbientDisplayConfiguration; 64 65 private final Context mContext; 66 private final PowerManager mPowerManager; 67 private final IDreamManager mDreamManager; 68 private final BatteryController mBatteryController; 69 70 private NotificationPresenter mPresenter; 71 private HeadsUpManager mHeadsUpManager; 72 private HeadsUpSuppressor mHeadsUpSuppressor; 73 74 private ContentObserver mHeadsUpObserver; 75 @VisibleForTesting 76 protected boolean mUseHeadsUp = false; 77 private boolean mDisableNotificationAlerts; 78 79 @Inject NotificationInterruptionStateProvider(Context context, NotificationFilter filter, StatusBarStateController stateController, BatteryController batteryController)80 public NotificationInterruptionStateProvider(Context context, NotificationFilter filter, 81 StatusBarStateController stateController, BatteryController batteryController) { 82 this(context, 83 (PowerManager) context.getSystemService(Context.POWER_SERVICE), 84 IDreamManager.Stub.asInterface( 85 ServiceManager.checkService(DreamService.DREAM_SERVICE)), 86 new AmbientDisplayConfiguration(context), 87 filter, 88 batteryController, 89 stateController); 90 } 91 92 @VisibleForTesting NotificationInterruptionStateProvider( Context context, PowerManager powerManager, IDreamManager dreamManager, AmbientDisplayConfiguration ambientDisplayConfiguration, NotificationFilter notificationFilter, BatteryController batteryController, StatusBarStateController statusBarStateController)93 protected NotificationInterruptionStateProvider( 94 Context context, 95 PowerManager powerManager, 96 IDreamManager dreamManager, 97 AmbientDisplayConfiguration ambientDisplayConfiguration, 98 NotificationFilter notificationFilter, 99 BatteryController batteryController, 100 StatusBarStateController statusBarStateController) { 101 mContext = context; 102 mPowerManager = powerManager; 103 mDreamManager = dreamManager; 104 mBatteryController = batteryController; 105 mAmbientDisplayConfiguration = ambientDisplayConfiguration; 106 mNotificationFilter = notificationFilter; 107 mStatusBarStateController = statusBarStateController; 108 } 109 110 /** Sets up late-binding dependencies for this component. */ setUpWithPresenter( NotificationPresenter notificationPresenter, HeadsUpManager headsUpManager, HeadsUpSuppressor headsUpSuppressor)111 public void setUpWithPresenter( 112 NotificationPresenter notificationPresenter, 113 HeadsUpManager headsUpManager, 114 HeadsUpSuppressor headsUpSuppressor) { 115 setUpWithPresenter(notificationPresenter, headsUpManager, headsUpSuppressor, 116 new ContentObserver(Dependency.get(Dependency.MAIN_HANDLER)) { 117 @Override 118 public void onChange(boolean selfChange) { 119 boolean wasUsing = mUseHeadsUp; 120 mUseHeadsUp = ENABLE_HEADS_UP && !mDisableNotificationAlerts 121 && Settings.Global.HEADS_UP_OFF != Settings.Global.getInt( 122 mContext.getContentResolver(), 123 Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED, 124 Settings.Global.HEADS_UP_OFF); 125 Log.d(TAG, "heads up is " + (mUseHeadsUp ? "enabled" : "disabled")); 126 if (wasUsing != mUseHeadsUp) { 127 if (!mUseHeadsUp) { 128 Log.d(TAG, 129 "dismissing any existing heads up notification on disable" 130 + " event"); 131 mHeadsUpManager.releaseAllImmediately(); 132 } 133 } 134 } 135 }); 136 } 137 138 /** Sets up late-binding dependencies for this component. */ setUpWithPresenter( NotificationPresenter notificationPresenter, HeadsUpManager headsUpManager, HeadsUpSuppressor headsUpSuppressor, ContentObserver observer)139 public void setUpWithPresenter( 140 NotificationPresenter notificationPresenter, 141 HeadsUpManager headsUpManager, 142 HeadsUpSuppressor headsUpSuppressor, 143 ContentObserver observer) { 144 mPresenter = notificationPresenter; 145 mHeadsUpManager = headsUpManager; 146 mHeadsUpSuppressor = headsUpSuppressor; 147 mHeadsUpObserver = observer; 148 149 if (ENABLE_HEADS_UP) { 150 mContext.getContentResolver().registerContentObserver( 151 Settings.Global.getUriFor(Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED), 152 true, 153 mHeadsUpObserver); 154 mContext.getContentResolver().registerContentObserver( 155 Settings.Global.getUriFor(SETTING_HEADS_UP_TICKER), true, 156 mHeadsUpObserver); 157 } 158 mHeadsUpObserver.onChange(true); // set up 159 } 160 161 /** 162 * Whether the notification should appear as a bubble with a fly-out on top of the screen. 163 * 164 * @param entry the entry to check 165 * @return true if the entry should bubble up, false otherwise 166 */ shouldBubbleUp(NotificationEntry entry)167 public boolean shouldBubbleUp(NotificationEntry entry) { 168 final StatusBarNotification sbn = entry.notification; 169 170 if (!canAlertCommon(entry)) { 171 return false; 172 } 173 174 if (!canAlertAwakeCommon(entry)) { 175 return false; 176 } 177 178 if (!entry.canBubble) { 179 if (DEBUG) { 180 Log.d(TAG, "No bubble up: not allowed to bubble: " + sbn.getKey()); 181 } 182 return false; 183 } 184 185 if (!entry.isBubble()) { 186 if (DEBUG) { 187 Log.d(TAG, "No bubble up: notification " + sbn.getKey() 188 + " is bubble? " + entry.isBubble()); 189 } 190 return false; 191 } 192 193 final Notification n = sbn.getNotification(); 194 if (n.getBubbleMetadata() == null || n.getBubbleMetadata().getIntent() == null) { 195 if (DEBUG) { 196 Log.d(TAG, "No bubble up: notification: " + sbn.getKey() 197 + " doesn't have valid metadata"); 198 } 199 return false; 200 } 201 202 return true; 203 } 204 205 /** 206 * Whether the notification should peek in from the top and alert the user. 207 * 208 * @param entry the entry to check 209 * @return true if the entry should heads up, false otherwise 210 */ shouldHeadsUp(NotificationEntry entry)211 public boolean shouldHeadsUp(NotificationEntry entry) { 212 if (mStatusBarStateController.isDozing()) { 213 return shouldHeadsUpWhenDozing(entry); 214 } else { 215 return shouldHeadsUpWhenAwake(entry); 216 } 217 } 218 shouldHeadsUpWhenAwake(NotificationEntry entry)219 private boolean shouldHeadsUpWhenAwake(NotificationEntry entry) { 220 StatusBarNotification sbn = entry.notification; 221 222 if (!mUseHeadsUp) { 223 if (DEBUG_HEADS_UP) { 224 Log.d(TAG, "No heads up: no huns"); 225 } 226 return false; 227 } 228 229 if (!canAlertCommon(entry)) { 230 return false; 231 } 232 233 if (!canAlertAwakeCommon(entry)) { 234 return false; 235 } 236 237 boolean inShade = mStatusBarStateController.getState() == SHADE; 238 if (entry.isBubble() && inShade) { 239 if (DEBUG_HEADS_UP) { 240 Log.d(TAG, "No heads up: in unlocked shade where notification is shown as a " 241 + "bubble: " + sbn.getKey()); 242 } 243 return false; 244 } 245 246 if (entry.shouldSuppressPeek()) { 247 if (DEBUG_HEADS_UP) { 248 Log.d(TAG, "No heads up: suppressed by DND: " + sbn.getKey()); 249 } 250 return false; 251 } 252 253 if (entry.importance < NotificationManager.IMPORTANCE_HIGH) { 254 if (DEBUG_HEADS_UP) { 255 Log.d(TAG, "No heads up: unimportant notification: " + sbn.getKey()); 256 } 257 return false; 258 } 259 260 boolean isDreaming = false; 261 try { 262 isDreaming = mDreamManager.isDreaming(); 263 } catch (RemoteException e) { 264 Log.e(TAG, "Failed to query dream manager.", e); 265 } 266 boolean inUse = mPowerManager.isScreenOn() && !isDreaming; 267 268 if (!inUse) { 269 if (DEBUG_HEADS_UP) { 270 Log.d(TAG, "No heads up: not in use: " + sbn.getKey()); 271 } 272 return false; 273 } 274 275 if (!mHeadsUpSuppressor.canHeadsUp(entry, sbn)) { 276 if (DEBUG_HEADS_UP) { 277 Log.d(TAG, "No heads up: aborted by suppressor: " + sbn.getKey()); 278 } 279 return false; 280 } 281 282 return true; 283 } 284 285 /** 286 * Whether or not the notification should "pulse" on the user's display when the phone is 287 * dozing. This displays the ambient view of the notification. 288 * 289 * @param entry the entry to check 290 * @return true if the entry should ambient pulse, false otherwise 291 */ shouldHeadsUpWhenDozing(NotificationEntry entry)292 private boolean shouldHeadsUpWhenDozing(NotificationEntry entry) { 293 StatusBarNotification sbn = entry.notification; 294 295 if (!mAmbientDisplayConfiguration.pulseOnNotificationEnabled(UserHandle.USER_CURRENT)) { 296 if (DEBUG_HEADS_UP) { 297 Log.d(TAG, "No pulsing: disabled by setting: " + sbn.getKey()); 298 } 299 return false; 300 } 301 302 if (mBatteryController.isAodPowerSave()) { 303 if (DEBUG_HEADS_UP) { 304 Log.d(TAG, "No pulsing: disabled by battery saver: " + sbn.getKey()); 305 } 306 return false; 307 } 308 309 if (!canAlertCommon(entry)) { 310 if (DEBUG_HEADS_UP) { 311 Log.d(TAG, "No pulsing: notification shouldn't alert: " + sbn.getKey()); 312 } 313 return false; 314 } 315 316 if (entry.shouldSuppressAmbient()) { 317 if (DEBUG_HEADS_UP) { 318 Log.d(TAG, "No pulsing: ambient effect suppressed: " + sbn.getKey()); 319 } 320 return false; 321 } 322 323 if (entry.importance < NotificationManager.IMPORTANCE_DEFAULT) { 324 if (DEBUG_HEADS_UP) { 325 Log.d(TAG, "No pulsing: not important enough: " + sbn.getKey()); 326 } 327 return false; 328 } 329 return true; 330 } 331 332 /** 333 * Common checks between regular & AOD heads up and bubbles. 334 * 335 * @param entry the entry to check 336 * @return true if these checks pass, false if the notification should not alert 337 */ 338 @VisibleForTesting canAlertCommon(NotificationEntry entry)339 public boolean canAlertCommon(NotificationEntry entry) { 340 StatusBarNotification sbn = entry.notification; 341 342 if (mNotificationFilter.shouldFilterOut(entry)) { 343 if (DEBUG || DEBUG_HEADS_UP) { 344 Log.d(TAG, "No alerting: filtered notification: " + sbn.getKey()); 345 } 346 return false; 347 } 348 349 // Don't alert notifications that are suppressed due to group alert behavior 350 if (sbn.isGroup() && sbn.getNotification().suppressAlertingDueToGrouping()) { 351 if (DEBUG || DEBUG_HEADS_UP) { 352 Log.d(TAG, "No alerting: suppressed due to group alert behavior"); 353 } 354 return false; 355 } 356 return true; 357 } 358 359 /** 360 * Common checks between alerts that occur while the device is awake (heads up & bubbles). 361 * 362 * @param entry the entry to check 363 * @return true if these checks pass, false if the notification should not alert 364 */ 365 @VisibleForTesting canAlertAwakeCommon(NotificationEntry entry)366 public boolean canAlertAwakeCommon(NotificationEntry entry) { 367 StatusBarNotification sbn = entry.notification; 368 369 if (mPresenter.isDeviceInVrMode()) { 370 if (DEBUG_HEADS_UP) { 371 Log.d(TAG, "No alerting: no huns or vr mode"); 372 } 373 return false; 374 } 375 376 if (isSnoozedPackage(sbn)) { 377 if (DEBUG_HEADS_UP) { 378 Log.d(TAG, "No alerting: snoozed package: " + sbn.getKey()); 379 } 380 return false; 381 } 382 383 if (entry.hasJustLaunchedFullScreenIntent()) { 384 if (DEBUG_HEADS_UP) { 385 Log.d(TAG, "No alerting: recent fullscreen: " + sbn.getKey()); 386 } 387 return false; 388 } 389 390 return true; 391 } 392 isSnoozedPackage(StatusBarNotification sbn)393 private boolean isSnoozedPackage(StatusBarNotification sbn) { 394 return mHeadsUpManager.isSnoozed(sbn.getPackageName()); 395 } 396 397 /** Sets whether to disable all alerts. */ setDisableNotificationAlerts(boolean disableNotificationAlerts)398 public void setDisableNotificationAlerts(boolean disableNotificationAlerts) { 399 mDisableNotificationAlerts = disableNotificationAlerts; 400 mHeadsUpObserver.onChange(true); 401 } 402 403 /** Whether all alerts are disabled. */ 404 @VisibleForTesting areNotificationAlertsDisabled()405 public boolean areNotificationAlertsDisabled() { 406 return mDisableNotificationAlerts; 407 } 408 409 /** Whether HUNs should be used. */ 410 @VisibleForTesting getUseHeadsUp()411 public boolean getUseHeadsUp() { 412 return mUseHeadsUp; 413 } 414 getPresenter()415 protected NotificationPresenter getPresenter() { 416 return mPresenter; 417 } 418 419 /** 420 * When an entry was added, should we launch its fullscreen intent? Examples are Alarms or 421 * incoming calls. 422 * 423 * @param entry the entry that was added 424 * @return {@code true} if we should launch the full screen intent 425 */ shouldLaunchFullScreenIntentWhenAdded(NotificationEntry entry)426 public boolean shouldLaunchFullScreenIntentWhenAdded(NotificationEntry entry) { 427 return entry.notification.getNotification().fullScreenIntent != null 428 && (!shouldHeadsUp(entry) 429 || mStatusBarStateController.getState() == StatusBarState.KEYGUARD); 430 } 431 432 /** A component which can suppress heads-up notifications due to the overall state of the UI. */ 433 public interface HeadsUpSuppressor { 434 /** 435 * Returns false if the provided notification is ineligible for heads-up according to this 436 * component. 437 * 438 * @param entry entry of the notification that might be heads upped 439 * @param sbn notification that might be heads upped 440 * @return false if the notification can not be heads upped 441 */ canHeadsUp(NotificationEntry entry, StatusBarNotification sbn)442 boolean canHeadsUp(NotificationEntry entry, StatusBarNotification sbn); 443 444 } 445 446 } 447