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