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