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