HeadsUpManager.java revision a69f2a6449b4b5eceae9cd5a6b1aae6eeec379b8
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 mGroupManager.onEntryHeadsUped(headsUp); 194 setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUp)); 195 } 196 } 197 198 private void addHeadsUpEntry(NotificationData.Entry entry) { 199 HeadsUpEntry headsUpEntry = mEntryPool.acquire(); 200 201 // This will also add the entry to the sortedList 202 headsUpEntry.setEntry(entry); 203 mHeadsUpEntries.put(entry.key, headsUpEntry); 204 entry.row.setHeadsUp(true); 205 setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(entry)); 206 for (OnHeadsUpChangedListener listener : mListeners) { 207 listener.onHeadsUpStateChanged(entry, true); 208 } 209 entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 210 } 211 212 private boolean shouldHeadsUpBecomePinned(NotificationData.Entry entry) { 213 return !mIsExpanded || hasFullScreenIntent(entry); 214 } 215 216 private boolean hasFullScreenIntent(NotificationData.Entry entry) { 217 return entry.notification.getNotification().fullScreenIntent != null; 218 } 219 220 private void setEntryPinned(HeadsUpEntry headsUpEntry, boolean isPinned) { 221 ExpandableNotificationRow row = headsUpEntry.entry.row; 222 if (row.isPinned() != isPinned) { 223 row.setPinned(isPinned); 224 updatePinnedMode(); 225 for (OnHeadsUpChangedListener listener : mListeners) { 226 if (isPinned) { 227 listener.onHeadsUpPinned(row); 228 } else { 229 listener.onHeadsUpUnPinned(row); 230 } 231 } 232 } 233 } 234 235 private void removeHeadsUpEntry(NotificationData.Entry entry) { 236 HeadsUpEntry remove = mHeadsUpEntries.remove(entry.key); 237 mSortedEntries.remove(remove); 238 entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 239 entry.row.setHeadsUp(false); 240 setEntryPinned(remove, false /* isPinned */); 241 for (OnHeadsUpChangedListener listener : mListeners) { 242 listener.onHeadsUpStateChanged(entry, false); 243 } 244 mEntryPool.release(remove); 245 } 246 247 private void updatePinnedMode() { 248 boolean hasPinnedNotification = hasPinnedNotificationInternal(); 249 if (hasPinnedNotification == mHasPinnedNotification) { 250 return; 251 } 252 mHasPinnedNotification = hasPinnedNotification; 253 if (mHasPinnedNotification) { 254 MetricsLogger.count(mContext, "note_peek", 1); 255 } 256 updateTouchableRegionListener(); 257 for (OnHeadsUpChangedListener listener : mListeners) { 258 listener.onHeadsUpPinnedModeChanged(hasPinnedNotification); 259 } 260 } 261 262 /** 263 * React to the removal of the notification in the heads up. 264 * 265 * @return true if the notification was removed and false if it still needs to be kept around 266 * for a bit since it wasn't shown long enough 267 */ 268 public boolean removeNotification(String key) { 269 if (DEBUG) Log.v(TAG, "remove"); 270 if (wasShownLongEnough(key)) { 271 releaseImmediately(key); 272 return true; 273 } else { 274 getHeadsUpEntry(key).removeAsSoonAsPossible(); 275 return false; 276 } 277 } 278 279 private boolean wasShownLongEnough(String key) { 280 HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); 281 HeadsUpEntry topEntry = getTopEntry(); 282 if (mSwipedOutKeys.contains(key)) { 283 // We always instantly dismiss views being manually swiped out. 284 mSwipedOutKeys.remove(key); 285 return true; 286 } 287 if (headsUpEntry != topEntry) { 288 return true; 289 } 290 return headsUpEntry.wasShownLongEnough(); 291 } 292 293 public boolean isHeadsUp(String key) { 294 return mHeadsUpEntries.containsKey(key); 295 } 296 297 /** 298 * Push any current Heads Up notification down into the shade. 299 */ 300 public void releaseAllImmediately() { 301 if (DEBUG) Log.v(TAG, "releaseAllImmediately"); 302 ArrayList<String> keys = new ArrayList<>(mHeadsUpEntries.keySet()); 303 for (String key : keys) { 304 releaseImmediately(key); 305 } 306 } 307 308 public void releaseImmediately(String key) { 309 HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); 310 if (headsUpEntry == null) { 311 return; 312 } 313 NotificationData.Entry shadeEntry = headsUpEntry.entry; 314 removeHeadsUpEntry(shadeEntry); 315 } 316 317 public boolean isSnoozed(String packageName) { 318 final String key = snoozeKey(packageName, mUser); 319 Long snoozedUntil = mSnoozedPackages.get(key); 320 if (snoozedUntil != null) { 321 if (snoozedUntil > SystemClock.elapsedRealtime()) { 322 if (DEBUG) Log.v(TAG, key + " snoozed"); 323 return true; 324 } 325 mSnoozedPackages.remove(packageName); 326 } 327 return false; 328 } 329 330 public void snooze() { 331 for (String key : mHeadsUpEntries.keySet()) { 332 HeadsUpEntry entry = mHeadsUpEntries.get(key); 333 String packageName = entry.entry.notification.getPackageName(); 334 mSnoozedPackages.put(snoozeKey(packageName, mUser), 335 SystemClock.elapsedRealtime() + mSnoozeLengthMs); 336 } 337 mReleaseOnExpandFinish = true; 338 } 339 340 private static String snoozeKey(String packageName, int user) { 341 return user + "," + packageName; 342 } 343 344 private HeadsUpEntry getHeadsUpEntry(String key) { 345 return mHeadsUpEntries.get(key); 346 } 347 348 public NotificationData.Entry getEntry(String key) { 349 return mHeadsUpEntries.get(key).entry; 350 } 351 352 public TreeSet<HeadsUpEntry> getSortedEntries() { 353 return mSortedEntries; 354 } 355 356 public HeadsUpEntry getTopEntry() { 357 return mSortedEntries.isEmpty() ? null : mSortedEntries.first(); 358 } 359 360 /** 361 * Decides whether a click is invalid for a notification, i.e it has not been shown long enough 362 * that a user might have consciously clicked on it. 363 * 364 * @param key the key of the touched notification 365 * @return whether the touch is invalid and should be discarded 366 */ 367 public boolean shouldSwallowClick(String key) { 368 HeadsUpEntry entry = mHeadsUpEntries.get(key); 369 if (entry != null && mClock.currentTimeMillis() < entry.postTime) { 370 return true; 371 } 372 return false; 373 } 374 375 public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) { 376 if (mIsExpanded) { 377 // The touchable region is always the full area when expanded 378 return; 379 } 380 if (mHasPinnedNotification) { 381 int minX = 0; 382 int maxX = 0; 383 int maxY = 0; 384 for (HeadsUpEntry entry : mSortedEntries) { 385 ExpandableNotificationRow row = entry.entry.row; 386 if (row.isPinned()) { 387 row.getLocationOnScreen(mTmpTwoArray); 388 minX = mTmpTwoArray[0]; 389 maxX = mTmpTwoArray[0] + row.getWidth(); 390 maxY = row.getHeadsUpHeight(); 391 break; 392 } 393 } 394 395 info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 396 info.touchableRegion.set(minX, 0, maxX, maxY + mNotificationsTopPadding); 397 } else if (mHeadsUpGoingAway || mWaitingOnCollapseWhenGoingAway) { 398 info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 399 info.touchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight); 400 } 401 } 402 403 public void setUser(int user) { 404 mUser = user; 405 } 406 407 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 408 pw.println("HeadsUpManager state:"); 409 pw.print(" mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay); 410 pw.print(" mSnoozeLengthMs="); pw.println(mSnoozeLengthMs); 411 pw.print(" now="); pw.println(SystemClock.elapsedRealtime()); 412 pw.print(" mUser="); pw.println(mUser); 413 for (HeadsUpEntry entry: mSortedEntries) { 414 pw.print(" HeadsUpEntry="); pw.println(entry.entry); 415 } 416 int N = mSnoozedPackages.size(); 417 pw.println(" snoozed packages: " + N); 418 for (int i = 0; i < N; i++) { 419 pw.print(" "); pw.print(mSnoozedPackages.valueAt(i)); 420 pw.print(", "); pw.println(mSnoozedPackages.keyAt(i)); 421 } 422 } 423 424 public boolean hasPinnedHeadsUp() { 425 return mHasPinnedNotification; 426 } 427 428 private boolean hasPinnedNotificationInternal() { 429 for (String key : mHeadsUpEntries.keySet()) { 430 HeadsUpEntry entry = mHeadsUpEntries.get(key); 431 if (entry.entry.row.isPinned()) { 432 return true; 433 } 434 } 435 return false; 436 } 437 438 /** 439 * Notifies that a notification was swiped out and will be removed. 440 * 441 * @param key the notification key 442 */ 443 public void addSwipedOutNotification(String key) { 444 mSwipedOutKeys.add(key); 445 } 446 447 public void unpinAll() { 448 for (String key : mHeadsUpEntries.keySet()) { 449 HeadsUpEntry entry = mHeadsUpEntries.get(key); 450 setEntryPinned(entry, false /* isPinned */); 451 } 452 } 453 454 public void onExpandingFinished() { 455 if (mReleaseOnExpandFinish) { 456 releaseAllImmediately(); 457 mReleaseOnExpandFinish = false; 458 } else { 459 for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) { 460 if (isHeadsUp(entry.key)) { 461 // Maybe the heads-up was removed already 462 removeHeadsUpEntry(entry); 463 } 464 } 465 } 466 mEntriesToRemoveAfterExpand.clear(); 467 } 468 469 public void setTrackingHeadsUp(boolean trackingHeadsUp) { 470 mTrackingHeadsUp = trackingHeadsUp; 471 } 472 473 public void setIsExpanded(boolean isExpanded) { 474 if (isExpanded != mIsExpanded) { 475 mIsExpanded = isExpanded; 476 if (isExpanded) { 477 // make sure our state is sane 478 mWaitingOnCollapseWhenGoingAway = false; 479 mHeadsUpGoingAway = false; 480 updateTouchableRegionListener(); 481 } 482 } 483 } 484 485 public int getTopHeadsUpHeight() { 486 HeadsUpEntry topEntry = getTopEntry(); 487 return topEntry != null ? topEntry.entry.row.getHeadsUpHeight() : 0; 488 } 489 490 /** 491 * Compare two entries and decide how they should be ranked. 492 * 493 * @return -1 if the first argument should be ranked higher than the second, 1 if the second 494 * one should be ranked higher and 0 if they are equal. 495 */ 496 public int compare(NotificationData.Entry a, NotificationData.Entry b) { 497 HeadsUpEntry aEntry = getHeadsUpEntry(a.key); 498 HeadsUpEntry bEntry = getHeadsUpEntry(b.key); 499 if (aEntry == null || bEntry == null) { 500 return aEntry == null ? 1 : -1; 501 } 502 return aEntry.compareTo(bEntry); 503 } 504 505 /** 506 * Set that we are exiting the headsUp pinned mode, but some notifications might still be 507 * animating out. This is used to keep the touchable regions in a sane state. 508 */ 509 public void setHeadsUpGoingAway(boolean headsUpGoingAway) { 510 if (headsUpGoingAway != mHeadsUpGoingAway) { 511 mHeadsUpGoingAway = headsUpGoingAway; 512 if (!headsUpGoingAway) { 513 waitForStatusBarLayout(); 514 } 515 updateTouchableRegionListener(); 516 } 517 } 518 519 /** 520 * We need to wait on the whole panel to collapse, before we can remove the touchable region 521 * listener. 522 */ 523 private void waitForStatusBarLayout() { 524 mWaitingOnCollapseWhenGoingAway = true; 525 mStatusBarWindowView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 526 @Override 527 public void onLayoutChange(View v, int left, int top, int right, int bottom, 528 int oldLeft, 529 int oldTop, int oldRight, int oldBottom) { 530 if (mStatusBarWindowView.getHeight() <= mStatusBarHeight) { 531 mStatusBarWindowView.removeOnLayoutChangeListener(this); 532 mWaitingOnCollapseWhenGoingAway = false; 533 updateTouchableRegionListener(); 534 } 535 } 536 }); 537 } 538 539 public static void setIsClickedNotification(View child, boolean clicked) { 540 child.setTag(TAG_CLICKED_NOTIFICATION, clicked ? true : null); 541 } 542 543 public static boolean isClickedHeadsUpNotification(View child) { 544 Boolean clicked = (Boolean) child.getTag(TAG_CLICKED_NOTIFICATION); 545 return clicked != null && clicked; 546 } 547 548 public void setRemoteInputActive(NotificationData.Entry entry, boolean remoteInputActive) { 549 HeadsUpEntry headsUpEntry = mHeadsUpEntries.get(entry.key); 550 if (headsUpEntry != null && headsUpEntry.remoteInputActive != remoteInputActive) { 551 headsUpEntry.remoteInputActive = remoteInputActive; 552 if (remoteInputActive) { 553 headsUpEntry.removeAutoRemovalCallbacks(); 554 } else { 555 headsUpEntry.updateEntry(false /* updatePostTime */); 556 } 557 } 558 } 559 560 /** 561 * This represents a notification and how long it is in a heads up mode. It also manages its 562 * lifecycle automatically when created. 563 */ 564 public class HeadsUpEntry implements Comparable<HeadsUpEntry> { 565 public NotificationData.Entry entry; 566 public long postTime; 567 public long earliestRemovaltime; 568 private Runnable mRemoveHeadsUpRunnable; 569 public boolean remoteInputActive; 570 571 public void setEntry(final NotificationData.Entry entry) { 572 this.entry = entry; 573 574 // The actual post time will be just after the heads-up really slided in 575 postTime = mClock.currentTimeMillis() + mTouchAcceptanceDelay; 576 mRemoveHeadsUpRunnable = new Runnable() { 577 @Override 578 public void run() { 579 if (!mTrackingHeadsUp) { 580 removeHeadsUpEntry(entry); 581 } else { 582 mEntriesToRemoveAfterExpand.add(entry); 583 } 584 } 585 }; 586 updateEntry(); 587 } 588 589 public void updateEntry() { 590 updateEntry(true); 591 } 592 593 public void updateEntry(boolean updatePostTime) { 594 mSortedEntries.remove(HeadsUpEntry.this); 595 long currentTime = mClock.currentTimeMillis(); 596 earliestRemovaltime = currentTime + mMinimumDisplayTime; 597 if (updatePostTime) { 598 postTime = Math.max(postTime, currentTime); 599 } 600 removeAutoRemovalCallbacks(); 601 if (mEntriesToRemoveAfterExpand.contains(entry)) { 602 mEntriesToRemoveAfterExpand.remove(entry); 603 } 604 if (!hasFullScreenIntent(entry) && !mRemoteInputActive) { 605 long finishTime = postTime + mHeadsUpNotificationDecay; 606 long removeDelay = Math.max(finishTime - currentTime, mMinimumDisplayTime); 607 mHandler.postDelayed(mRemoveHeadsUpRunnable, removeDelay); 608 } 609 mSortedEntries.add(HeadsUpEntry.this); 610 } 611 612 @Override 613 public int compareTo(HeadsUpEntry o) { 614 boolean selfFullscreen = hasFullScreenIntent(entry); 615 boolean otherFullscreen = hasFullScreenIntent(o.entry); 616 if (selfFullscreen && !otherFullscreen) { 617 return -1; 618 } else if (!selfFullscreen && otherFullscreen) { 619 return 1; 620 } 621 return postTime < o.postTime ? 1 622 : postTime == o.postTime ? entry.key.compareTo(o.entry.key) 623 : -1; 624 } 625 626 public void removeAutoRemovalCallbacks() { 627 mHandler.removeCallbacks(mRemoveHeadsUpRunnable); 628 } 629 630 public boolean wasShownLongEnough() { 631 return earliestRemovaltime < mClock.currentTimeMillis(); 632 } 633 634 public void removeAsSoonAsPossible() { 635 removeAutoRemovalCallbacks(); 636 mHandler.postDelayed(mRemoveHeadsUpRunnable, 637 earliestRemovaltime - mClock.currentTimeMillis()); 638 } 639 640 public void reset() { 641 removeAutoRemovalCallbacks(); 642 entry = null; 643 mRemoveHeadsUpRunnable = null; 644 } 645 } 646 647 public static class Clock { 648 public long currentTimeMillis() { 649 return SystemClock.elapsedRealtime(); 650 } 651 } 652 653 public interface OnHeadsUpChangedListener { 654 /** 655 * The state whether there exist pinned heads-ups or not changed. 656 * 657 * @param inPinnedMode whether there are any pinned heads-ups 658 */ 659 void onHeadsUpPinnedModeChanged(boolean inPinnedMode); 660 661 /** 662 * A notification was just pinned to the top. 663 */ 664 void onHeadsUpPinned(ExpandableNotificationRow headsUp); 665 666 /** 667 * A notification was just unpinned from the top. 668 */ 669 void onHeadsUpUnPinned(ExpandableNotificationRow headsUp); 670 671 /** 672 * A notification just became a heads up or turned back to its normal state. 673 * 674 * @param entry the entry of the changed notification 675 * @param isHeadsUp whether the notification is now a headsUp notification 676 */ 677 void onHeadsUpStateChanged(NotificationData.Entry entry, boolean isHeadsUp); 678 } 679} 680