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.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.NotificationRowContentBinder.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.getSbn().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 } 165 updatePinnedMode()166 protected void updatePinnedMode() { 167 boolean hasPinnedNotification = hasPinnedNotificationInternal(); 168 if (hasPinnedNotification == mHasPinnedNotification) { 169 return; 170 } 171 if (Log.isLoggable(TAG, Log.VERBOSE)) { 172 Log.v(TAG, "Pinned mode changed: " + mHasPinnedNotification + " -> " + 173 hasPinnedNotification); 174 } 175 mHasPinnedNotification = hasPinnedNotification; 176 if (mHasPinnedNotification) { 177 MetricsLogger.count(mContext, "note_peek", 1); 178 } 179 for (OnHeadsUpChangedListener listener : mListeners) { 180 listener.onHeadsUpPinnedModeChanged(hasPinnedNotification); 181 } 182 } 183 184 /** 185 * Returns if the given notification is snoozed or not. 186 */ isSnoozed(@onNull String packageName)187 public boolean isSnoozed(@NonNull String packageName) { 188 final String key = snoozeKey(packageName, mUser); 189 Long snoozedUntil = mSnoozedPackages.get(key); 190 if (snoozedUntil != null) { 191 if (snoozedUntil > mClock.currentTimeMillis()) { 192 if (Log.isLoggable(TAG, Log.VERBOSE)) { 193 Log.v(TAG, key + " snoozed"); 194 } 195 return true; 196 } 197 mSnoozedPackages.remove(packageName); 198 } 199 return false; 200 } 201 202 /** 203 * Snoozes all current Heads Up Notifications. 204 */ snooze()205 public void snooze() { 206 for (String key : mAlertEntries.keySet()) { 207 AlertEntry entry = getHeadsUpEntry(key); 208 String packageName = entry.mEntry.getSbn().getPackageName(); 209 mSnoozedPackages.put(snoozeKey(packageName, mUser), 210 mClock.currentTimeMillis() + mSnoozeLengthMs); 211 } 212 } 213 214 @NonNull snoozeKey(@onNull String packageName, int user)215 private static String snoozeKey(@NonNull String packageName, int user) { 216 return user + "," + packageName; 217 } 218 219 @Nullable getHeadsUpEntry(@onNull String key)220 protected HeadsUpEntry getHeadsUpEntry(@NonNull String key) { 221 return (HeadsUpEntry) mAlertEntries.get(key); 222 } 223 224 /** 225 * Returns the top Heads Up Notification, which appears to show at first. 226 */ 227 @Nullable getTopEntry()228 public NotificationEntry getTopEntry() { 229 HeadsUpEntry topEntry = getTopHeadsUpEntry(); 230 return (topEntry != null) ? topEntry.mEntry : null; 231 } 232 233 @Nullable getTopHeadsUpEntry()234 protected HeadsUpEntry getTopHeadsUpEntry() { 235 if (mAlertEntries.isEmpty()) { 236 return null; 237 } 238 HeadsUpEntry topEntry = null; 239 for (AlertEntry entry: mAlertEntries.values()) { 240 if (topEntry == null || entry.compareTo(topEntry) < 0) { 241 topEntry = (HeadsUpEntry) entry; 242 } 243 } 244 return topEntry; 245 } 246 247 /** 248 * Sets the current user. 249 */ setUser(int user)250 public void setUser(int user) { 251 mUser = user; 252 } 253 dump(@onNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args)254 public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { 255 pw.println("HeadsUpManager state:"); 256 dumpInternal(fd, pw, args); 257 } 258 dumpInternal( @onNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args)259 protected void dumpInternal( 260 @NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { 261 pw.print(" mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay); 262 pw.print(" mSnoozeLengthMs="); pw.println(mSnoozeLengthMs); 263 pw.print(" now="); pw.println(mClock.currentTimeMillis()); 264 pw.print(" mUser="); pw.println(mUser); 265 for (AlertEntry entry: mAlertEntries.values()) { 266 pw.print(" HeadsUpEntry="); pw.println(entry.mEntry); 267 } 268 int N = mSnoozedPackages.size(); 269 pw.println(" snoozed packages: " + N); 270 for (int i = 0; i < N; i++) { 271 pw.print(" "); pw.print(mSnoozedPackages.valueAt(i)); 272 pw.print(", "); pw.println(mSnoozedPackages.keyAt(i)); 273 } 274 } 275 276 /** 277 * Returns if there are any pinned Heads Up Notifications or not. 278 */ hasPinnedHeadsUp()279 public boolean hasPinnedHeadsUp() { 280 return mHasPinnedNotification; 281 } 282 hasPinnedNotificationInternal()283 private boolean hasPinnedNotificationInternal() { 284 for (String key : mAlertEntries.keySet()) { 285 AlertEntry entry = getHeadsUpEntry(key); 286 if (entry.mEntry.isRowPinned()) { 287 return true; 288 } 289 } 290 return false; 291 } 292 293 /** 294 * Unpins all pinned Heads Up Notifications. 295 * @param userUnPinned The unpinned action is trigger by user real operation. 296 */ unpinAll(boolean userUnPinned)297 public void unpinAll(boolean userUnPinned) { 298 for (String key : mAlertEntries.keySet()) { 299 HeadsUpEntry entry = getHeadsUpEntry(key); 300 setEntryPinned(entry, false /* isPinned */); 301 // maybe it got un sticky 302 entry.updateEntry(false /* updatePostTime */); 303 304 // when the user unpinned all of HUNs by moving one HUN, all of HUNs should not stay 305 // on the screen. 306 if (userUnPinned && entry.mEntry != null) { 307 if (entry.mEntry.mustStayOnScreen()) { 308 entry.mEntry.setHeadsUpIsVisible(); 309 } 310 } 311 } 312 } 313 314 /** 315 * Returns the value of the tracking-heads-up flag. See the doc of {@code setTrackingHeadsUp} as 316 * well. 317 */ isTrackingHeadsUp()318 public boolean isTrackingHeadsUp() { 319 // Might be implemented in subclass. 320 return false; 321 } 322 323 /** 324 * Compare two entries and decide how they should be ranked. 325 * 326 * @return -1 if the first argument should be ranked higher than the second, 1 if the second 327 * one should be ranked higher and 0 if they are equal. 328 */ compare(@onNull NotificationEntry a, @NonNull NotificationEntry b)329 public int compare(@NonNull NotificationEntry a, @NonNull NotificationEntry b) { 330 AlertEntry aEntry = getHeadsUpEntry(a.getKey()); 331 AlertEntry bEntry = getHeadsUpEntry(b.getKey()); 332 if (aEntry == null || bEntry == null) { 333 return aEntry == null ? 1 : -1; 334 } 335 return aEntry.compareTo(bEntry); 336 } 337 338 /** 339 * Set an entry to be expanded and therefore stick in the heads up area if it's pinned 340 * until it's collapsed again. 341 */ setExpanded(@onNull NotificationEntry entry, boolean expanded)342 public void setExpanded(@NonNull NotificationEntry entry, boolean expanded) { 343 HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey()); 344 if (headsUpEntry != null && entry.isRowPinned()) { 345 headsUpEntry.setExpanded(expanded); 346 } 347 } 348 349 @NonNull 350 @Override createAlertEntry()351 protected HeadsUpEntry createAlertEntry() { 352 return new HeadsUpEntry(); 353 } 354 onDensityOrFontScaleChanged()355 public void onDensityOrFontScaleChanged() { 356 } 357 isEntryAutoHeadsUpped(String key)358 public boolean isEntryAutoHeadsUpped(String key) { 359 return false; 360 } 361 362 /** 363 * This represents a notification and how long it is in a heads up mode. It also manages its 364 * lifecycle automatically when created. 365 */ 366 protected class HeadsUpEntry extends AlertEntry { 367 public boolean remoteInputActive; 368 protected boolean expanded; 369 370 @Override isSticky()371 public boolean isSticky() { 372 return (mEntry.isRowPinned() && expanded) 373 || remoteInputActive || hasFullScreenIntent(mEntry); 374 } 375 376 @Override compareTo(@onNull AlertEntry alertEntry)377 public int compareTo(@NonNull AlertEntry alertEntry) { 378 HeadsUpEntry headsUpEntry = (HeadsUpEntry) alertEntry; 379 boolean isPinned = mEntry.isRowPinned(); 380 boolean otherPinned = headsUpEntry.mEntry.isRowPinned(); 381 if (isPinned && !otherPinned) { 382 return -1; 383 } else if (!isPinned && otherPinned) { 384 return 1; 385 } 386 boolean selfFullscreen = hasFullScreenIntent(mEntry); 387 boolean otherFullscreen = hasFullScreenIntent(headsUpEntry.mEntry); 388 if (selfFullscreen && !otherFullscreen) { 389 return -1; 390 } else if (!selfFullscreen && otherFullscreen) { 391 return 1; 392 } 393 394 if (remoteInputActive && !headsUpEntry.remoteInputActive) { 395 return -1; 396 } else if (!remoteInputActive && headsUpEntry.remoteInputActive) { 397 return 1; 398 } 399 400 return super.compareTo(headsUpEntry); 401 } 402 setExpanded(boolean expanded)403 public void setExpanded(boolean expanded) { 404 this.expanded = expanded; 405 } 406 407 @Override reset()408 public void reset() { 409 super.reset(); 410 expanded = false; 411 remoteInputActive = false; 412 } 413 414 @Override calculatePostTime()415 protected long calculatePostTime() { 416 // The actual post time will be just after the heads-up really slided in 417 return super.calculatePostTime() + mTouchAcceptanceDelay; 418 } 419 420 @Override calculateFinishTime()421 protected long calculateFinishTime() { 422 return mPostTime + getRecommendedHeadsUpTimeoutMs(mAutoDismissNotificationDecay); 423 } 424 425 /** 426 * Get user-preferred or default timeout duration. The larger one will be returned. 427 * @return milliseconds before auto-dismiss 428 * @param requestedTimeout 429 */ getRecommendedHeadsUpTimeoutMs(int requestedTimeout)430 protected int getRecommendedHeadsUpTimeoutMs(int requestedTimeout) { 431 return mAccessibilityMgr.getRecommendedTimeoutMillis( 432 requestedTimeout, 433 AccessibilityManager.FLAG_CONTENT_CONTROLS 434 | AccessibilityManager.FLAG_CONTENT_ICONS 435 | AccessibilityManager.FLAG_CONTENT_TEXT); 436 } 437 } 438 } 439