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