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