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