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