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.Configuration; 23 import android.content.res.Resources; 24 import android.graphics.Rect; 25 import android.graphics.Region; 26 import android.util.Log; 27 import android.util.Pools; 28 import android.view.DisplayCutout; 29 import android.view.Gravity; 30 import android.view.View; 31 import android.view.ViewTreeObserver; 32 33 import androidx.collection.ArraySet; 34 35 import com.android.systemui.Dependency; 36 import com.android.systemui.Dumpable; 37 import com.android.systemui.R; 38 import com.android.systemui.ScreenDecorations; 39 import com.android.systemui.plugins.statusbar.StatusBarStateController; 40 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; 41 import com.android.systemui.statusbar.StatusBarState; 42 import com.android.systemui.statusbar.notification.VisualStabilityManager; 43 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 44 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 45 import com.android.systemui.statusbar.policy.ConfigurationController; 46 import com.android.systemui.statusbar.policy.HeadsUpManager; 47 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; 48 49 import java.io.FileDescriptor; 50 import java.io.PrintWriter; 51 import java.util.HashSet; 52 import java.util.Stack; 53 54 /** 55 * A implementation of HeadsUpManager for phone and car. 56 */ 57 public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable, 58 VisualStabilityManager.Callback, OnHeadsUpChangedListener, 59 ConfigurationController.ConfigurationListener, StateListener { 60 private static final String TAG = "HeadsUpManagerPhone"; 61 62 private final View mStatusBarWindowView; 63 private final NotificationGroupManager mGroupManager; 64 private final VisualStabilityManager mVisualStabilityManager; 65 private final StatusBarTouchableRegionManager mStatusBarTouchableRegionManager; 66 private boolean mReleaseOnExpandFinish; 67 68 private int mStatusBarHeight; 69 private int mHeadsUpInset; 70 private int mDisplayCutoutTouchableRegionSize; 71 private boolean mTrackingHeadsUp; 72 private HashSet<String> mSwipedOutKeys = new HashSet<>(); 73 private HashSet<NotificationEntry> mEntriesToRemoveAfterExpand = new HashSet<>(); 74 private ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed 75 = new ArraySet<>(); 76 private boolean mIsExpanded; 77 private int[] mTmpTwoArray = new int[2]; 78 private boolean mHeadsUpGoingAway; 79 private int mStatusBarState; 80 private Region mTouchableRegion = new Region(); 81 82 private AnimationStateHandler mAnimationStateHandler; 83 84 private final Pools.Pool<HeadsUpEntryPhone> mEntryPool = new Pools.Pool<HeadsUpEntryPhone>() { 85 private Stack<HeadsUpEntryPhone> mPoolObjects = new Stack<>(); 86 87 @Override 88 public HeadsUpEntryPhone acquire() { 89 if (!mPoolObjects.isEmpty()) { 90 return mPoolObjects.pop(); 91 } 92 return new HeadsUpEntryPhone(); 93 } 94 95 @Override 96 public boolean release(@NonNull HeadsUpEntryPhone instance) { 97 mPoolObjects.push(instance); 98 return true; 99 } 100 }; 101 102 /////////////////////////////////////////////////////////////////////////////////////////////// 103 // Constructor: 104 HeadsUpManagerPhone(@onNull final Context context, @NonNull View statusBarWindowView, @NonNull NotificationGroupManager groupManager, @NonNull StatusBar bar, @NonNull VisualStabilityManager visualStabilityManager)105 public HeadsUpManagerPhone(@NonNull final Context context, 106 @NonNull View statusBarWindowView, 107 @NonNull NotificationGroupManager groupManager, 108 @NonNull StatusBar bar, 109 @NonNull VisualStabilityManager visualStabilityManager) { 110 super(context); 111 112 mStatusBarWindowView = statusBarWindowView; 113 mStatusBarTouchableRegionManager = new StatusBarTouchableRegionManager(context, this, bar, 114 statusBarWindowView); 115 mGroupManager = groupManager; 116 mVisualStabilityManager = visualStabilityManager; 117 118 initResources(); 119 120 addListener(new OnHeadsUpChangedListener() { 121 @Override 122 public void onHeadsUpPinnedModeChanged(boolean hasPinnedNotification) { 123 if (Log.isLoggable(TAG, Log.WARN)) { 124 Log.w(TAG, "onHeadsUpPinnedModeChanged"); 125 } 126 mStatusBarTouchableRegionManager.updateTouchableRegion(); 127 } 128 }); 129 Dependency.get(StatusBarStateController.class).addCallback(this); 130 } 131 setAnimationStateHandler(AnimationStateHandler handler)132 public void setAnimationStateHandler(AnimationStateHandler handler) { 133 mAnimationStateHandler = handler; 134 } 135 initResources()136 private void initResources() { 137 Resources resources = mContext.getResources(); 138 mStatusBarHeight = resources.getDimensionPixelSize( 139 com.android.internal.R.dimen.status_bar_height); 140 mHeadsUpInset = mStatusBarHeight + resources.getDimensionPixelSize( 141 R.dimen.heads_up_status_bar_padding); 142 mDisplayCutoutTouchableRegionSize = resources.getDimensionPixelSize( 143 com.android.internal.R.dimen.display_cutout_touchable_region_size); 144 } 145 146 @Override onDensityOrFontScaleChanged()147 public void onDensityOrFontScaleChanged() { 148 super.onDensityOrFontScaleChanged(); 149 initResources(); 150 } 151 152 @Override onOverlayChanged()153 public void onOverlayChanged() { 154 initResources(); 155 } 156 157 /////////////////////////////////////////////////////////////////////////////////////////////// 158 // Public methods: 159 160 /** 161 * Decides whether a click is invalid for a notification, i.e it has not been shown long enough 162 * that a user might have consciously clicked on it. 163 * 164 * @param key the key of the touched notification 165 * @return whether the touch is invalid and should be discarded 166 */ shouldSwallowClick(@onNull String key)167 public boolean shouldSwallowClick(@NonNull String key) { 168 HeadsUpManager.HeadsUpEntry entry = getHeadsUpEntry(key); 169 return entry != null && mClock.currentTimeMillis() < entry.mPostTime; 170 } 171 onExpandingFinished()172 public void onExpandingFinished() { 173 if (mReleaseOnExpandFinish) { 174 releaseAllImmediately(); 175 mReleaseOnExpandFinish = false; 176 } else { 177 for (NotificationEntry entry : mEntriesToRemoveAfterExpand) { 178 if (isAlerting(entry.key)) { 179 // Maybe the heads-up was removed already 180 removeAlertEntry(entry.key); 181 } 182 } 183 } 184 mEntriesToRemoveAfterExpand.clear(); 185 } 186 187 /** 188 * Sets the tracking-heads-up flag. If the flag is true, HeadsUpManager doesn't remove the entry 189 * from the list even after a Heads Up Notification is gone. 190 */ setTrackingHeadsUp(boolean trackingHeadsUp)191 public void setTrackingHeadsUp(boolean trackingHeadsUp) { 192 mTrackingHeadsUp = trackingHeadsUp; 193 } 194 195 /** 196 * Notify that the status bar panel gets expanded or collapsed. 197 * 198 * @param isExpanded True to notify expanded, false to notify collapsed. 199 */ setIsPanelExpanded(boolean isExpanded)200 public void setIsPanelExpanded(boolean isExpanded) { 201 if (isExpanded != mIsExpanded) { 202 mIsExpanded = isExpanded; 203 if (isExpanded) { 204 mHeadsUpGoingAway = false; 205 } 206 mStatusBarTouchableRegionManager.setIsStatusBarExpanded(isExpanded); 207 mStatusBarTouchableRegionManager.updateTouchableRegion(); 208 } 209 } 210 211 @Override onStateChanged(int newState)212 public void onStateChanged(int newState) { 213 mStatusBarState = newState; 214 } 215 216 /** 217 * Set that we are exiting the headsUp pinned mode, but some notifications might still be 218 * animating out. This is used to keep the touchable regions in a sane state. 219 */ setHeadsUpGoingAway(boolean headsUpGoingAway)220 public void setHeadsUpGoingAway(boolean headsUpGoingAway) { 221 if (headsUpGoingAway != mHeadsUpGoingAway) { 222 mHeadsUpGoingAway = headsUpGoingAway; 223 if (!headsUpGoingAway) { 224 mStatusBarTouchableRegionManager.updateTouchableRegionAfterLayout(); 225 } else { 226 mStatusBarTouchableRegionManager.updateTouchableRegion(); 227 } 228 } 229 } 230 isHeadsUpGoingAway()231 public boolean isHeadsUpGoingAway() { 232 return mHeadsUpGoingAway; 233 } 234 235 /** 236 * Notifies that a remote input textbox in notification gets active or inactive. 237 * 238 * @param entry The entry of the target notification. 239 * @param remoteInputActive True to notify active, False to notify inactive. 240 */ setRemoteInputActive( @onNull NotificationEntry entry, boolean remoteInputActive)241 public void setRemoteInputActive( 242 @NonNull NotificationEntry entry, boolean remoteInputActive) { 243 HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(entry.key); 244 if (headsUpEntry != null && headsUpEntry.remoteInputActive != remoteInputActive) { 245 headsUpEntry.remoteInputActive = remoteInputActive; 246 if (remoteInputActive) { 247 headsUpEntry.removeAutoRemovalCallbacks(); 248 } else { 249 headsUpEntry.updateEntry(false /* updatePostTime */); 250 } 251 } 252 } 253 254 /** 255 * Sets whether an entry's menu row is exposed and therefore it should stick in the heads up 256 * area if it's pinned until it's hidden again. 257 */ setMenuShown(@onNull NotificationEntry entry, boolean menuShown)258 public void setMenuShown(@NonNull NotificationEntry entry, boolean menuShown) { 259 HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.key); 260 if (headsUpEntry instanceof HeadsUpEntryPhone && entry.isRowPinned()) { 261 ((HeadsUpEntryPhone) headsUpEntry).setMenuShownPinned(menuShown); 262 } 263 } 264 265 /////////////////////////////////////////////////////////////////////////////////////////////// 266 // HeadsUpManager public methods overrides: 267 268 @Override isTrackingHeadsUp()269 public boolean isTrackingHeadsUp() { 270 return mTrackingHeadsUp; 271 } 272 273 @Override snooze()274 public void snooze() { 275 super.snooze(); 276 mReleaseOnExpandFinish = true; 277 } 278 addSwipedOutNotification(@onNull String key)279 public void addSwipedOutNotification(@NonNull String key) { 280 mSwipedOutKeys.add(key); 281 } 282 283 /////////////////////////////////////////////////////////////////////////////////////////////// 284 // Dumpable overrides: 285 286 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)287 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 288 pw.println("HeadsUpManagerPhone state:"); 289 dumpInternal(fd, pw, args); 290 } 291 292 /** 293 * Update touch insets to include any area needed for touching a heads up notification. 294 * 295 * @param info Insets that will include heads up notification touch area after execution. 296 */ 297 @Nullable updateTouchableRegion(ViewTreeObserver.InternalInsetsInfo info)298 public void updateTouchableRegion(ViewTreeObserver.InternalInsetsInfo info) { 299 info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 300 info.touchableRegion.set(calculateTouchableRegion()); 301 } 302 calculateTouchableRegion()303 public Region calculateTouchableRegion() { 304 NotificationEntry topEntry = getTopEntry(); 305 // This call could be made in an inconsistent state while the pinnedMode hasn't been 306 // updated yet, but callbacks leading out of the headsUp manager, querying it. Let's 307 // therefore also check if the topEntry is null. 308 if (!hasPinnedHeadsUp() || topEntry == null) { 309 mTouchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight); 310 updateRegionForNotch(mTouchableRegion); 311 312 } else { 313 if (topEntry.isChildInGroup()) { 314 final NotificationEntry groupSummary = 315 mGroupManager.getGroupSummary(topEntry.notification); 316 if (groupSummary != null) { 317 topEntry = groupSummary; 318 } 319 } 320 ExpandableNotificationRow topRow = topEntry.getRow(); 321 topRow.getLocationOnScreen(mTmpTwoArray); 322 int minX = mTmpTwoArray[0]; 323 int maxX = mTmpTwoArray[0] + topRow.getWidth(); 324 int height = topRow.getIntrinsicHeight(); 325 mTouchableRegion.set(minX, 0, maxX, mHeadsUpInset + height); 326 } 327 return mTouchableRegion; 328 } 329 updateRegionForNotch(Region region)330 private void updateRegionForNotch(Region region) { 331 DisplayCutout cutout = mStatusBarWindowView.getRootWindowInsets().getDisplayCutout(); 332 if (cutout == null) { 333 return; 334 } 335 336 // Expand touchable region such that we also catch touches that just start below the notch 337 // area. 338 Rect bounds = new Rect(); 339 ScreenDecorations.DisplayCutoutView.boundsFromDirection(cutout, Gravity.TOP, bounds); 340 bounds.offset(0, mDisplayCutoutTouchableRegionSize); 341 region.union(bounds); 342 } 343 344 @Override shouldExtendLifetime(NotificationEntry entry)345 public boolean shouldExtendLifetime(NotificationEntry entry) { 346 // We should not defer the removal if reordering isn't allowed since otherwise 347 // these won't disappear until reordering is allowed again, which happens only once 348 // the notification panel is collapsed again. 349 return mVisualStabilityManager.isReorderingAllowed() && super.shouldExtendLifetime(entry); 350 } 351 352 @Override onConfigChanged(Configuration newConfig)353 public void onConfigChanged(Configuration newConfig) { 354 initResources(); 355 } 356 357 /////////////////////////////////////////////////////////////////////////////////////////////// 358 // VisualStabilityManager.Callback overrides: 359 360 @Override onReorderingAllowed()361 public void onReorderingAllowed() { 362 mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(false); 363 for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) { 364 if (isAlerting(entry.key)) { 365 // Maybe the heads-up was removed already 366 removeAlertEntry(entry.key); 367 } 368 } 369 mEntriesToRemoveWhenReorderingAllowed.clear(); 370 mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(true); 371 } 372 373 /////////////////////////////////////////////////////////////////////////////////////////////// 374 // HeadsUpManager utility (protected) methods overrides: 375 376 @Override createAlertEntry()377 protected HeadsUpEntry createAlertEntry() { 378 return mEntryPool.acquire(); 379 } 380 381 @Override onAlertEntryRemoved(AlertEntry alertEntry)382 protected void onAlertEntryRemoved(AlertEntry alertEntry) { 383 super.onAlertEntryRemoved(alertEntry); 384 mEntryPool.release((HeadsUpEntryPhone) alertEntry); 385 } 386 387 @Override shouldHeadsUpBecomePinned(NotificationEntry entry)388 protected boolean shouldHeadsUpBecomePinned(NotificationEntry entry) { 389 return mStatusBarState != StatusBarState.KEYGUARD && !mIsExpanded 390 || super.shouldHeadsUpBecomePinned(entry); 391 } 392 393 @Override dumpInternal(FileDescriptor fd, PrintWriter pw, String[] args)394 protected void dumpInternal(FileDescriptor fd, PrintWriter pw, String[] args) { 395 super.dumpInternal(fd, pw, args); 396 pw.print(" mBarState="); 397 pw.println(mStatusBarState); 398 pw.print(" mTouchableRegion="); 399 pw.println(mTouchableRegion); 400 } 401 402 /////////////////////////////////////////////////////////////////////////////////////////////// 403 // Private utility methods: 404 405 @Nullable getHeadsUpEntryPhone(@onNull String key)406 private HeadsUpEntryPhone getHeadsUpEntryPhone(@NonNull String key) { 407 return (HeadsUpEntryPhone) mAlertEntries.get(key); 408 } 409 410 @Nullable getTopHeadsUpEntryPhone()411 private HeadsUpEntryPhone getTopHeadsUpEntryPhone() { 412 return (HeadsUpEntryPhone) getTopHeadsUpEntry(); 413 } 414 415 @Override canRemoveImmediately(@onNull String key)416 protected boolean canRemoveImmediately(@NonNull String key) { 417 if (mSwipedOutKeys.contains(key)) { 418 // We always instantly dismiss views being manually swiped out. 419 mSwipedOutKeys.remove(key); 420 return true; 421 } 422 423 HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(key); 424 HeadsUpEntryPhone topEntry = getTopHeadsUpEntryPhone(); 425 426 return headsUpEntry == null || headsUpEntry != topEntry || super.canRemoveImmediately(key); 427 } 428 429 /////////////////////////////////////////////////////////////////////////////////////////////// 430 // HeadsUpEntryPhone: 431 432 protected class HeadsUpEntryPhone extends HeadsUpManager.HeadsUpEntry { 433 434 private boolean mMenuShownPinned; 435 436 @Override isSticky()437 protected boolean isSticky() { 438 return super.isSticky() || mMenuShownPinned; 439 } 440 setEntry(@onNull final NotificationEntry entry)441 public void setEntry(@NonNull final NotificationEntry entry) { 442 Runnable removeHeadsUpRunnable = () -> { 443 if (!mVisualStabilityManager.isReorderingAllowed()) { 444 mEntriesToRemoveWhenReorderingAllowed.add(entry); 445 mVisualStabilityManager.addReorderingAllowedCallback( 446 HeadsUpManagerPhone.this); 447 } else if (!mTrackingHeadsUp) { 448 removeAlertEntry(entry.key); 449 } else { 450 mEntriesToRemoveAfterExpand.add(entry); 451 } 452 }; 453 454 setEntry(entry, removeHeadsUpRunnable); 455 } 456 457 @Override updateEntry(boolean updatePostTime)458 public void updateEntry(boolean updatePostTime) { 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 } 468 469 @Override setExpanded(boolean expanded)470 public void setExpanded(boolean expanded) { 471 if (this.expanded == expanded) { 472 return; 473 } 474 475 this.expanded = expanded; 476 if (expanded) { 477 removeAutoRemovalCallbacks(); 478 } else { 479 updateEntry(false /* updatePostTime */); 480 } 481 } 482 setMenuShownPinned(boolean menuShownPinned)483 public void setMenuShownPinned(boolean menuShownPinned) { 484 if (mMenuShownPinned == menuShownPinned) { 485 return; 486 } 487 488 mMenuShownPinned = menuShownPinned; 489 if (menuShownPinned) { 490 removeAutoRemovalCallbacks(); 491 } else { 492 updateEntry(false /* updatePostTime */); 493 } 494 } 495 496 @Override reset()497 public void reset() { 498 super.reset(); 499 mMenuShownPinned = false; 500 } 501 } 502 503 public interface AnimationStateHandler { setHeadsUpGoingAwayAnimationsAllowed(boolean allowed)504 void setHeadsUpGoingAwayAnimationsAllowed(boolean allowed); 505 } 506 } 507