HeadsUpManager.java revision b349af573121cc659d775696066a13ed7101c308
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 17package com.android.systemui.statusbar.policy; 18 19import android.content.Context; 20import android.content.res.Resources; 21import android.database.ContentObserver; 22import android.os.Handler; 23import android.os.SystemClock; 24import android.provider.Settings; 25import android.util.ArrayMap; 26import android.util.Log; 27import android.util.Pools; 28import android.view.ViewTreeObserver; 29import android.view.accessibility.AccessibilityEvent; 30 31import com.android.systemui.R; 32import com.android.systemui.statusbar.ExpandableNotificationRow; 33import com.android.systemui.statusbar.NotificationData; 34import com.android.systemui.statusbar.phone.PhoneStatusBar; 35 36import java.io.FileDescriptor; 37import java.io.PrintWriter; 38import java.util.ArrayList; 39import java.util.HashMap; 40import java.util.HashSet; 41import java.util.Stack; 42import java.util.TreeSet; 43 44/** 45 * A manager which handles heads up notifications which is a special mode where 46 * they simply peek from the top of the screen. 47 */ 48public class HeadsUpManager implements ViewTreeObserver.OnComputeInternalInsetsListener { 49 private static final String TAG = "HeadsUpManager"; 50 private static final boolean DEBUG = false; 51 private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms"; 52 53 private final int mHeadsUpNotificationDecay; 54 private final int mMinimumDisplayTime; 55 56 private final int mTouchAcceptanceDelay; 57 private final ArrayMap<String, Long> mSnoozedPackages; 58 private final HashSet<OnHeadsUpChangedListener> mListeners = new HashSet<>(); 59 private final int mDefaultSnoozeLengthMs; 60 private final Handler mHandler = new Handler(); 61 private final Pools.Pool<HeadsUpEntry> mEntryPool = new Pools.Pool<HeadsUpEntry>() { 62 63 private Stack<HeadsUpEntry> mPoolObjects = new Stack<>(); 64 65 @Override 66 public HeadsUpEntry acquire() { 67 if (!mPoolObjects.isEmpty()) { 68 return mPoolObjects.pop(); 69 } 70 return new HeadsUpEntry(); 71 } 72 73 @Override 74 public boolean release(HeadsUpEntry instance) { 75 instance.reset(); 76 mPoolObjects.push(instance); 77 return true; 78 } 79 }; 80 81 private PhoneStatusBar mBar; 82 private int mSnoozeLengthMs; 83 private ContentObserver mSettingsObserver; 84 private HashMap<String, HeadsUpEntry> mHeadsUpEntries = new HashMap<>(); 85 private TreeSet<HeadsUpEntry> mSortedEntries = new TreeSet<>(); 86 private HashSet<String> mSwipedOutKeys = new HashSet<>(); 87 private int mUser; 88 private Clock mClock; 89 private boolean mReleaseOnExpandFinish; 90 private boolean mTrackingHeadsUp; 91 private HashSet<NotificationData.Entry> mEntriesToRemoveAfterExpand = new HashSet<>(); 92 private boolean mIsExpanded; 93 private boolean mHasPinnedNotification; 94 private int[] mTmpTwoArray = new int[2]; 95 96 public HeadsUpManager(final Context context, ViewTreeObserver observer) { 97 Resources resources = context.getResources(); 98 mTouchAcceptanceDelay = resources.getInteger(R.integer.touch_acceptance_delay); 99 mSnoozedPackages = new ArrayMap<>(); 100 mDefaultSnoozeLengthMs = resources.getInteger(R.integer.heads_up_default_snooze_length_ms); 101 mSnoozeLengthMs = mDefaultSnoozeLengthMs; 102 mMinimumDisplayTime = resources.getInteger(R.integer.heads_up_notification_minimum_time); 103 mHeadsUpNotificationDecay = resources.getInteger(R.integer.heads_up_notification_decay); 104 mClock = new Clock(); 105 106 mSnoozeLengthMs = Settings.Global.getInt(context.getContentResolver(), 107 SETTING_HEADS_UP_SNOOZE_LENGTH_MS, mDefaultSnoozeLengthMs); 108 mSettingsObserver = new ContentObserver(mHandler) { 109 @Override 110 public void onChange(boolean selfChange) { 111 final int packageSnoozeLengthMs = Settings.Global.getInt( 112 context.getContentResolver(), SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1); 113 if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) { 114 mSnoozeLengthMs = packageSnoozeLengthMs; 115 if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs); 116 } 117 } 118 }; 119 context.getContentResolver().registerContentObserver( 120 Settings.Global.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), false, 121 mSettingsObserver); 122 observer.addOnComputeInternalInsetsListener(this); 123 } 124 125 public void setBar(PhoneStatusBar bar) { 126 mBar = bar; 127 } 128 129 public void addListener(OnHeadsUpChangedListener listener) { 130 mListeners.add(listener); 131 } 132 133 public PhoneStatusBar getBar() { 134 return mBar; 135 } 136 137 /** 138 * Called when posting a new notification to the heads up. 139 */ 140 public void showNotification(NotificationData.Entry headsUp) { 141 if (DEBUG) Log.v(TAG, "showNotification"); 142 addHeadsUpEntry(headsUp); 143 updateNotification(headsUp, true); 144 headsUp.setInterruption(); 145 } 146 147 /** 148 * Called when updating or posting a notification to the heads up. 149 */ 150 public void updateNotification(NotificationData.Entry headsUp, boolean alert) { 151 if (DEBUG) Log.v(TAG, "updateNotification"); 152 153 headsUp.row.setChildrenExpanded(false /* expanded */, false /* animated */); 154 headsUp.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 155 156 if (alert) { 157 HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(headsUp.key); 158 headsUpEntry.updateEntry(); 159 setEntryPinned(headsUpEntry, !mIsExpanded /* isPinned */); 160 } 161 } 162 163 private void addHeadsUpEntry(NotificationData.Entry entry) { 164 HeadsUpEntry headsUpEntry = mEntryPool.acquire(); 165 166 // This will also add the entry to the sortedList 167 headsUpEntry.setEntry(entry); 168 mHeadsUpEntries.put(entry.key, headsUpEntry); 169 entry.row.setHeadsUp(true); 170 setEntryPinned(headsUpEntry, !mIsExpanded /* isPinned */); 171 for (OnHeadsUpChangedListener listener : mListeners) { 172 listener.onHeadsUpStateChanged(entry, true); 173 } 174 entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 175 } 176 177 private void setEntryPinned(HeadsUpEntry headsUpEntry, boolean isPinned) { 178 ExpandableNotificationRow row = headsUpEntry.entry.row; 179 if (row.isPinned() != isPinned) { 180 row.setPinned(isPinned); 181 updatePinnedMode(); 182 for (OnHeadsUpChangedListener listener : mListeners) { 183 if (isPinned) { 184 listener.onHeadsUpPinned(row); 185 } else { 186 listener.onHeadsUpUnPinned(row); 187 } 188 } 189 } 190 } 191 192 private void removeHeadsUpEntry(NotificationData.Entry entry) { 193 HeadsUpEntry remove = mHeadsUpEntries.remove(entry.key); 194 mSortedEntries.remove(remove); 195 entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 196 entry.row.setHeadsUp(false); 197 setEntryPinned(remove, false /* isPinned */); 198 for (OnHeadsUpChangedListener listener : mListeners) { 199 listener.onHeadsUpStateChanged(entry, false); 200 } 201 mEntryPool.release(remove); 202 } 203 204 private void updatePinnedMode() { 205 boolean hasPinnedNotification = hasPinnedNotificationInternal(); 206 if (hasPinnedNotification == mHasPinnedNotification) { 207 return; 208 } 209 mHasPinnedNotification = hasPinnedNotification; 210 for (OnHeadsUpChangedListener listener : mListeners) { 211 listener.onHeadsUpPinnedModeChanged(hasPinnedNotification); 212 } 213 } 214 215 /** 216 * React to the removal of the notification in the heads up. 217 * 218 * @return true if the notification was removed and false if it still needs to be kept around 219 * for a bit since it wasn't shown long enough 220 */ 221 public boolean removeNotification(String key) { 222 if (DEBUG) Log.v(TAG, "remove"); 223 if (wasShownLongEnough(key)) { 224 releaseImmediately(key); 225 return true; 226 } else { 227 getHeadsUpEntry(key).removeAsSoonAsPossible(); 228 return false; 229 } 230 } 231 232 private boolean wasShownLongEnough(String key) { 233 HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); 234 HeadsUpEntry topEntry = getTopEntry(); 235 if (mSwipedOutKeys.contains(key)) { 236 // We always instantly dismiss views being manually swiped out. 237 mSwipedOutKeys.remove(key); 238 return true; 239 } 240 if (headsUpEntry != topEntry) { 241 return true; 242 } 243 return headsUpEntry.wasShownLongEnough(); 244 } 245 246 public boolean isHeadsUp(String key) { 247 return mHeadsUpEntries.containsKey(key); 248 } 249 250 /** 251 * Push any current Heads Up notification down into the shade. 252 */ 253 public void releaseAllImmediately() { 254 if (DEBUG) Log.v(TAG, "releaseAllImmediately"); 255 ArrayList<String> keys = new ArrayList<>(mHeadsUpEntries.keySet()); 256 for (String key : keys) { 257 releaseImmediately(key); 258 } 259 } 260 261 public void releaseImmediately(String key) { 262 HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); 263 if (headsUpEntry == null) { 264 return; 265 } 266 NotificationData.Entry shadeEntry = headsUpEntry.entry; 267 removeHeadsUpEntry(shadeEntry); 268 } 269 270 public boolean isSnoozed(String packageName) { 271 final String key = snoozeKey(packageName, mUser); 272 Long snoozedUntil = mSnoozedPackages.get(key); 273 if (snoozedUntil != null) { 274 if (snoozedUntil > SystemClock.elapsedRealtime()) { 275 if (DEBUG) Log.v(TAG, key + " snoozed"); 276 return true; 277 } 278 mSnoozedPackages.remove(packageName); 279 } 280 return false; 281 } 282 283 public void snooze() { 284 for (String key : mHeadsUpEntries.keySet()) { 285 HeadsUpEntry entry = mHeadsUpEntries.get(key); 286 String packageName = entry.entry.notification.getPackageName(); 287 mSnoozedPackages.put(snoozeKey(packageName, mUser), 288 SystemClock.elapsedRealtime() + mSnoozeLengthMs); 289 } 290 mReleaseOnExpandFinish = true; 291 } 292 293 private static String snoozeKey(String packageName, int user) { 294 return user + "," + packageName; 295 } 296 297 private HeadsUpEntry getHeadsUpEntry(String key) { 298 return mHeadsUpEntries.get(key); 299 } 300 301 public NotificationData.Entry getEntry(String key) { 302 return mHeadsUpEntries.get(key).entry; 303 } 304 305 public TreeSet<HeadsUpEntry> getSortedEntries() { 306 return mSortedEntries; 307 } 308 309 public HeadsUpEntry getTopEntry() { 310 return mSortedEntries.isEmpty() ? null : mSortedEntries.first(); 311 } 312 313 /** 314 * Decides whether a click is invalid for a notification, i.e it has not been shown long enough 315 * that a user might have consciously clicked on it. 316 * 317 * @param key the key of the touched notification 318 * @return whether the touch is invalid and should be discarded 319 */ 320 public boolean shouldSwallowClick(String key) { 321 HeadsUpEntry entry = mHeadsUpEntries.get(key); 322 if (entry != null && mClock.currentTimeMillis() < entry.postTime) { 323 return true; 324 } 325 return false; 326 } 327 328 public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) { 329 if (!mIsExpanded && mHasPinnedNotification) { 330 int minX = Integer.MAX_VALUE; 331 int maxX = 0; 332 int minY = Integer.MAX_VALUE; 333 int maxY = 0; 334 for (HeadsUpEntry entry : mSortedEntries) { 335 ExpandableNotificationRow row = entry.entry.row; 336 if (row.isPinned()) { 337 row.getLocationOnScreen(mTmpTwoArray); 338 minX = Math.min(minX, mTmpTwoArray[0]); 339 minY = Math.min(minY, 0); 340 maxX = Math.max(maxX, mTmpTwoArray[0] + row.getWidth()); 341 maxY = Math.max(maxY, row.getHeadsUpHeight()); 342 } 343 } 344 345 info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 346 info.touchableRegion.set(minX, minY, maxX, maxY); 347 } 348 } 349 350 public void setUser(int user) { 351 mUser = user; 352 } 353 354 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 355 pw.println("HeadsUpManager state:"); 356 pw.print(" mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay); 357 pw.print(" mSnoozeLengthMs="); pw.println(mSnoozeLengthMs); 358 pw.print(" now="); pw.println(SystemClock.elapsedRealtime()); 359 pw.print(" mUser="); pw.println(mUser); 360 for (HeadsUpEntry entry: mSortedEntries) { 361 pw.print(" HeadsUpEntry="); pw.println(entry.entry); 362 } 363 int N = mSnoozedPackages.size(); 364 pw.println(" snoozed packages: " + N); 365 for (int i = 0; i < N; i++) { 366 pw.print(" "); pw.print(mSnoozedPackages.valueAt(i)); 367 pw.print(", "); pw.println(mSnoozedPackages.keyAt(i)); 368 } 369 } 370 371 public boolean hasPinnedHeadsUp() { 372 return mHasPinnedNotification; 373 } 374 375 private boolean hasPinnedNotificationInternal() { 376 for (String key : mHeadsUpEntries.keySet()) { 377 HeadsUpEntry entry = mHeadsUpEntries.get(key); 378 if (entry.entry.row.isPinned()) { 379 return true; 380 } 381 } 382 return false; 383 } 384 385 /** 386 * Notifies that a notification was swiped out and will be removed. 387 * 388 * @param key the notification key 389 */ 390 public void addSwipedOutNotification(String key) { 391 mSwipedOutKeys.add(key); 392 } 393 394 public void unpinAll() { 395 for (String key : mHeadsUpEntries.keySet()) { 396 HeadsUpEntry entry = mHeadsUpEntries.get(key); 397 setEntryPinned(entry, false /* isPinned */); 398 } 399 } 400 401 public void onExpandingFinished() { 402 if (mReleaseOnExpandFinish) { 403 releaseAllImmediately(); 404 mReleaseOnExpandFinish = false; 405 } else { 406 for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) { 407 removeHeadsUpEntry(entry); 408 } 409 mEntriesToRemoveAfterExpand.clear(); 410 } 411 } 412 413 public void setTrackingHeadsUp(boolean trackingHeadsUp) { 414 mTrackingHeadsUp = trackingHeadsUp; 415 } 416 417 public void setIsExpanded(boolean isExpanded) { 418 if (isExpanded != mIsExpanded) { 419 mIsExpanded = isExpanded; 420 if (isExpanded) { 421 unpinAll(); 422 } 423 } 424 } 425 426 public int getTopHeadsUpHeight() { 427 HeadsUpEntry topEntry = getTopEntry(); 428 return topEntry != null ? topEntry.entry.row.getHeadsUpHeight() : 0; 429 } 430 431 /** 432 * Compare two entries and decide how they should be ranked. 433 * 434 * @return -1 if the first argument should be ranked higher than the second, 1 if the second 435 * one should be ranked higher and 0 if they are equal. 436 */ 437 public int compare(NotificationData.Entry a, NotificationData.Entry b) { 438 HeadsUpEntry aEntry = getHeadsUpEntry(a.key); 439 HeadsUpEntry bEntry = getHeadsUpEntry(b.key); 440 if (aEntry == null || bEntry == null) { 441 return aEntry == null ? 1 : -1; 442 } 443 return aEntry.compareTo(bEntry); 444 } 445 446 447 /** 448 * This represents a notification and how long it is in a heads up mode. It also manages its 449 * lifecycle automatically when created. 450 */ 451 public class HeadsUpEntry implements Comparable<HeadsUpEntry> { 452 public NotificationData.Entry entry; 453 public long postTime; 454 public long earliestRemovaltime; 455 private Runnable mRemoveHeadsUpRunnable; 456 457 public void setEntry(final NotificationData.Entry entry) { 458 this.entry = entry; 459 460 // The actual post time will be just after the heads-up really slided in 461 postTime = mClock.currentTimeMillis() + mTouchAcceptanceDelay; 462 mRemoveHeadsUpRunnable = new Runnable() { 463 @Override 464 public void run() { 465 if (!mTrackingHeadsUp) { 466 removeHeadsUpEntry(entry); 467 } else { 468 mEntriesToRemoveAfterExpand.add(entry); 469 } 470 } 471 }; 472 updateEntry(); 473 } 474 475 public void updateEntry() { 476 long currentTime = mClock.currentTimeMillis(); 477 earliestRemovaltime = currentTime + mMinimumDisplayTime; 478 postTime = Math.max(postTime, currentTime); 479 removeAutoRemovalCallbacks(); 480 if (canEntryDecay()) { 481 long finishTime = postTime + mHeadsUpNotificationDecay; 482 long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime); 483 mHandler.postDelayed(mRemoveHeadsUpRunnable, removeDelay); 484 } 485 updateSortOrder(HeadsUpEntry.this); 486 } 487 488 private boolean canEntryDecay() { 489 return entry.notification.getNotification().fullScreenIntent == null; 490 } 491 492 @Override 493 public int compareTo(HeadsUpEntry o) { 494 return postTime < o.postTime ? 1 495 : postTime == o.postTime ? 0 496 : -1; 497 } 498 499 public void removeAutoRemovalCallbacks() { 500 mHandler.removeCallbacks(mRemoveHeadsUpRunnable); 501 } 502 503 public boolean wasShownLongEnough() { 504 return earliestRemovaltime < mClock.currentTimeMillis(); 505 } 506 507 public void removeAsSoonAsPossible() { 508 removeAutoRemovalCallbacks(); 509 mHandler.postDelayed(mRemoveHeadsUpRunnable, 510 earliestRemovaltime - mClock.currentTimeMillis()); 511 } 512 513 public void reset() { 514 removeAutoRemovalCallbacks(); 515 entry = null; 516 mRemoveHeadsUpRunnable = null; 517 } 518 } 519 520 /** 521 * Update the sorted heads up order. 522 * 523 * @param headsUpEntry the headsUp that changed 524 */ 525 private void updateSortOrder(HeadsUpEntry headsUpEntry) { 526 mSortedEntries.remove(headsUpEntry); 527 mSortedEntries.add(headsUpEntry); 528 } 529 530 public static class Clock { 531 public long currentTimeMillis() { 532 return SystemClock.elapsedRealtime(); 533 } 534 } 535 536 public interface OnHeadsUpChangedListener { 537 /** 538 * The state whether there exist pinned heads-ups or not changed. 539 * 540 * @param inPinnedMode whether there are any pinned heads-ups 541 */ 542 void onHeadsUpPinnedModeChanged(boolean inPinnedMode); 543 544 /** 545 * A notification was just pinned to the top. 546 */ 547 void onHeadsUpPinned(ExpandableNotificationRow headsUp); 548 549 /** 550 * A notification was just unpinned from the top. 551 */ 552 void onHeadsUpUnPinned(ExpandableNotificationRow headsUp); 553 554 /** 555 * A notification just became a heads up or turned back to its normal state. 556 * 557 * @param entry the entry of the changed notification 558 * @param isHeadsUp whether the notification is now a headsUp notification 559 */ 560 void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp); 561 } 562} 563