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