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