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 mTouchableRegion.set(minX, 0, maxX, mHeadsUpInset + height); 188 return mTouchableRegion; 189 } 190 } 191 192 /** 193 * Decides whether a click is invalid for a notification, i.e it has not been shown long enough 194 * that a user might have consciously clicked on it. 195 * 196 * @param key the key of the touched notification 197 * @return whether the touch is invalid and should be discarded 198 */ shouldSwallowClick(@onNull String key)199 boolean shouldSwallowClick(@NonNull String key) { 200 HeadsUpManager.HeadsUpEntry entry = getHeadsUpEntry(key); 201 return entry != null && mClock.currentTimeMillis() < entry.mPostTime; 202 } 203 onExpandingFinished()204 public void onExpandingFinished() { 205 if (mReleaseOnExpandFinish) { 206 releaseAllImmediately(); 207 mReleaseOnExpandFinish = false; 208 } else { 209 for (NotificationEntry entry : mEntriesToRemoveAfterExpand) { 210 if (isAlerting(entry.getKey())) { 211 // Maybe the heads-up was removed already 212 removeAlertEntry(entry.getKey()); 213 } 214 } 215 } 216 mEntriesToRemoveAfterExpand.clear(); 217 } 218 219 /** 220 * Sets the tracking-heads-up flag. If the flag is true, HeadsUpManager doesn't remove the entry 221 * from the list even after a Heads Up Notification is gone. 222 */ setTrackingHeadsUp(boolean trackingHeadsUp)223 public void setTrackingHeadsUp(boolean trackingHeadsUp) { 224 mTrackingHeadsUp = trackingHeadsUp; 225 } 226 onShadeExpansionFullyChanged(Boolean isExpanded)227 private void onShadeExpansionFullyChanged(Boolean isExpanded) { 228 if (isExpanded != mIsExpanded) { 229 mIsExpanded = isExpanded; 230 if (isExpanded) { 231 mHeadsUpGoingAway = false; 232 } 233 } 234 } 235 236 /** 237 * Set that we are exiting the headsUp pinned mode, but some notifications might still be 238 * animating out. This is used to keep the touchable regions in a reasonable state. 239 */ setHeadsUpGoingAway(boolean headsUpGoingAway)240 void setHeadsUpGoingAway(boolean headsUpGoingAway) { 241 if (headsUpGoingAway != mHeadsUpGoingAway) { 242 mHeadsUpGoingAway = headsUpGoingAway; 243 for (OnHeadsUpPhoneListenerChange listener : mHeadsUpPhoneListeners) { 244 listener.onHeadsUpGoingAwayStateChanged(headsUpGoingAway); 245 } 246 } 247 } 248 isHeadsUpGoingAway()249 boolean isHeadsUpGoingAway() { 250 return mHeadsUpGoingAway; 251 } 252 253 /** 254 * Notifies that a remote input textbox in notification gets active or inactive. 255 * 256 * @param entry The entry of the target notification. 257 * @param remoteInputActive True to notify active, False to notify inactive. 258 */ setRemoteInputActive( @onNull NotificationEntry entry, boolean remoteInputActive)259 public void setRemoteInputActive( 260 @NonNull NotificationEntry entry, boolean remoteInputActive) { 261 HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(entry.getKey()); 262 if (headsUpEntry != null && headsUpEntry.remoteInputActive != remoteInputActive) { 263 headsUpEntry.remoteInputActive = remoteInputActive; 264 if (remoteInputActive) { 265 headsUpEntry.removeAutoRemovalCallbacks(); 266 } else { 267 headsUpEntry.updateEntry(false /* updatePostTime */); 268 } 269 } 270 } 271 272 /** 273 * Sets whether an entry's menu row is exposed and therefore it should stick in the heads up 274 * area if it's pinned until it's hidden again. 275 */ setMenuShown(@onNull NotificationEntry entry, boolean menuShown)276 public void setMenuShown(@NonNull NotificationEntry entry, boolean menuShown) { 277 HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey()); 278 if (headsUpEntry instanceof HeadsUpEntryPhone && entry.isRowPinned()) { 279 ((HeadsUpEntryPhone) headsUpEntry).setMenuShownPinned(menuShown); 280 } 281 } 282 283 /** 284 * Extends the lifetime of the currently showing pulsing notification so that the pulse lasts 285 * longer. 286 */ extendHeadsUp()287 public void extendHeadsUp() { 288 HeadsUpEntryPhone topEntry = getTopHeadsUpEntryPhone(); 289 if (topEntry == null) { 290 return; 291 } 292 topEntry.extendPulse(); 293 } 294 295 /////////////////////////////////////////////////////////////////////////////////////////////// 296 // HeadsUpManager public methods overrides and overloads: 297 298 @Override isTrackingHeadsUp()299 public boolean isTrackingHeadsUp() { 300 return mTrackingHeadsUp; 301 } 302 303 @Override snooze()304 public void snooze() { 305 super.snooze(); 306 mReleaseOnExpandFinish = true; 307 } 308 addSwipedOutNotification(@onNull String key)309 public void addSwipedOutNotification(@NonNull String key) { 310 mSwipedOutKeys.add(key); 311 } 312 removeNotification(@onNull String key, boolean releaseImmediately, boolean animate)313 public boolean removeNotification(@NonNull String key, boolean releaseImmediately, 314 boolean animate) { 315 if (animate) { 316 return removeNotification(key, releaseImmediately); 317 } else { 318 mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(false); 319 boolean removed = removeNotification(key, releaseImmediately); 320 mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(true); 321 return removed; 322 } 323 } 324 325 /////////////////////////////////////////////////////////////////////////////////////////////// 326 // Dumpable overrides: 327 328 @Override dump(PrintWriter pw, String[] args)329 public void dump(PrintWriter pw, String[] args) { 330 pw.println("HeadsUpManagerPhone state:"); 331 dumpInternal(pw, args); 332 } 333 334 /////////////////////////////////////////////////////////////////////////////////////////////// 335 // OnReorderingAllowedListener: 336 337 private final OnReorderingAllowedListener mOnReorderingAllowedListener = () -> { 338 mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(false); 339 for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) { 340 if (isAlerting(entry.getKey())) { 341 // Maybe the heads-up was removed already 342 removeAlertEntry(entry.getKey()); 343 } 344 } 345 mEntriesToRemoveWhenReorderingAllowed.clear(); 346 mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(true); 347 }; 348 349 /////////////////////////////////////////////////////////////////////////////////////////////// 350 // HeadsUpManager utility (protected) methods overrides: 351 352 @Override createAlertEntry()353 protected HeadsUpEntry createAlertEntry() { 354 return mEntryPool.acquire(); 355 } 356 357 @Override onAlertEntryRemoved(AlertEntry alertEntry)358 protected void onAlertEntryRemoved(AlertEntry alertEntry) { 359 super.onAlertEntryRemoved(alertEntry); 360 mEntryPool.release((HeadsUpEntryPhone) alertEntry); 361 } 362 363 @Override shouldHeadsUpBecomePinned(NotificationEntry entry)364 protected boolean shouldHeadsUpBecomePinned(NotificationEntry entry) { 365 boolean pin = mStatusBarState == StatusBarState.SHADE && !mIsExpanded; 366 if (mBypassController.getBypassEnabled()) { 367 pin |= mStatusBarState == StatusBarState.KEYGUARD; 368 } 369 return pin || super.shouldHeadsUpBecomePinned(entry); 370 } 371 372 @Override dumpInternal(PrintWriter pw, String[] args)373 protected void dumpInternal(PrintWriter pw, String[] args) { 374 super.dumpInternal(pw, args); 375 pw.print(" mBarState="); 376 pw.println(mStatusBarState); 377 pw.print(" mTouchableRegion="); 378 pw.println(mTouchableRegion); 379 } 380 381 /////////////////////////////////////////////////////////////////////////////////////////////// 382 // Private utility methods: 383 384 @Nullable getHeadsUpEntryPhone(@onNull String key)385 private HeadsUpEntryPhone getHeadsUpEntryPhone(@NonNull String key) { 386 return (HeadsUpEntryPhone) mAlertEntries.get(key); 387 } 388 389 @Nullable getTopHeadsUpEntryPhone()390 private HeadsUpEntryPhone getTopHeadsUpEntryPhone() { 391 return (HeadsUpEntryPhone) getTopHeadsUpEntry(); 392 } 393 394 @Override canRemoveImmediately(@onNull String key)395 public boolean canRemoveImmediately(@NonNull String key) { 396 if (mSwipedOutKeys.contains(key)) { 397 // We always instantly dismiss views being manually swiped out. 398 mSwipedOutKeys.remove(key); 399 return true; 400 } 401 402 HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(key); 403 HeadsUpEntryPhone topEntry = getTopHeadsUpEntryPhone(); 404 405 return headsUpEntry == null || headsUpEntry != topEntry || super.canRemoveImmediately(key); 406 } 407 408 /////////////////////////////////////////////////////////////////////////////////////////////// 409 // HeadsUpEntryPhone: 410 411 protected class HeadsUpEntryPhone extends HeadsUpManager.HeadsUpEntry { 412 413 private boolean mMenuShownPinned; 414 415 /** 416 * If the time this entry has been on was extended 417 */ 418 private boolean extended; 419 420 421 @Override isSticky()422 public boolean isSticky() { 423 return super.isSticky() || mMenuShownPinned; 424 } 425 setEntry(@onNull final NotificationEntry entry)426 public void setEntry(@NonNull final NotificationEntry entry) { 427 Runnable removeHeadsUpRunnable = () -> { 428 if (!mVisualStabilityProvider.isReorderingAllowed() 429 // We don't want to allow reordering while pulsing, but headsup need to 430 // time out anyway 431 && !entry.showingPulsing()) { 432 mEntriesToRemoveWhenReorderingAllowed.add(entry); 433 mVisualStabilityProvider.addTemporaryReorderingAllowedListener( 434 mOnReorderingAllowedListener); 435 } else if (mTrackingHeadsUp) { 436 mEntriesToRemoveAfterExpand.add(entry); 437 } else { 438 removeAlertEntry(entry.getKey()); 439 } 440 }; 441 442 setEntry(entry, removeHeadsUpRunnable); 443 } 444 445 @Override updateEntry(boolean updatePostTime)446 public void updateEntry(boolean updatePostTime) { 447 super.updateEntry(updatePostTime); 448 449 if (mEntriesToRemoveAfterExpand.contains(mEntry)) { 450 mEntriesToRemoveAfterExpand.remove(mEntry); 451 } 452 if (mEntriesToRemoveWhenReorderingAllowed.contains(mEntry)) { 453 mEntriesToRemoveWhenReorderingAllowed.remove(mEntry); 454 } 455 } 456 457 @Override setExpanded(boolean expanded)458 public void setExpanded(boolean expanded) { 459 if (this.expanded == expanded) { 460 return; 461 } 462 463 this.expanded = expanded; 464 if (expanded) { 465 removeAutoRemovalCallbacks(); 466 } else { 467 updateEntry(false /* updatePostTime */); 468 } 469 } 470 setMenuShownPinned(boolean menuShownPinned)471 public void setMenuShownPinned(boolean menuShownPinned) { 472 if (mMenuShownPinned == menuShownPinned) { 473 return; 474 } 475 476 mMenuShownPinned = menuShownPinned; 477 if (menuShownPinned) { 478 removeAutoRemovalCallbacks(); 479 } else { 480 updateEntry(false /* updatePostTime */); 481 } 482 } 483 484 @Override reset()485 public void reset() { 486 super.reset(); 487 mMenuShownPinned = false; 488 extended = false; 489 } 490 extendPulse()491 private void extendPulse() { 492 if (!extended) { 493 extended = true; 494 updateEntry(false); 495 } 496 } 497 498 @Override calculateFinishTime()499 protected long calculateFinishTime() { 500 return super.calculateFinishTime() + (extended ? mExtensionTime : 0); 501 } 502 } 503 504 public interface AnimationStateHandler { setHeadsUpGoingAwayAnimationsAllowed(boolean allowed)505 void setHeadsUpGoingAwayAnimationsAllowed(boolean allowed); 506 } 507 508 /** 509 * Listener to register for HeadsUpNotification Phone changes. 510 */ 511 public interface OnHeadsUpPhoneListenerChange { 512 /** 513 * Called when a heads up notification is 'going away' or no longer 'going away'. 514 * See {@link HeadsUpManagerPhone#setHeadsUpGoingAway}. 515 */ onHeadsUpGoingAwayStateChanged(boolean headsUpGoingAway)516 void onHeadsUpGoingAwayStateChanged(boolean headsUpGoingAway); 517 } 518 519 private final StateListener mStatusBarStateListener = new StateListener() { 520 @Override 521 public void onStateChanged(int newState) { 522 boolean wasKeyguard = mStatusBarState == StatusBarState.KEYGUARD; 523 boolean isKeyguard = newState == StatusBarState.KEYGUARD; 524 mStatusBarState = newState; 525 if (wasKeyguard && !isKeyguard && mBypassController.getBypassEnabled()) { 526 ArrayList<String> keysToRemove = new ArrayList<>(); 527 for (AlertEntry entry : mAlertEntries.values()) { 528 if (entry.mEntry != null && entry.mEntry.isBubble() && !entry.isSticky()) { 529 keysToRemove.add(entry.mEntry.getKey()); 530 } 531 } 532 for (String key : keysToRemove) { 533 removeAlertEntry(key); 534 } 535 } 536 } 537 538 @Override 539 public void onDozingChanged(boolean isDozing) { 540 if (!isDozing) { 541 // Let's make sure all huns we got while dozing time out within the normal timeout 542 // duration. Otherwise they could get stuck for a very long time 543 for (AlertEntry entry : mAlertEntries.values()) { 544 entry.updateEntry(true /* updatePostTime */); 545 } 546 } 547 } 548 }; 549 } 550