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