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