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