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