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.NotificationContentInflater.FLAG_CONTENT_VIEW_HEADS_UP; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.database.ContentObserver; 26 import android.provider.Settings; 27 import android.util.ArrayMap; 28 import android.util.Log; 29 import android.view.accessibility.AccessibilityManager; 30 31 import com.android.internal.logging.MetricsLogger; 32 import com.android.systemui.Dependency; 33 import com.android.systemui.R; 34 import com.android.systemui.statusbar.AlertingNotificationManager; 35 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 36 import com.android.systemui.statusbar.notification.row.NotificationContentInflater.InflationFlag; 37 38 import java.io.FileDescriptor; 39 import java.io.PrintWriter; 40 import java.util.HashSet; 41 42 /** 43 * A manager which handles heads up notifications which is a special mode where 44 * they simply peek from the top of the screen. 45 */ 46 public abstract class HeadsUpManager extends AlertingNotificationManager { 47 private static final String TAG = "HeadsUpManager"; 48 private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms"; 49 50 protected final HashSet<OnHeadsUpChangedListener> mListeners = new HashSet<>(); 51 52 protected final Context mContext; 53 54 protected int mTouchAcceptanceDelay; 55 protected int mSnoozeLengthMs; 56 protected boolean mHasPinnedNotification; 57 protected int mUser; 58 59 private final ArrayMap<String, Long> mSnoozedPackages; 60 private final AccessibilityManagerWrapper mAccessibilityMgr; 61 HeadsUpManager(@onNull final Context context)62 public HeadsUpManager(@NonNull final Context context) { 63 mContext = context; 64 mAccessibilityMgr = Dependency.get(AccessibilityManagerWrapper.class); 65 Resources resources = context.getResources(); 66 mMinimumDisplayTime = resources.getInteger(R.integer.heads_up_notification_minimum_time); 67 mAutoDismissNotificationDecay = resources.getInteger(R.integer.heads_up_notification_decay); 68 mTouchAcceptanceDelay = resources.getInteger(R.integer.touch_acceptance_delay); 69 mSnoozedPackages = new ArrayMap<>(); 70 int defaultSnoozeLengthMs = 71 resources.getInteger(R.integer.heads_up_default_snooze_length_ms); 72 73 mSnoozeLengthMs = Settings.Global.getInt(context.getContentResolver(), 74 SETTING_HEADS_UP_SNOOZE_LENGTH_MS, defaultSnoozeLengthMs); 75 ContentObserver settingsObserver = new ContentObserver(mHandler) { 76 @Override 77 public void onChange(boolean selfChange) { 78 final int packageSnoozeLengthMs = Settings.Global.getInt( 79 context.getContentResolver(), SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1); 80 if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) { 81 mSnoozeLengthMs = packageSnoozeLengthMs; 82 if (Log.isLoggable(TAG, Log.VERBOSE)) { 83 Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs); 84 } 85 } 86 } 87 }; 88 context.getContentResolver().registerContentObserver( 89 Settings.Global.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), false, 90 settingsObserver); 91 } 92 93 /** 94 * Adds an OnHeadUpChangedListener to observe events. 95 */ addListener(@onNull OnHeadsUpChangedListener listener)96 public void addListener(@NonNull OnHeadsUpChangedListener listener) { 97 mListeners.add(listener); 98 } 99 100 /** 101 * Removes the OnHeadUpChangedListener from the observer list. 102 */ removeListener(@onNull OnHeadsUpChangedListener listener)103 public void removeListener(@NonNull OnHeadsUpChangedListener listener) { 104 mListeners.remove(listener); 105 } 106 updateNotification(@onNull String key, boolean alert)107 public void updateNotification(@NonNull String key, boolean alert) { 108 super.updateNotification(key, alert); 109 AlertEntry alertEntry = getHeadsUpEntry(key); 110 if (alert && alertEntry != null) { 111 setEntryPinned((HeadsUpEntry) alertEntry, shouldHeadsUpBecomePinned(alertEntry.mEntry)); 112 } 113 } 114 shouldHeadsUpBecomePinned(@onNull NotificationEntry entry)115 protected boolean shouldHeadsUpBecomePinned(@NonNull NotificationEntry entry) { 116 return hasFullScreenIntent(entry); 117 } 118 hasFullScreenIntent(@onNull NotificationEntry entry)119 protected boolean hasFullScreenIntent(@NonNull NotificationEntry entry) { 120 return entry.notification.getNotification().fullScreenIntent != null; 121 } 122 setEntryPinned( @onNull HeadsUpManager.HeadsUpEntry headsUpEntry, boolean isPinned)123 protected void setEntryPinned( 124 @NonNull HeadsUpManager.HeadsUpEntry headsUpEntry, boolean isPinned) { 125 if (Log.isLoggable(TAG, Log.VERBOSE)) { 126 Log.v(TAG, "setEntryPinned: " + isPinned); 127 } 128 NotificationEntry entry = headsUpEntry.mEntry; 129 if (entry.isRowPinned() != isPinned) { 130 entry.setRowPinned(isPinned); 131 updatePinnedMode(); 132 for (OnHeadsUpChangedListener listener : mListeners) { 133 if (isPinned) { 134 listener.onHeadsUpPinned(entry); 135 } else { 136 listener.onHeadsUpUnPinned(entry); 137 } 138 } 139 } 140 } 141 getContentFlag()142 public @InflationFlag int getContentFlag() { 143 return FLAG_CONTENT_VIEW_HEADS_UP; 144 } 145 146 @Override onAlertEntryAdded(AlertEntry alertEntry)147 protected void onAlertEntryAdded(AlertEntry alertEntry) { 148 NotificationEntry entry = alertEntry.mEntry; 149 entry.setHeadsUp(true); 150 setEntryPinned((HeadsUpEntry) alertEntry, shouldHeadsUpBecomePinned(entry)); 151 for (OnHeadsUpChangedListener listener : mListeners) { 152 listener.onHeadsUpStateChanged(entry, true); 153 } 154 } 155 156 @Override onAlertEntryRemoved(AlertEntry alertEntry)157 protected void onAlertEntryRemoved(AlertEntry alertEntry) { 158 NotificationEntry entry = alertEntry.mEntry; 159 entry.setHeadsUp(false); 160 setEntryPinned((HeadsUpEntry) alertEntry, false /* isPinned */); 161 for (OnHeadsUpChangedListener listener : mListeners) { 162 listener.onHeadsUpStateChanged(entry, false); 163 } 164 entry.freeContentViewWhenSafe(FLAG_CONTENT_VIEW_HEADS_UP); 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.notification.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.key); 332 AlertEntry bEntry = getHeadsUpEntry(b.key); 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.key); 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 363 /** 364 * This represents a notification and how long it is in a heads up mode. It also manages its 365 * lifecycle automatically when created. 366 */ 367 protected class HeadsUpEntry extends AlertEntry { 368 public boolean remoteInputActive; 369 protected boolean expanded; 370 371 @Override isSticky()372 protected boolean isSticky() { 373 return (mEntry.isRowPinned() && expanded) 374 || remoteInputActive || hasFullScreenIntent(mEntry); 375 } 376 377 @Override compareTo(@onNull AlertEntry alertEntry)378 public int compareTo(@NonNull AlertEntry alertEntry) { 379 HeadsUpEntry headsUpEntry = (HeadsUpEntry) alertEntry; 380 boolean isPinned = mEntry.isRowPinned(); 381 boolean otherPinned = headsUpEntry.mEntry.isRowPinned(); 382 if (isPinned && !otherPinned) { 383 return -1; 384 } else if (!isPinned && otherPinned) { 385 return 1; 386 } 387 boolean selfFullscreen = hasFullScreenIntent(mEntry); 388 boolean otherFullscreen = hasFullScreenIntent(headsUpEntry.mEntry); 389 if (selfFullscreen && !otherFullscreen) { 390 return -1; 391 } else if (!selfFullscreen && otherFullscreen) { 392 return 1; 393 } 394 395 if (remoteInputActive && !headsUpEntry.remoteInputActive) { 396 return -1; 397 } else if (!remoteInputActive && headsUpEntry.remoteInputActive) { 398 return 1; 399 } 400 401 return super.compareTo(headsUpEntry); 402 } 403 setExpanded(boolean expanded)404 public void setExpanded(boolean expanded) { 405 this.expanded = expanded; 406 } 407 408 @Override reset()409 public void reset() { 410 super.reset(); 411 expanded = false; 412 remoteInputActive = false; 413 } 414 415 @Override calculatePostTime()416 protected long calculatePostTime() { 417 // The actual post time will be just after the heads-up really slided in 418 return super.calculatePostTime() + mTouchAcceptanceDelay; 419 } 420 421 @Override calculateFinishTime()422 protected long calculateFinishTime() { 423 return mPostTime + getRecommendedHeadsUpTimeoutMs(mAutoDismissNotificationDecay); 424 } 425 426 /** 427 * Get user-preferred or default timeout duration. The larger one will be returned. 428 * @return milliseconds before auto-dismiss 429 * @param requestedTimeout 430 */ getRecommendedHeadsUpTimeoutMs(int requestedTimeout)431 protected int getRecommendedHeadsUpTimeoutMs(int requestedTimeout) { 432 return mAccessibilityMgr.getRecommendedTimeoutMillis( 433 requestedTimeout, 434 AccessibilityManager.FLAG_CONTENT_CONTROLS 435 | AccessibilityManager.FLAG_CONTENT_ICONS 436 | AccessibilityManager.FLAG_CONTENT_TEXT); 437 } 438 } 439 } 440