1 /* 2 * Copyright (C) 2015 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.policy; 18 19 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.app.Notification; 24 import android.content.Context; 25 import android.content.res.Resources; 26 import android.database.ContentObserver; 27 import android.os.Handler; 28 import android.provider.Settings; 29 import android.util.ArrayMap; 30 import android.view.accessibility.AccessibilityManager; 31 32 import com.android.internal.logging.MetricsLogger; 33 import com.android.internal.logging.UiEvent; 34 import com.android.internal.logging.UiEventLogger; 35 import com.android.systemui.EventLogTags; 36 import com.android.systemui.R; 37 import com.android.systemui.dagger.qualifiers.Main; 38 import com.android.systemui.statusbar.AlertingNotificationManager; 39 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 40 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; 41 import com.android.systemui.util.ListenerSet; 42 43 import java.io.PrintWriter; 44 45 /** 46 * A manager which handles heads up notifications which is a special mode where 47 * they simply peek from the top of the screen. 48 */ 49 public abstract class HeadsUpManager extends AlertingNotificationManager { 50 private static final String TAG = "HeadsUpManager"; 51 private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms"; 52 53 protected final ListenerSet<OnHeadsUpChangedListener> mListeners = new ListenerSet<>(); 54 55 protected final Context mContext; 56 57 protected int mTouchAcceptanceDelay; 58 protected int mSnoozeLengthMs; 59 protected boolean mHasPinnedNotification; 60 protected int mUser; 61 62 private final ArrayMap<String, Long> mSnoozedPackages; 63 private final AccessibilityManagerWrapper mAccessibilityMgr; 64 65 private final UiEventLogger mUiEventLogger; 66 67 /** 68 * Enum entry for notification peek logged from this class. 69 */ 70 enum NotificationPeekEvent implements UiEventLogger.UiEventEnum { 71 @UiEvent(doc = "Heads-up notification peeked on screen.") 72 NOTIFICATION_PEEK(801); 73 74 private final int mId; NotificationPeekEvent(int id)75 NotificationPeekEvent(int id) { 76 mId = id; 77 } getId()78 @Override public int getId() { 79 return mId; 80 } 81 } 82 HeadsUpManager(@onNull final Context context, HeadsUpManagerLogger logger, @Main Handler handler, AccessibilityManagerWrapper accessibilityManagerWrapper, UiEventLogger uiEventLogger)83 public HeadsUpManager(@NonNull final Context context, 84 HeadsUpManagerLogger logger, 85 @Main Handler handler, 86 AccessibilityManagerWrapper accessibilityManagerWrapper, 87 UiEventLogger uiEventLogger) { 88 super(logger, handler); 89 mContext = context; 90 mAccessibilityMgr = accessibilityManagerWrapper; 91 mUiEventLogger = uiEventLogger; 92 Resources resources = context.getResources(); 93 mMinimumDisplayTime = resources.getInteger(R.integer.heads_up_notification_minimum_time); 94 mAutoDismissNotificationDecay = resources.getInteger(R.integer.heads_up_notification_decay); 95 mTouchAcceptanceDelay = resources.getInteger(R.integer.touch_acceptance_delay); 96 mSnoozedPackages = new ArrayMap<>(); 97 int defaultSnoozeLengthMs = 98 resources.getInteger(R.integer.heads_up_default_snooze_length_ms); 99 100 mSnoozeLengthMs = Settings.Global.getInt(context.getContentResolver(), 101 SETTING_HEADS_UP_SNOOZE_LENGTH_MS, defaultSnoozeLengthMs); 102 ContentObserver settingsObserver = new ContentObserver(mHandler) { 103 @Override 104 public void onChange(boolean selfChange) { 105 final int packageSnoozeLengthMs = Settings.Global.getInt( 106 context.getContentResolver(), SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1); 107 if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) { 108 mSnoozeLengthMs = packageSnoozeLengthMs; 109 mLogger.logSnoozeLengthChange(packageSnoozeLengthMs); 110 } 111 } 112 }; 113 context.getContentResolver().registerContentObserver( 114 Settings.Global.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), false, 115 settingsObserver); 116 } 117 118 /** 119 * Adds an OnHeadUpChangedListener to observe events. 120 */ addListener(@onNull OnHeadsUpChangedListener listener)121 public void addListener(@NonNull OnHeadsUpChangedListener listener) { 122 mListeners.addIfAbsent(listener); 123 } 124 125 /** 126 * Removes the OnHeadUpChangedListener from the observer list. 127 */ removeListener(@onNull OnHeadsUpChangedListener listener)128 public void removeListener(@NonNull OnHeadsUpChangedListener listener) { 129 mListeners.remove(listener); 130 } 131 updateNotification(@onNull String key, boolean alert)132 public void updateNotification(@NonNull String key, boolean alert) { 133 super.updateNotification(key, alert); 134 HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); 135 if (alert && headsUpEntry != null) { 136 setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUpEntry.mEntry)); 137 } 138 } 139 shouldHeadsUpBecomePinned(@onNull NotificationEntry entry)140 protected boolean shouldHeadsUpBecomePinned(@NonNull NotificationEntry entry) { 141 return hasFullScreenIntent(entry); 142 } 143 hasFullScreenIntent(@onNull NotificationEntry entry)144 protected boolean hasFullScreenIntent(@NonNull NotificationEntry entry) { 145 return entry.getSbn().getNotification().fullScreenIntent != null; 146 } 147 setEntryPinned( @onNull HeadsUpManager.HeadsUpEntry headsUpEntry, boolean isPinned)148 protected void setEntryPinned( 149 @NonNull HeadsUpManager.HeadsUpEntry headsUpEntry, boolean isPinned) { 150 mLogger.logSetEntryPinned(headsUpEntry.mEntry, isPinned); 151 NotificationEntry entry = headsUpEntry.mEntry; 152 if (entry.isRowPinned() != isPinned) { 153 entry.setRowPinned(isPinned); 154 updatePinnedMode(); 155 if (isPinned && entry.getSbn() != null) { 156 mUiEventLogger.logWithInstanceId( 157 NotificationPeekEvent.NOTIFICATION_PEEK, entry.getSbn().getUid(), 158 entry.getSbn().getPackageName(), entry.getSbn().getInstanceId()); 159 } 160 for (OnHeadsUpChangedListener listener : mListeners) { 161 if (isPinned) { 162 listener.onHeadsUpPinned(entry); 163 } else { 164 listener.onHeadsUpUnPinned(entry); 165 } 166 } 167 } 168 } 169 getContentFlag()170 public @InflationFlag int getContentFlag() { 171 return FLAG_CONTENT_VIEW_HEADS_UP; 172 } 173 174 @Override onAlertEntryAdded(AlertEntry alertEntry)175 protected void onAlertEntryAdded(AlertEntry alertEntry) { 176 NotificationEntry entry = alertEntry.mEntry; 177 entry.setHeadsUp(true); 178 setEntryPinned((HeadsUpEntry) alertEntry, shouldHeadsUpBecomePinned(entry)); 179 EventLogTags.writeSysuiHeadsUpStatus(entry.getKey(), 1 /* visible */); 180 for (OnHeadsUpChangedListener listener : mListeners) { 181 listener.onHeadsUpStateChanged(entry, true); 182 } 183 } 184 185 @Override onAlertEntryRemoved(AlertEntry alertEntry)186 protected void onAlertEntryRemoved(AlertEntry alertEntry) { 187 NotificationEntry entry = alertEntry.mEntry; 188 entry.setHeadsUp(false); 189 setEntryPinned((HeadsUpEntry) alertEntry, false /* isPinned */); 190 EventLogTags.writeSysuiHeadsUpStatus(entry.getKey(), 0 /* visible */); 191 mLogger.logNotificationActuallyRemoved(entry); 192 for (OnHeadsUpChangedListener listener : mListeners) { 193 listener.onHeadsUpStateChanged(entry, false); 194 } 195 } 196 updatePinnedMode()197 protected void updatePinnedMode() { 198 boolean hasPinnedNotification = hasPinnedNotificationInternal(); 199 if (hasPinnedNotification == mHasPinnedNotification) { 200 return; 201 } 202 mLogger.logUpdatePinnedMode(hasPinnedNotification); 203 mHasPinnedNotification = hasPinnedNotification; 204 if (mHasPinnedNotification) { 205 MetricsLogger.count(mContext, "note_peek", 1); 206 } 207 for (OnHeadsUpChangedListener listener : mListeners) { 208 listener.onHeadsUpPinnedModeChanged(hasPinnedNotification); 209 } 210 } 211 212 /** 213 * Returns if the given notification is snoozed or not. 214 */ isSnoozed(@onNull String packageName)215 public boolean isSnoozed(@NonNull String packageName) { 216 final String key = snoozeKey(packageName, mUser); 217 Long snoozedUntil = mSnoozedPackages.get(key); 218 if (snoozedUntil != null) { 219 if (snoozedUntil > mClock.currentTimeMillis()) { 220 mLogger.logIsSnoozedReturned(key); 221 return true; 222 } 223 mLogger.logPackageUnsnoozed(key); 224 mSnoozedPackages.remove(key); 225 } 226 return false; 227 } 228 229 /** 230 * Snoozes all current Heads Up Notifications. 231 */ snooze()232 public void snooze() { 233 for (String key : mAlertEntries.keySet()) { 234 AlertEntry entry = getHeadsUpEntry(key); 235 String packageName = entry.mEntry.getSbn().getPackageName(); 236 String snoozeKey = snoozeKey(packageName, mUser); 237 mLogger.logPackageSnoozed(snoozeKey); 238 mSnoozedPackages.put(snoozeKey, mClock.currentTimeMillis() + mSnoozeLengthMs); 239 } 240 } 241 242 @NonNull snoozeKey(@onNull String packageName, int user)243 private static String snoozeKey(@NonNull String packageName, int user) { 244 return user + "," + packageName; 245 } 246 247 @Nullable getHeadsUpEntry(@onNull String key)248 protected HeadsUpEntry getHeadsUpEntry(@NonNull String key) { 249 return (HeadsUpEntry) mAlertEntries.get(key); 250 } 251 252 /** 253 * Returns the top Heads Up Notification, which appears to show at first. 254 */ 255 @Nullable getTopEntry()256 public NotificationEntry getTopEntry() { 257 HeadsUpEntry topEntry = getTopHeadsUpEntry(); 258 return (topEntry != null) ? topEntry.mEntry : null; 259 } 260 261 @Nullable getTopHeadsUpEntry()262 protected HeadsUpEntry getTopHeadsUpEntry() { 263 if (mAlertEntries.isEmpty()) { 264 return null; 265 } 266 HeadsUpEntry topEntry = null; 267 for (AlertEntry entry: mAlertEntries.values()) { 268 if (topEntry == null || entry.compareTo(topEntry) < 0) { 269 topEntry = (HeadsUpEntry) entry; 270 } 271 } 272 return topEntry; 273 } 274 275 /** 276 * Sets the current user. 277 */ setUser(int user)278 public void setUser(int user) { 279 mUser = user; 280 } 281 dump(@onNull PrintWriter pw, @NonNull String[] args)282 public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { 283 pw.println("HeadsUpManager state:"); 284 dumpInternal(pw, args); 285 } 286 dumpInternal(@onNull PrintWriter pw, @NonNull String[] args)287 protected void dumpInternal(@NonNull PrintWriter pw, @NonNull String[] args) { 288 pw.print(" mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay); 289 pw.print(" mSnoozeLengthMs="); pw.println(mSnoozeLengthMs); 290 pw.print(" now="); pw.println(mClock.currentTimeMillis()); 291 pw.print(" mUser="); pw.println(mUser); 292 for (AlertEntry entry: mAlertEntries.values()) { 293 pw.print(" HeadsUpEntry="); pw.println(entry.mEntry); 294 } 295 int N = mSnoozedPackages.size(); 296 pw.println(" snoozed packages: " + N); 297 for (int i = 0; i < N; i++) { 298 pw.print(" "); pw.print(mSnoozedPackages.valueAt(i)); 299 pw.print(", "); pw.println(mSnoozedPackages.keyAt(i)); 300 } 301 } 302 303 /** 304 * Returns if there are any pinned Heads Up Notifications or not. 305 */ hasPinnedHeadsUp()306 public boolean hasPinnedHeadsUp() { 307 return mHasPinnedNotification; 308 } 309 hasPinnedNotificationInternal()310 private boolean hasPinnedNotificationInternal() { 311 for (String key : mAlertEntries.keySet()) { 312 AlertEntry entry = getHeadsUpEntry(key); 313 if (entry.mEntry.isRowPinned()) { 314 return true; 315 } 316 } 317 return false; 318 } 319 320 /** 321 * Unpins all pinned Heads Up Notifications. 322 * @param userUnPinned The unpinned action is trigger by user real operation. 323 */ unpinAll(boolean userUnPinned)324 public void unpinAll(boolean userUnPinned) { 325 for (String key : mAlertEntries.keySet()) { 326 HeadsUpEntry entry = getHeadsUpEntry(key); 327 setEntryPinned(entry, false /* isPinned */); 328 // maybe it got un sticky 329 entry.updateEntry(false /* updatePostTime */); 330 331 // when the user unpinned all of HUNs by moving one HUN, all of HUNs should not stay 332 // on the screen. 333 if (userUnPinned && entry.mEntry != null) { 334 if (entry.mEntry.mustStayOnScreen()) { 335 entry.mEntry.setHeadsUpIsVisible(); 336 } 337 } 338 } 339 } 340 341 /** 342 * Returns the value of the tracking-heads-up flag. See the doc of {@code setTrackingHeadsUp} as 343 * well. 344 */ isTrackingHeadsUp()345 public boolean isTrackingHeadsUp() { 346 // Might be implemented in subclass. 347 return false; 348 } 349 350 /** 351 * Compare two entries and decide how they should be ranked. 352 * 353 * @return -1 if the first argument should be ranked higher than the second, 1 if the second 354 * one should be ranked higher and 0 if they are equal. 355 */ compare(@ullable NotificationEntry a, @Nullable NotificationEntry b)356 public int compare(@Nullable NotificationEntry a, @Nullable NotificationEntry b) { 357 if (a == null || b == null) { 358 return Boolean.compare(a == null, b == null); 359 } 360 AlertEntry aEntry = getHeadsUpEntry(a.getKey()); 361 AlertEntry bEntry = getHeadsUpEntry(b.getKey()); 362 if (aEntry == null || bEntry == null) { 363 return Boolean.compare(aEntry == null, bEntry == null); 364 } 365 return aEntry.compareTo(bEntry); 366 } 367 368 /** 369 * Set an entry to be expanded and therefore stick in the heads up area if it's pinned 370 * until it's collapsed again. 371 */ setExpanded(@onNull NotificationEntry entry, boolean expanded)372 public void setExpanded(@NonNull NotificationEntry entry, boolean expanded) { 373 HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey()); 374 if (headsUpEntry != null && entry.isRowPinned()) { 375 headsUpEntry.setExpanded(expanded); 376 } 377 } 378 379 @NonNull 380 @Override createAlertEntry()381 protected HeadsUpEntry createAlertEntry() { 382 return new HeadsUpEntry(); 383 } 384 onDensityOrFontScaleChanged()385 public void onDensityOrFontScaleChanged() { 386 } 387 388 /** 389 * Determines if the notification is for a critical call that must display on top of an active 390 * input notification. 391 * The call isOngoing check is for a special case of incoming calls (see b/164291424). 392 */ isCriticalCallNotif(NotificationEntry entry)393 private static boolean isCriticalCallNotif(NotificationEntry entry) { 394 Notification n = entry.getSbn().getNotification(); 395 boolean isIncomingCall = n.isStyle(Notification.CallStyle.class) && n.extras.getInt( 396 Notification.EXTRA_CALL_TYPE) == Notification.CallStyle.CALL_TYPE_INCOMING; 397 return isIncomingCall || (entry.getSbn().isOngoing() 398 && Notification.CATEGORY_CALL.equals(n.category)); 399 } 400 401 /** 402 * This represents a notification and how long it is in a heads up mode. It also manages its 403 * lifecycle automatically when created. 404 */ 405 protected class HeadsUpEntry extends AlertEntry { 406 public boolean remoteInputActive; 407 protected boolean expanded; 408 409 @Override isSticky()410 public boolean isSticky() { 411 return (mEntry.isRowPinned() && expanded) 412 || remoteInputActive || hasFullScreenIntent(mEntry); 413 } 414 415 @Override compareTo(@onNull AlertEntry alertEntry)416 public int compareTo(@NonNull AlertEntry alertEntry) { 417 HeadsUpEntry headsUpEntry = (HeadsUpEntry) alertEntry; 418 boolean isPinned = mEntry.isRowPinned(); 419 boolean otherPinned = headsUpEntry.mEntry.isRowPinned(); 420 if (isPinned && !otherPinned) { 421 return -1; 422 } else if (!isPinned && otherPinned) { 423 return 1; 424 } 425 boolean selfFullscreen = hasFullScreenIntent(mEntry); 426 boolean otherFullscreen = hasFullScreenIntent(headsUpEntry.mEntry); 427 if (selfFullscreen && !otherFullscreen) { 428 return -1; 429 } else if (!selfFullscreen && otherFullscreen) { 430 return 1; 431 } 432 433 boolean selfCall = isCriticalCallNotif(mEntry); 434 boolean otherCall = isCriticalCallNotif(headsUpEntry.mEntry); 435 436 if (selfCall && !otherCall) { 437 return -1; 438 } else if (!selfCall && otherCall) { 439 return 1; 440 } 441 442 if (remoteInputActive && !headsUpEntry.remoteInputActive) { 443 return -1; 444 } else if (!remoteInputActive && headsUpEntry.remoteInputActive) { 445 return 1; 446 } 447 448 return super.compareTo(headsUpEntry); 449 } 450 setExpanded(boolean expanded)451 public void setExpanded(boolean expanded) { 452 this.expanded = expanded; 453 } 454 455 @Override reset()456 public void reset() { 457 super.reset(); 458 expanded = false; 459 remoteInputActive = false; 460 } 461 462 @Override calculatePostTime()463 protected long calculatePostTime() { 464 // The actual post time will be just after the heads-up really slided in 465 return super.calculatePostTime() + mTouchAcceptanceDelay; 466 } 467 468 @Override calculateFinishTime()469 protected long calculateFinishTime() { 470 return mPostTime + getRecommendedHeadsUpTimeoutMs(mAutoDismissNotificationDecay); 471 } 472 473 /** 474 * Get user-preferred or default timeout duration. The larger one will be returned. 475 * @return milliseconds before auto-dismiss 476 * @param requestedTimeout 477 */ getRecommendedHeadsUpTimeoutMs(int requestedTimeout)478 protected int getRecommendedHeadsUpTimeoutMs(int requestedTimeout) { 479 return mAccessibilityMgr.getRecommendedTimeoutMillis( 480 requestedTimeout, 481 AccessibilityManager.FLAG_CONTENT_CONTROLS 482 | AccessibilityManager.FLAG_CONTENT_ICONS 483 | AccessibilityManager.FLAG_CONTENT_TEXT); 484 } 485 } 486 } 487