1 /* 2 * Copyright (C) 2018 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.phone; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.graphics.Region; 24 import android.util.Pools; 25 26 import androidx.collection.ArraySet; 27 28 import com.android.internal.annotations.VisibleForTesting; 29 import com.android.systemui.Dumpable; 30 import com.android.systemui.R; 31 import com.android.systemui.plugins.statusbar.StatusBarStateController; 32 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; 33 import com.android.systemui.statusbar.StatusBarState; 34 import com.android.systemui.statusbar.notification.VisualStabilityManager; 35 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 36 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 37 import com.android.systemui.statusbar.policy.ConfigurationController; 38 import com.android.systemui.statusbar.policy.HeadsUpManager; 39 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; 40 41 import java.io.FileDescriptor; 42 import java.io.PrintWriter; 43 import java.util.ArrayList; 44 import java.util.HashSet; 45 import java.util.List; 46 import java.util.Stack; 47 48 /** 49 * A implementation of HeadsUpManager for phone and car. 50 */ 51 public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable, 52 VisualStabilityManager.Callback, OnHeadsUpChangedListener { 53 private static final String TAG = "HeadsUpManagerPhone"; 54 55 @VisibleForTesting 56 final int mExtensionTime; 57 private final KeyguardBypassController mBypassController; 58 private final NotificationGroupManager mGroupManager; 59 private final List<OnHeadsUpPhoneListenerChange> mHeadsUpPhoneListeners = new ArrayList<>(); 60 private final int mAutoHeadsUpNotificationDecay; 61 private VisualStabilityManager mVisualStabilityManager; 62 private boolean mReleaseOnExpandFinish; 63 64 private boolean mTrackingHeadsUp; 65 private HashSet<String> mSwipedOutKeys = new HashSet<>(); 66 private HashSet<NotificationEntry> mEntriesToRemoveAfterExpand = new HashSet<>(); 67 private HashSet<String> mKeysToRemoveWhenLeavingKeyguard = new HashSet<>(); 68 private ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed 69 = new ArraySet<>(); 70 private boolean mIsExpanded; 71 private boolean mHeadsUpGoingAway; 72 private int mStatusBarState; 73 private AnimationStateHandler mAnimationStateHandler; 74 private int mHeadsUpInset; 75 76 // Used for determining the region for touch interaction 77 private final Region mTouchableRegion = new Region(); 78 79 private final Pools.Pool<HeadsUpEntryPhone> mEntryPool = new Pools.Pool<HeadsUpEntryPhone>() { 80 private Stack<HeadsUpEntryPhone> mPoolObjects = new Stack<>(); 81 82 @Override 83 public HeadsUpEntryPhone acquire() { 84 if (!mPoolObjects.isEmpty()) { 85 return mPoolObjects.pop(); 86 } 87 return new HeadsUpEntryPhone(); 88 } 89 90 @Override 91 public boolean release(@NonNull HeadsUpEntryPhone instance) { 92 mPoolObjects.push(instance); 93 return true; 94 } 95 }; 96 97 /////////////////////////////////////////////////////////////////////////////////////////////// 98 // Constructor: 99 HeadsUpManagerPhone(@onNull final Context context, StatusBarStateController statusBarStateController, KeyguardBypassController bypassController, NotificationGroupManager groupManager, ConfigurationController configurationController)100 public HeadsUpManagerPhone(@NonNull final Context context, 101 StatusBarStateController statusBarStateController, 102 KeyguardBypassController bypassController, 103 NotificationGroupManager groupManager, 104 ConfigurationController configurationController) { 105 super(context); 106 Resources resources = mContext.getResources(); 107 mExtensionTime = resources.getInteger(R.integer.ambient_notification_extension_time); 108 mAutoHeadsUpNotificationDecay = resources.getInteger( 109 R.integer.auto_heads_up_notification_decay); 110 statusBarStateController.addCallback(mStatusBarStateListener); 111 mBypassController = bypassController; 112 mGroupManager = groupManager; 113 114 updateResources(); 115 configurationController.addCallback(new ConfigurationController.ConfigurationListener() { 116 @Override 117 public void onDensityOrFontScaleChanged() { 118 updateResources(); 119 } 120 121 @Override 122 public void onOverlayChanged() { 123 updateResources(); 124 } 125 }); 126 } 127 setup(VisualStabilityManager visualStabilityManager)128 void setup(VisualStabilityManager visualStabilityManager) { 129 mVisualStabilityManager = visualStabilityManager; 130 } 131 setAnimationStateHandler(AnimationStateHandler handler)132 public void setAnimationStateHandler(AnimationStateHandler handler) { 133 mAnimationStateHandler = handler; 134 } 135 updateResources()136 private void updateResources() { 137 Resources resources = mContext.getResources(); 138 mHeadsUpInset = 139 resources.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height) 140 + resources.getDimensionPixelSize(R.dimen.heads_up_status_bar_padding); 141 } 142 143 /////////////////////////////////////////////////////////////////////////////////////////////// 144 // Public methods: 145 146 /** 147 * Add a listener to receive callbacks onHeadsUpGoingAway 148 */ addHeadsUpPhoneListener(OnHeadsUpPhoneListenerChange listener)149 void addHeadsUpPhoneListener(OnHeadsUpPhoneListenerChange listener) { 150 mHeadsUpPhoneListeners.add(listener); 151 } 152 153 /** 154 * Gets the touchable region needed for heads up notifications. Returns null if no touchable 155 * region is required (ie: no heads up notification currently exists). 156 */ getTouchableRegion()157 @Nullable Region getTouchableRegion() { 158 NotificationEntry topEntry = getTopEntry(); 159 160 // This call could be made in an inconsistent state while the pinnedMode hasn't been 161 // updated yet, but callbacks leading out of the headsUp manager, querying it. Let's 162 // therefore also check if the topEntry is null. 163 if (!hasPinnedHeadsUp() || topEntry == null) { 164 return null; 165 } else { 166 if (topEntry.isChildInGroup()) { 167 final NotificationEntry groupSummary = 168 mGroupManager.getGroupSummary(topEntry.getSbn()); 169 if (groupSummary != null) { 170 topEntry = groupSummary; 171 } 172 } 173 ExpandableNotificationRow topRow = topEntry.getRow(); 174 int[] tmpArray = new int[2]; 175 topRow.getLocationOnScreen(tmpArray); 176 int minX = tmpArray[0]; 177 int maxX = tmpArray[0] + topRow.getWidth(); 178 int height = topRow.getIntrinsicHeight(); 179 mTouchableRegion.set(minX, 0, maxX, mHeadsUpInset + height); 180 return mTouchableRegion; 181 } 182 } 183 184 /** 185 * Decides whether a click is invalid for a notification, i.e it has not been shown long enough 186 * that a user might have consciously clicked on it. 187 * 188 * @param key the key of the touched notification 189 * @return whether the touch is invalid and should be discarded 190 */ shouldSwallowClick(@onNull String key)191 boolean shouldSwallowClick(@NonNull String key) { 192 HeadsUpManager.HeadsUpEntry entry = getHeadsUpEntry(key); 193 return entry != null && mClock.currentTimeMillis() < entry.mPostTime; 194 } 195 onExpandingFinished()196 public void onExpandingFinished() { 197 if (mReleaseOnExpandFinish) { 198 releaseAllImmediately(); 199 mReleaseOnExpandFinish = false; 200 } else { 201 for (NotificationEntry entry : mEntriesToRemoveAfterExpand) { 202 if (isAlerting(entry.getKey())) { 203 // Maybe the heads-up was removed already 204 removeAlertEntry(entry.getKey()); 205 } 206 } 207 } 208 mEntriesToRemoveAfterExpand.clear(); 209 } 210 211 /** 212 * Sets the tracking-heads-up flag. If the flag is true, HeadsUpManager doesn't remove the entry 213 * from the list even after a Heads Up Notification is gone. 214 */ setTrackingHeadsUp(boolean trackingHeadsUp)215 public void setTrackingHeadsUp(boolean trackingHeadsUp) { 216 mTrackingHeadsUp = trackingHeadsUp; 217 } 218 219 /** 220 * Notify that the status bar panel gets expanded or collapsed. 221 * 222 * @param isExpanded True to notify expanded, false to notify collapsed. 223 */ setIsPanelExpanded(boolean isExpanded)224 void setIsPanelExpanded(boolean isExpanded) { 225 if (isExpanded != mIsExpanded) { 226 mIsExpanded = isExpanded; 227 if (isExpanded) { 228 mHeadsUpGoingAway = false; 229 } 230 } 231 } 232 233 @Override isEntryAutoHeadsUpped(String key)234 public boolean isEntryAutoHeadsUpped(String key) { 235 HeadsUpEntryPhone headsUpEntryPhone = getHeadsUpEntryPhone(key); 236 if (headsUpEntryPhone == null) { 237 return false; 238 } 239 return headsUpEntryPhone.isAutoHeadsUp(); 240 } 241 242 /** 243 * Set that we are exiting the headsUp pinned mode, but some notifications might still be 244 * animating out. This is used to keep the touchable regions in a sane state. 245 */ setHeadsUpGoingAway(boolean headsUpGoingAway)246 void setHeadsUpGoingAway(boolean headsUpGoingAway) { 247 if (headsUpGoingAway != mHeadsUpGoingAway) { 248 mHeadsUpGoingAway = headsUpGoingAway; 249 for (OnHeadsUpPhoneListenerChange listener : mHeadsUpPhoneListeners) { 250 listener.onHeadsUpGoingAwayStateChanged(headsUpGoingAway); 251 } 252 } 253 } 254 isHeadsUpGoingAway()255 boolean isHeadsUpGoingAway() { 256 return mHeadsUpGoingAway; 257 } 258 259 /** 260 * Notifies that a remote input textbox in notification gets active or inactive. 261 * 262 * @param entry The entry of the target notification. 263 * @param remoteInputActive True to notify active, False to notify inactive. 264 */ setRemoteInputActive( @onNull NotificationEntry entry, boolean remoteInputActive)265 public void setRemoteInputActive( 266 @NonNull NotificationEntry entry, boolean remoteInputActive) { 267 HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(entry.getKey()); 268 if (headsUpEntry != null && headsUpEntry.remoteInputActive != remoteInputActive) { 269 headsUpEntry.remoteInputActive = remoteInputActive; 270 if (remoteInputActive) { 271 headsUpEntry.removeAutoRemovalCallbacks(); 272 } else { 273 headsUpEntry.updateEntry(false /* updatePostTime */); 274 } 275 } 276 } 277 278 /** 279 * Sets whether an entry's menu row is exposed and therefore it should stick in the heads up 280 * area if it's pinned until it's hidden again. 281 */ setMenuShown(@onNull NotificationEntry entry, boolean menuShown)282 public void setMenuShown(@NonNull NotificationEntry entry, boolean menuShown) { 283 HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey()); 284 if (headsUpEntry instanceof HeadsUpEntryPhone && entry.isRowPinned()) { 285 ((HeadsUpEntryPhone) headsUpEntry).setMenuShownPinned(menuShown); 286 } 287 } 288 289 /** 290 * Extends the lifetime of the currently showing pulsing notification so that the pulse lasts 291 * longer. 292 */ extendHeadsUp()293 public void extendHeadsUp() { 294 HeadsUpEntryPhone topEntry = getTopHeadsUpEntryPhone(); 295 if (topEntry == null) { 296 return; 297 } 298 topEntry.extendPulse(); 299 } 300 301 /////////////////////////////////////////////////////////////////////////////////////////////// 302 // HeadsUpManager public methods overrides: 303 304 @Override isTrackingHeadsUp()305 public boolean isTrackingHeadsUp() { 306 return mTrackingHeadsUp; 307 } 308 309 @Override snooze()310 public void snooze() { 311 super.snooze(); 312 mReleaseOnExpandFinish = true; 313 } 314 addSwipedOutNotification(@onNull String key)315 public void addSwipedOutNotification(@NonNull String key) { 316 mSwipedOutKeys.add(key); 317 } 318 319 /////////////////////////////////////////////////////////////////////////////////////////////// 320 // Dumpable overrides: 321 322 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)323 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 324 pw.println("HeadsUpManagerPhone state:"); 325 dumpInternal(fd, pw, args); 326 } 327 328 @Override shouldExtendLifetime(NotificationEntry entry)329 public boolean shouldExtendLifetime(NotificationEntry entry) { 330 // We should not defer the removal if reordering isn't allowed since otherwise 331 // these won't disappear until reordering is allowed again, which happens only once 332 // the notification panel is collapsed again. 333 return mVisualStabilityManager.isReorderingAllowed() && super.shouldExtendLifetime(entry); 334 } 335 336 /////////////////////////////////////////////////////////////////////////////////////////////// 337 // VisualStabilityManager.Callback overrides: 338 339 @Override onChangeAllowed()340 public void onChangeAllowed() { 341 mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(false); 342 for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) { 343 if (isAlerting(entry.getKey())) { 344 // Maybe the heads-up was removed already 345 removeAlertEntry(entry.getKey()); 346 } 347 } 348 mEntriesToRemoveWhenReorderingAllowed.clear(); 349 mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(true); 350 } 351 352 /////////////////////////////////////////////////////////////////////////////////////////////// 353 // HeadsUpManager utility (protected) methods overrides: 354 355 @Override createAlertEntry()356 protected HeadsUpEntry createAlertEntry() { 357 return mEntryPool.acquire(); 358 } 359 360 @Override onAlertEntryRemoved(AlertEntry alertEntry)361 protected void onAlertEntryRemoved(AlertEntry alertEntry) { 362 mKeysToRemoveWhenLeavingKeyguard.remove(alertEntry.mEntry.getKey()); 363 super.onAlertEntryRemoved(alertEntry); 364 mEntryPool.release((HeadsUpEntryPhone) alertEntry); 365 } 366 367 @Override shouldHeadsUpBecomePinned(NotificationEntry entry)368 protected boolean shouldHeadsUpBecomePinned(NotificationEntry entry) { 369 boolean pin = mStatusBarState == StatusBarState.SHADE && !mIsExpanded; 370 if (mBypassController.getBypassEnabled()) { 371 pin |= mStatusBarState == StatusBarState.KEYGUARD; 372 } 373 return pin || super.shouldHeadsUpBecomePinned(entry); 374 } 375 376 @Override dumpInternal(FileDescriptor fd, PrintWriter pw, String[] args)377 protected void dumpInternal(FileDescriptor fd, PrintWriter pw, String[] args) { 378 super.dumpInternal(fd, pw, args); 379 pw.print(" mBarState="); 380 pw.println(mStatusBarState); 381 pw.print(" mTouchableRegion="); 382 pw.println(mTouchableRegion); 383 } 384 385 /////////////////////////////////////////////////////////////////////////////////////////////// 386 // Private utility methods: 387 388 @Nullable getHeadsUpEntryPhone(@onNull String key)389 private HeadsUpEntryPhone getHeadsUpEntryPhone(@NonNull String key) { 390 return (HeadsUpEntryPhone) mAlertEntries.get(key); 391 } 392 393 @Nullable getTopHeadsUpEntryPhone()394 private HeadsUpEntryPhone getTopHeadsUpEntryPhone() { 395 return (HeadsUpEntryPhone) getTopHeadsUpEntry(); 396 } 397 398 @Override canRemoveImmediately(@onNull String key)399 protected boolean canRemoveImmediately(@NonNull String key) { 400 if (mSwipedOutKeys.contains(key)) { 401 // We always instantly dismiss views being manually swiped out. 402 mSwipedOutKeys.remove(key); 403 return true; 404 } 405 406 HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(key); 407 HeadsUpEntryPhone topEntry = getTopHeadsUpEntryPhone(); 408 409 return headsUpEntry == null || headsUpEntry != topEntry || super.canRemoveImmediately(key); 410 } 411 412 /////////////////////////////////////////////////////////////////////////////////////////////// 413 // HeadsUpEntryPhone: 414 415 protected class HeadsUpEntryPhone extends HeadsUpManager.HeadsUpEntry { 416 417 private boolean mMenuShownPinned; 418 419 /** 420 * If the time this entry has been on was extended 421 */ 422 private boolean extended; 423 424 /** 425 * Was this entry received while on keyguard 426 */ 427 private boolean mIsAutoHeadsUp; 428 429 430 @Override isSticky()431 public boolean isSticky() { 432 return super.isSticky() || mMenuShownPinned; 433 } 434 setEntry(@onNull final NotificationEntry entry)435 public void setEntry(@NonNull final NotificationEntry entry) { 436 Runnable removeHeadsUpRunnable = () -> { 437 if (!mVisualStabilityManager.isReorderingAllowed() 438 // We don't want to allow reordering while pulsing, but headsup need to 439 // time out anyway 440 && !entry.showingPulsing()) { 441 mEntriesToRemoveWhenReorderingAllowed.add(entry); 442 mVisualStabilityManager.addReorderingAllowedCallback(HeadsUpManagerPhone.this, 443 false /* persistent */); 444 } else if (mTrackingHeadsUp) { 445 mEntriesToRemoveAfterExpand.add(entry); 446 } else if (mIsAutoHeadsUp && mStatusBarState == StatusBarState.KEYGUARD) { 447 mKeysToRemoveWhenLeavingKeyguard.add(entry.getKey()); 448 } else { 449 removeAlertEntry(entry.getKey()); 450 } 451 }; 452 453 setEntry(entry, removeHeadsUpRunnable); 454 } 455 456 @Override updateEntry(boolean updatePostTime)457 public void updateEntry(boolean updatePostTime) { 458 mIsAutoHeadsUp = mEntry.isAutoHeadsUp(); 459 super.updateEntry(updatePostTime); 460 461 if (mEntriesToRemoveAfterExpand.contains(mEntry)) { 462 mEntriesToRemoveAfterExpand.remove(mEntry); 463 } 464 if (mEntriesToRemoveWhenReorderingAllowed.contains(mEntry)) { 465 mEntriesToRemoveWhenReorderingAllowed.remove(mEntry); 466 } 467 mKeysToRemoveWhenLeavingKeyguard.remove(mEntry.getKey()); 468 } 469 470 @Override setExpanded(boolean expanded)471 public void setExpanded(boolean expanded) { 472 if (this.expanded == expanded) { 473 return; 474 } 475 476 this.expanded = expanded; 477 if (expanded) { 478 removeAutoRemovalCallbacks(); 479 } else { 480 updateEntry(false /* updatePostTime */); 481 } 482 } 483 setMenuShownPinned(boolean menuShownPinned)484 public void setMenuShownPinned(boolean menuShownPinned) { 485 if (mMenuShownPinned == menuShownPinned) { 486 return; 487 } 488 489 mMenuShownPinned = menuShownPinned; 490 if (menuShownPinned) { 491 removeAutoRemovalCallbacks(); 492 } else { 493 updateEntry(false /* updatePostTime */); 494 } 495 } 496 497 @Override reset()498 public void reset() { 499 super.reset(); 500 mMenuShownPinned = false; 501 extended = false; 502 mIsAutoHeadsUp = false; 503 } 504 extendPulse()505 private void extendPulse() { 506 if (!extended) { 507 extended = true; 508 updateEntry(false); 509 } 510 } 511 512 @Override compareTo(AlertEntry alertEntry)513 public int compareTo(AlertEntry alertEntry) { 514 HeadsUpEntryPhone headsUpEntry = (HeadsUpEntryPhone) alertEntry; 515 boolean autoShown = isAutoHeadsUp(); 516 boolean otherAutoShown = headsUpEntry.isAutoHeadsUp(); 517 if (autoShown && !otherAutoShown) { 518 return 1; 519 } else if (!autoShown && otherAutoShown) { 520 return -1; 521 } 522 return super.compareTo(alertEntry); 523 } 524 525 @Override calculateFinishTime()526 protected long calculateFinishTime() { 527 return mPostTime + getDecayDuration() + (extended ? mExtensionTime : 0); 528 } 529 getDecayDuration()530 private int getDecayDuration() { 531 if (isAutoHeadsUp()) { 532 return getRecommendedHeadsUpTimeoutMs(mAutoHeadsUpNotificationDecay); 533 } else { 534 return getRecommendedHeadsUpTimeoutMs(mAutoDismissNotificationDecay); 535 } 536 } 537 isAutoHeadsUp()538 private boolean isAutoHeadsUp() { 539 return mIsAutoHeadsUp; 540 } 541 } 542 543 public interface AnimationStateHandler { setHeadsUpGoingAwayAnimationsAllowed(boolean allowed)544 void setHeadsUpGoingAwayAnimationsAllowed(boolean allowed); 545 } 546 547 /** 548 * Listener to register for HeadsUpNotification Phone changes. 549 */ 550 public interface OnHeadsUpPhoneListenerChange { 551 /** 552 * Called when a heads up notification is 'going away' or no longer 'going away'. 553 * See {@link HeadsUpManagerPhone#setHeadsUpGoingAway}. 554 */ onHeadsUpGoingAwayStateChanged(boolean headsUpGoingAway)555 void onHeadsUpGoingAwayStateChanged(boolean headsUpGoingAway); 556 } 557 558 private final StateListener mStatusBarStateListener = new StateListener() { 559 @Override 560 public void onStateChanged(int newState) { 561 boolean wasKeyguard = mStatusBarState == StatusBarState.KEYGUARD; 562 boolean isKeyguard = newState == StatusBarState.KEYGUARD; 563 mStatusBarState = newState; 564 if (wasKeyguard && !isKeyguard && mKeysToRemoveWhenLeavingKeyguard.size() != 0) { 565 String[] keys = mKeysToRemoveWhenLeavingKeyguard.toArray(new String[0]); 566 for (String key : keys) { 567 removeAlertEntry(key); 568 } 569 mKeysToRemoveWhenLeavingKeyguard.clear(); 570 } 571 if (wasKeyguard && !isKeyguard && mBypassController.getBypassEnabled()) { 572 ArrayList<String> keysToRemove = new ArrayList<>(); 573 for (AlertEntry entry : mAlertEntries.values()) { 574 if (entry.mEntry != null && entry.mEntry.isBubble() && !entry.isSticky()) { 575 keysToRemove.add(entry.mEntry.getKey()); 576 } 577 } 578 for (String key : keysToRemove) { 579 removeAlertEntry(key); 580 } 581 } 582 } 583 584 @Override 585 public void onDozingChanged(boolean isDozing) { 586 if (!isDozing) { 587 // Let's make sure all huns we got while dozing time out within the normal timeout 588 // duration. Otherwise they could get stuck for a very long time 589 for (AlertEntry entry : mAlertEntries.values()) { 590 entry.updateEntry(true /* updatePostTime */); 591 } 592 } 593 } 594 }; 595 } 596