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.support.v4.util.ArraySet; 25 import android.util.Log; 26 import android.util.Pools; 27 import android.view.View; 28 import android.view.ViewTreeObserver; 29 30 import com.android.internal.annotations.VisibleForTesting; 31 import com.android.systemui.Dumpable; 32 import com.android.systemui.R; 33 import com.android.systemui.statusbar.ExpandableNotificationRow; 34 import com.android.systemui.statusbar.NotificationData; 35 import com.android.systemui.statusbar.StatusBarState; 36 import com.android.systemui.statusbar.notification.VisualStabilityManager; 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.HashSet; 44 import java.util.Stack; 45 46 /** 47 * A implementation of HeadsUpManager for phone and car. 48 */ 49 public class HeadsUpManagerPhone extends HeadsUpManager implements Dumpable, 50 ViewTreeObserver.OnComputeInternalInsetsListener, VisualStabilityManager.Callback, 51 OnHeadsUpChangedListener, ConfigurationController.ConfigurationListener { 52 private static final String TAG = "HeadsUpManagerPhone"; 53 private static final boolean DEBUG = false; 54 55 private final View mStatusBarWindowView; 56 private final NotificationGroupManager mGroupManager; 57 private final StatusBar mBar; 58 private final VisualStabilityManager mVisualStabilityManager; 59 private boolean mReleaseOnExpandFinish; 60 61 private int mStatusBarHeight; 62 private int mHeadsUpInset; 63 private boolean mTrackingHeadsUp; 64 private HashSet<String> mSwipedOutKeys = new HashSet<>(); 65 private HashSet<NotificationData.Entry> mEntriesToRemoveAfterExpand = new HashSet<>(); 66 private ArraySet<NotificationData.Entry> mEntriesToRemoveWhenReorderingAllowed 67 = new ArraySet<>(); 68 private boolean mIsExpanded; 69 private int[] mTmpTwoArray = new int[2]; 70 private boolean mHeadsUpGoingAway; 71 private boolean mWaitingOnCollapseWhenGoingAway; 72 private boolean mIsObserving; 73 private int mStatusBarState; 74 75 private final Pools.Pool<HeadsUpEntryPhone> mEntryPool = new Pools.Pool<HeadsUpEntryPhone>() { 76 private Stack<HeadsUpEntryPhone> mPoolObjects = new Stack<>(); 77 78 @Override 79 public HeadsUpEntryPhone acquire() { 80 if (!mPoolObjects.isEmpty()) { 81 return mPoolObjects.pop(); 82 } 83 return new HeadsUpEntryPhone(); 84 } 85 86 @Override 87 public boolean release(@NonNull HeadsUpEntryPhone instance) { 88 mPoolObjects.push(instance); 89 return true; 90 } 91 }; 92 93 /////////////////////////////////////////////////////////////////////////////////////////////// 94 // Constructor: 95 HeadsUpManagerPhone(@onNull final Context context, @NonNull View statusBarWindowView, @NonNull NotificationGroupManager groupManager, @NonNull StatusBar bar, @NonNull VisualStabilityManager visualStabilityManager)96 public HeadsUpManagerPhone(@NonNull final Context context, @NonNull View statusBarWindowView, 97 @NonNull NotificationGroupManager groupManager, @NonNull StatusBar bar, 98 @NonNull VisualStabilityManager visualStabilityManager) { 99 super(context); 100 101 mStatusBarWindowView = statusBarWindowView; 102 mGroupManager = groupManager; 103 mBar = bar; 104 mVisualStabilityManager = visualStabilityManager; 105 106 initResources(); 107 108 addListener(new OnHeadsUpChangedListener() { 109 @Override 110 public void onHeadsUpPinnedModeChanged(boolean hasPinnedNotification) { 111 if (DEBUG) Log.w(TAG, "onHeadsUpPinnedModeChanged"); 112 updateTouchableRegionListener(); 113 } 114 }); 115 } 116 initResources()117 private void initResources() { 118 Resources resources = mContext.getResources(); 119 mStatusBarHeight = resources.getDimensionPixelSize( 120 com.android.internal.R.dimen.status_bar_height); 121 mHeadsUpInset = mStatusBarHeight + resources.getDimensionPixelSize( 122 R.dimen.heads_up_status_bar_padding); 123 } 124 125 @Override onDensityOrFontScaleChanged()126 public void onDensityOrFontScaleChanged() { 127 super.onDensityOrFontScaleChanged(); 128 initResources(); 129 } 130 131 /////////////////////////////////////////////////////////////////////////////////////////////// 132 // Public methods: 133 134 /** 135 * Decides whether a click is invalid for a notification, i.e it has not been shown long enough 136 * that a user might have consciously clicked on it. 137 * 138 * @param key the key of the touched notification 139 * @return whether the touch is invalid and should be discarded 140 */ shouldSwallowClick(@onNull String key)141 public boolean shouldSwallowClick(@NonNull String key) { 142 HeadsUpManager.HeadsUpEntry entry = getHeadsUpEntry(key); 143 return entry != null && mClock.currentTimeMillis() < entry.postTime; 144 } 145 onExpandingFinished()146 public void onExpandingFinished() { 147 if (mReleaseOnExpandFinish) { 148 releaseAllImmediately(); 149 mReleaseOnExpandFinish = false; 150 } else { 151 for (NotificationData.Entry entry : mEntriesToRemoveAfterExpand) { 152 if (isHeadsUp(entry.key)) { 153 // Maybe the heads-up was removed already 154 removeHeadsUpEntry(entry); 155 } 156 } 157 } 158 mEntriesToRemoveAfterExpand.clear(); 159 } 160 161 /** 162 * Sets the tracking-heads-up flag. If the flag is true, HeadsUpManager doesn't remove the entry 163 * from the list even after a Heads Up Notification is gone. 164 */ setTrackingHeadsUp(boolean trackingHeadsUp)165 public void setTrackingHeadsUp(boolean trackingHeadsUp) { 166 mTrackingHeadsUp = trackingHeadsUp; 167 } 168 169 /** 170 * Notify that the status bar panel gets expanded or collapsed. 171 * 172 * @param isExpanded True to notify expanded, false to notify collapsed. 173 */ setIsPanelExpanded(boolean isExpanded)174 public void setIsPanelExpanded(boolean isExpanded) { 175 if (isExpanded != mIsExpanded) { 176 mIsExpanded = isExpanded; 177 if (isExpanded) { 178 // make sure our state is sane 179 mWaitingOnCollapseWhenGoingAway = false; 180 mHeadsUpGoingAway = false; 181 updateTouchableRegionListener(); 182 } 183 } 184 } 185 186 /** 187 * Set the current state of the statusbar. 188 */ setStatusBarState(int statusBarState)189 public void setStatusBarState(int statusBarState) { 190 mStatusBarState = statusBarState; 191 } 192 193 /** 194 * Set that we are exiting the headsUp pinned mode, but some notifications might still be 195 * animating out. This is used to keep the touchable regions in a sane state. 196 */ setHeadsUpGoingAway(boolean headsUpGoingAway)197 public void setHeadsUpGoingAway(boolean headsUpGoingAway) { 198 if (headsUpGoingAway != mHeadsUpGoingAway) { 199 mHeadsUpGoingAway = headsUpGoingAway; 200 if (!headsUpGoingAway) { 201 waitForStatusBarLayout(); 202 } 203 updateTouchableRegionListener(); 204 } 205 } 206 207 /** 208 * Notifies that a remote input textbox in notification gets active or inactive. 209 * @param entry The entry of the target notification. 210 * @param remoteInputActive True to notify active, False to notify inactive. 211 */ setRemoteInputActive( @onNull NotificationData.Entry entry, boolean remoteInputActive)212 public void setRemoteInputActive( 213 @NonNull NotificationData.Entry entry, boolean remoteInputActive) { 214 HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(entry.key); 215 if (headsUpEntry != null && headsUpEntry.remoteInputActive != remoteInputActive) { 216 headsUpEntry.remoteInputActive = remoteInputActive; 217 if (remoteInputActive) { 218 headsUpEntry.removeAutoRemovalCallbacks(); 219 } else { 220 headsUpEntry.updateEntry(false /* updatePostTime */); 221 } 222 } 223 } 224 225 @VisibleForTesting removeMinimumDisplayTimeForTesting()226 public void removeMinimumDisplayTimeForTesting() { 227 mMinimumDisplayTime = 0; 228 mHeadsUpNotificationDecay = 0; 229 mTouchAcceptanceDelay = 0; 230 } 231 232 /////////////////////////////////////////////////////////////////////////////////////////////// 233 // HeadsUpManager public methods overrides: 234 235 @Override isTrackingHeadsUp()236 public boolean isTrackingHeadsUp() { 237 return mTrackingHeadsUp; 238 } 239 240 @Override snooze()241 public void snooze() { 242 super.snooze(); 243 mReleaseOnExpandFinish = true; 244 } 245 246 /** 247 * React to the removal of the notification in the heads up. 248 * 249 * @return true if the notification was removed and false if it still needs to be kept around 250 * for a bit since it wasn't shown long enough 251 */ 252 @Override removeNotification(@onNull String key, boolean ignoreEarliestRemovalTime)253 public boolean removeNotification(@NonNull String key, boolean ignoreEarliestRemovalTime) { 254 if (wasShownLongEnough(key) || ignoreEarliestRemovalTime) { 255 return super.removeNotification(key, ignoreEarliestRemovalTime); 256 } else { 257 HeadsUpEntryPhone entry = getHeadsUpEntryPhone(key); 258 entry.removeAsSoonAsPossible(); 259 return false; 260 } 261 } 262 addSwipedOutNotification(@onNull String key)263 public void addSwipedOutNotification(@NonNull String key) { 264 mSwipedOutKeys.add(key); 265 } 266 267 /////////////////////////////////////////////////////////////////////////////////////////////// 268 // Dumpable overrides: 269 270 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)271 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 272 pw.println("HeadsUpManagerPhone state:"); 273 dumpInternal(fd, pw, args); 274 } 275 276 /////////////////////////////////////////////////////////////////////////////////////////////// 277 // ViewTreeObserver.OnComputeInternalInsetsListener overrides: 278 279 /** 280 * Overridden from TreeObserver. 281 */ 282 @Override onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info)283 public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) { 284 if (mIsExpanded || mBar.isBouncerShowing()) { 285 // The touchable region is always the full area when expanded 286 return; 287 } 288 if (hasPinnedHeadsUp()) { 289 ExpandableNotificationRow topEntry = getTopEntry().row; 290 if (topEntry.isChildInGroup()) { 291 final ExpandableNotificationRow groupSummary 292 = mGroupManager.getGroupSummary(topEntry.getStatusBarNotification()); 293 if (groupSummary != null) { 294 topEntry = groupSummary; 295 } 296 } 297 topEntry.getLocationOnScreen(mTmpTwoArray); 298 int minX = mTmpTwoArray[0]; 299 int maxX = mTmpTwoArray[0] + topEntry.getWidth(); 300 int height = topEntry.getIntrinsicHeight(); 301 302 info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 303 info.touchableRegion.set(minX, 0, maxX, mHeadsUpInset + height); 304 } else if (mHeadsUpGoingAway || mWaitingOnCollapseWhenGoingAway) { 305 info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 306 info.touchableRegion.set(0, 0, mStatusBarWindowView.getWidth(), mStatusBarHeight); 307 } 308 } 309 310 @Override onConfigChanged(Configuration newConfig)311 public void onConfigChanged(Configuration newConfig) { 312 Resources resources = mContext.getResources(); 313 mStatusBarHeight = resources.getDimensionPixelSize( 314 com.android.internal.R.dimen.status_bar_height); 315 } 316 317 /////////////////////////////////////////////////////////////////////////////////////////////// 318 // VisualStabilityManager.Callback overrides: 319 320 @Override onReorderingAllowed()321 public void onReorderingAllowed() { 322 mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(false); 323 for (NotificationData.Entry entry : mEntriesToRemoveWhenReorderingAllowed) { 324 if (isHeadsUp(entry.key)) { 325 // Maybe the heads-up was removed already 326 removeHeadsUpEntry(entry); 327 } 328 } 329 mEntriesToRemoveWhenReorderingAllowed.clear(); 330 mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(true); 331 } 332 333 /////////////////////////////////////////////////////////////////////////////////////////////// 334 // HeadsUpManager utility (protected) methods overrides: 335 336 @Override createHeadsUpEntry()337 protected HeadsUpEntry createHeadsUpEntry() { 338 return mEntryPool.acquire(); 339 } 340 341 @Override releaseHeadsUpEntry(HeadsUpEntry entry)342 protected void releaseHeadsUpEntry(HeadsUpEntry entry) { 343 entry.reset(); 344 mEntryPool.release((HeadsUpEntryPhone) entry); 345 } 346 347 @Override shouldHeadsUpBecomePinned(NotificationData.Entry entry)348 protected boolean shouldHeadsUpBecomePinned(NotificationData.Entry entry) { 349 return mStatusBarState != StatusBarState.KEYGUARD && !mIsExpanded 350 || super.shouldHeadsUpBecomePinned(entry); 351 } 352 353 @Override dumpInternal(FileDescriptor fd, PrintWriter pw, String[] args)354 protected void dumpInternal(FileDescriptor fd, PrintWriter pw, String[] args) { 355 super.dumpInternal(fd, pw, args); 356 pw.print(" mStatusBarState="); pw.println(mStatusBarState); 357 } 358 359 /////////////////////////////////////////////////////////////////////////////////////////////// 360 // Private utility methods: 361 362 @Nullable getHeadsUpEntryPhone(@onNull String key)363 private HeadsUpEntryPhone getHeadsUpEntryPhone(@NonNull String key) { 364 return (HeadsUpEntryPhone) getHeadsUpEntry(key); 365 } 366 367 @Nullable getTopHeadsUpEntryPhone()368 private HeadsUpEntryPhone getTopHeadsUpEntryPhone() { 369 return (HeadsUpEntryPhone) getTopHeadsUpEntry(); 370 } 371 wasShownLongEnough(@onNull String key)372 private boolean wasShownLongEnough(@NonNull String key) { 373 if (mSwipedOutKeys.contains(key)) { 374 // We always instantly dismiss views being manually swiped out. 375 mSwipedOutKeys.remove(key); 376 return true; 377 } 378 379 HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(key); 380 HeadsUpEntryPhone topEntry = getTopHeadsUpEntryPhone(); 381 return headsUpEntry != topEntry || headsUpEntry.wasShownLongEnough(); 382 } 383 384 /** 385 * We need to wait on the whole panel to collapse, before we can remove the touchable region 386 * listener. 387 */ waitForStatusBarLayout()388 private void waitForStatusBarLayout() { 389 mWaitingOnCollapseWhenGoingAway = true; 390 mStatusBarWindowView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 391 @Override 392 public void onLayoutChange(View v, int left, int top, int right, int bottom, 393 int oldLeft, 394 int oldTop, int oldRight, int oldBottom) { 395 if (mStatusBarWindowView.getHeight() <= mStatusBarHeight) { 396 mStatusBarWindowView.removeOnLayoutChangeListener(this); 397 mWaitingOnCollapseWhenGoingAway = false; 398 updateTouchableRegionListener(); 399 } 400 } 401 }); 402 } 403 updateTouchableRegionListener()404 private void updateTouchableRegionListener() { 405 boolean shouldObserve = hasPinnedHeadsUp() || mHeadsUpGoingAway 406 || mWaitingOnCollapseWhenGoingAway; 407 if (shouldObserve == mIsObserving) { 408 return; 409 } 410 if (shouldObserve) { 411 mStatusBarWindowView.getViewTreeObserver().addOnComputeInternalInsetsListener(this); 412 mStatusBarWindowView.requestLayout(); 413 } else { 414 mStatusBarWindowView.getViewTreeObserver().removeOnComputeInternalInsetsListener(this); 415 } 416 mIsObserving = shouldObserve; 417 } 418 419 /////////////////////////////////////////////////////////////////////////////////////////////// 420 // HeadsUpEntryPhone: 421 422 protected class HeadsUpEntryPhone extends HeadsUpManager.HeadsUpEntry { setEntry(@onNull final NotificationData.Entry entry)423 public void setEntry(@NonNull final NotificationData.Entry entry) { 424 Runnable removeHeadsUpRunnable = () -> { 425 if (!mVisualStabilityManager.isReorderingAllowed()) { 426 mEntriesToRemoveWhenReorderingAllowed.add(entry); 427 mVisualStabilityManager.addReorderingAllowedCallback( 428 HeadsUpManagerPhone.this); 429 } else if (!mTrackingHeadsUp) { 430 removeHeadsUpEntry(entry); 431 } else { 432 mEntriesToRemoveAfterExpand.add(entry); 433 } 434 }; 435 436 super.setEntry(entry, removeHeadsUpRunnable); 437 } 438 wasShownLongEnough()439 public boolean wasShownLongEnough() { 440 return earliestRemovaltime < mClock.currentTimeMillis(); 441 } 442 443 @Override updateEntry(boolean updatePostTime)444 public void updateEntry(boolean updatePostTime) { 445 super.updateEntry(updatePostTime); 446 447 if (mEntriesToRemoveAfterExpand.contains(entry)) { 448 mEntriesToRemoveAfterExpand.remove(entry); 449 } 450 if (mEntriesToRemoveWhenReorderingAllowed.contains(entry)) { 451 mEntriesToRemoveWhenReorderingAllowed.remove(entry); 452 } 453 } 454 455 @Override expanded(boolean expanded)456 public void expanded(boolean expanded) { 457 if (this.expanded == expanded) { 458 return; 459 } 460 461 this.expanded = expanded; 462 if (expanded) { 463 removeAutoRemovalCallbacks(); 464 } else { 465 updateEntry(false /* updatePostTime */); 466 } 467 } 468 } 469 } 470