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.ArrayMap; 26 import android.util.Pools; 27 28 import androidx.collection.ArraySet; 29 30 import com.android.internal.annotations.VisibleForTesting; 31 import com.android.internal.logging.UiEventLogger; 32 import com.android.internal.policy.SystemBarUtils; 33 import com.android.systemui.dagger.SysUISingleton; 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.res.R; 38 import com.android.systemui.shade.domain.interactor.ShadeInteractor; 39 import com.android.systemui.statusbar.StatusBarState; 40 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 41 import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener; 42 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider; 43 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; 44 import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository; 45 import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository; 46 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 47 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor; 48 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; 49 import com.android.systemui.statusbar.policy.AnimationStateHandler; 50 import com.android.systemui.statusbar.policy.AvalancheController; 51 import com.android.systemui.statusbar.policy.BaseHeadsUpManager; 52 import com.android.systemui.statusbar.policy.ConfigurationController; 53 import com.android.systemui.statusbar.policy.HeadsUpManagerLogger; 54 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; 55 import com.android.systemui.statusbar.policy.OnHeadsUpPhoneListenerChange; 56 import com.android.systemui.util.concurrency.DelayableExecutor; 57 import com.android.systemui.util.kotlin.JavaAdapter; 58 import com.android.systemui.util.settings.GlobalSettings; 59 import com.android.systemui.util.time.SystemClock; 60 61 import kotlinx.coroutines.flow.Flow; 62 import kotlinx.coroutines.flow.MutableStateFlow; 63 import kotlinx.coroutines.flow.StateFlow; 64 import kotlinx.coroutines.flow.StateFlowKt; 65 66 import java.io.PrintWriter; 67 import java.util.ArrayList; 68 import java.util.HashSet; 69 import java.util.List; 70 import java.util.Objects; 71 import java.util.Set; 72 import java.util.Stack; 73 74 import javax.inject.Inject; 75 76 /** A implementation of HeadsUpManager for phone. */ 77 @SysUISingleton 78 public class HeadsUpManagerPhone extends BaseHeadsUpManager implements 79 HeadsUpRepository, OnHeadsUpChangedListener { 80 private static final String TAG = "HeadsUpManagerPhone"; 81 82 @VisibleForTesting 83 public final int mExtensionTime; 84 private final KeyguardBypassController mBypassController; 85 private final GroupMembershipManager mGroupMembershipManager; 86 private final List<OnHeadsUpPhoneListenerChange> mHeadsUpPhoneListeners = new ArrayList<>(); 87 private final VisualStabilityProvider mVisualStabilityProvider; 88 89 private final AvalancheController mAvalancheController; 90 91 // TODO(b/328393698) move the topHeadsUpRow logic to an interactor 92 private final MutableStateFlow<HeadsUpRowRepository> mTopHeadsUpRow = 93 StateFlowKt.MutableStateFlow(null); 94 private final MutableStateFlow<Set<HeadsUpRowRepository>> mHeadsUpNotificationRows = 95 StateFlowKt.MutableStateFlow(new HashSet<>()); 96 private final MutableStateFlow<Boolean> mHeadsUpAnimatingAway = 97 StateFlowKt.MutableStateFlow(false); 98 private boolean mReleaseOnExpandFinish; 99 private boolean mTrackingHeadsUp; 100 private final HashSet<String> mSwipedOutKeys = new HashSet<>(); 101 private final HashSet<NotificationEntry> mEntriesToRemoveAfterExpand = new HashSet<>(); 102 private final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed 103 = new ArraySet<>(); 104 private boolean mIsExpanded; 105 private int mStatusBarState; 106 private AnimationStateHandler mAnimationStateHandler; 107 private int mHeadsUpInset; 108 109 // Used for determining the region for touch interaction 110 private final Region mTouchableRegion = new Region(); 111 112 private final Pools.Pool<HeadsUpEntryPhone> mEntryPool = new Pools.Pool<HeadsUpEntryPhone>() { 113 private Stack<HeadsUpEntryPhone> mPoolObjects = new Stack<>(); 114 115 @Override 116 public HeadsUpEntryPhone acquire() { 117 NotificationsHeadsUpRefactor.assertInLegacyMode(); 118 if (!mPoolObjects.isEmpty()) { 119 return mPoolObjects.pop(); 120 } 121 return new HeadsUpEntryPhone(); 122 } 123 124 @Override 125 public boolean release(@NonNull HeadsUpEntryPhone instance) { 126 NotificationsHeadsUpRefactor.assertInLegacyMode(); 127 mPoolObjects.push(instance); 128 return true; 129 } 130 }; 131 132 /////////////////////////////////////////////////////////////////////////////////////////////// 133 // Constructor: 134 @Inject HeadsUpManagerPhone( @onNull final Context context, HeadsUpManagerLogger logger, StatusBarStateController statusBarStateController, KeyguardBypassController bypassController, GroupMembershipManager groupMembershipManager, VisualStabilityProvider visualStabilityProvider, ConfigurationController configurationController, @Main Handler handler, GlobalSettings globalSettings, SystemClock systemClock, @Main DelayableExecutor executor, AccessibilityManagerWrapper accessibilityManagerWrapper, UiEventLogger uiEventLogger, JavaAdapter javaAdapter, ShadeInteractor shadeInteractor, AvalancheController avalancheController)135 public HeadsUpManagerPhone( 136 @NonNull final Context context, 137 HeadsUpManagerLogger logger, 138 StatusBarStateController statusBarStateController, 139 KeyguardBypassController bypassController, 140 GroupMembershipManager groupMembershipManager, 141 VisualStabilityProvider visualStabilityProvider, 142 ConfigurationController configurationController, 143 @Main Handler handler, 144 GlobalSettings globalSettings, 145 SystemClock systemClock, 146 @Main DelayableExecutor executor, 147 AccessibilityManagerWrapper accessibilityManagerWrapper, 148 UiEventLogger uiEventLogger, 149 JavaAdapter javaAdapter, 150 ShadeInteractor shadeInteractor, 151 AvalancheController avalancheController) { 152 super(context, logger, handler, globalSettings, systemClock, executor, 153 accessibilityManagerWrapper, uiEventLogger, avalancheController); 154 Resources resources = mContext.getResources(); 155 mExtensionTime = resources.getInteger(R.integer.ambient_notification_extension_time); 156 statusBarStateController.addCallback(mStatusBarStateListener); 157 mBypassController = bypassController; 158 mGroupMembershipManager = groupMembershipManager; 159 mVisualStabilityProvider = visualStabilityProvider; 160 mAvalancheController = avalancheController; 161 162 updateResources(); 163 configurationController.addCallback(new ConfigurationController.ConfigurationListener() { 164 @Override 165 public void onDensityOrFontScaleChanged() { 166 updateResources(); 167 } 168 169 @Override 170 public void onThemeChanged() { 171 updateResources(); 172 } 173 }); 174 if (!NotificationsHeadsUpRefactor.isEnabled()) { 175 javaAdapter.alwaysCollectFlow(shadeInteractor.isAnyExpanded(), 176 this::onShadeOrQsExpanded); 177 } 178 } 179 setAnimationStateHandler(AnimationStateHandler handler)180 public void setAnimationStateHandler(AnimationStateHandler handler) { 181 mAnimationStateHandler = handler; 182 } 183 updateResources()184 private void updateResources() { 185 Resources resources = mContext.getResources(); 186 mHeadsUpInset = SystemBarUtils.getStatusBarHeight(mContext) 187 + resources.getDimensionPixelSize(R.dimen.heads_up_status_bar_padding); 188 } 189 190 /////////////////////////////////////////////////////////////////////////////////////////////// 191 // Public methods: 192 193 /** 194 * Add a listener to receive callbacks {@link #setHeadsUpAnimatingAway(boolean)} 195 */ 196 @Override addHeadsUpPhoneListener(OnHeadsUpPhoneListenerChange listener)197 public void addHeadsUpPhoneListener(OnHeadsUpPhoneListenerChange listener) { 198 mHeadsUpPhoneListeners.add(listener); 199 } 200 201 /** 202 * Gets the touchable region needed for heads up notifications. Returns null if no touchable 203 * region is required (ie: no heads up notification currently exists). 204 */ 205 // TODO(b/347007367): With scene container enabled this method may report outdated regions 206 @Override getTouchableRegion()207 public @Nullable Region getTouchableRegion() { 208 NotificationEntry topEntry = getTopEntry(); 209 210 // This call could be made in an inconsistent state while the pinnedMode hasn't been 211 // updated yet, but callbacks leading out of the headsUp manager, querying it. Let's 212 // therefore also check if the topEntry is null. 213 if (!hasPinnedHeadsUp() || topEntry == null) { 214 return null; 215 } else { 216 if (topEntry.rowIsChildInGroup()) { 217 final NotificationEntry groupSummary = 218 mGroupMembershipManager.getGroupSummary(topEntry); 219 if (groupSummary != null) { 220 topEntry = groupSummary; 221 } 222 } 223 ExpandableNotificationRow topRow = topEntry.getRow(); 224 int[] tmpArray = new int[2]; 225 topRow.getLocationOnScreen(tmpArray); 226 int minX = tmpArray[0]; 227 int maxX = tmpArray[0] + topRow.getWidth(); 228 int height = topRow.getIntrinsicHeight(); 229 final boolean stretchToTop = tmpArray[1] <= mHeadsUpInset; 230 mTouchableRegion.set(minX, stretchToTop ? 0 : tmpArray[1], maxX, tmpArray[1] + height); 231 return mTouchableRegion; 232 } 233 } 234 235 /** 236 * Decides whether a click is invalid for a notification, i.e it has not been shown long enough 237 * that a user might have consciously clicked on it. 238 * 239 * @param key the key of the touched notification 240 * @return whether the touch is invalid and should be discarded 241 */ 242 @Override shouldSwallowClick(@onNull String key)243 public boolean shouldSwallowClick(@NonNull String key) { 244 BaseHeadsUpManager.HeadsUpEntry entry = getHeadsUpEntry(key); 245 return entry != null && mSystemClock.elapsedRealtime() < entry.mPostTime; 246 } 247 onExpandingFinished()248 public void onExpandingFinished() { 249 if (mReleaseOnExpandFinish) { 250 releaseAllImmediately(); 251 mReleaseOnExpandFinish = false; 252 } else { 253 for (NotificationEntry entry : mEntriesToRemoveAfterExpand) { 254 if (isHeadsUpEntry(entry.getKey())) { 255 // Maybe the heads-up was removed already 256 removeEntry(entry.getKey(), "onExpandingFinished"); 257 } 258 } 259 } 260 mEntriesToRemoveAfterExpand.clear(); 261 } 262 263 /** 264 * Sets the tracking-heads-up flag. If the flag is true, HeadsUpManager doesn't remove the entry 265 * from the list even after a Heads Up Notification is gone. 266 */ setTrackingHeadsUp(boolean trackingHeadsUp)267 public void setTrackingHeadsUp(boolean trackingHeadsUp) { 268 mTrackingHeadsUp = trackingHeadsUp; 269 } 270 onShadeOrQsExpanded(Boolean isExpanded)271 private void onShadeOrQsExpanded(Boolean isExpanded) { 272 NotificationsHeadsUpRefactor.assertInLegacyMode(); 273 if (isExpanded != mIsExpanded) { 274 mIsExpanded = isExpanded; 275 if (isExpanded) { 276 mHeadsUpAnimatingAway.setValue(false); 277 } 278 } 279 } 280 281 /** 282 * Set that we are exiting the headsUp pinned mode, but some notifications might still be 283 * animating out. This is used to keep the touchable regions in a reasonable state. 284 */ 285 @Override setHeadsUpAnimatingAway(boolean headsUpAnimatingAway)286 public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) { 287 if (headsUpAnimatingAway != mHeadsUpAnimatingAway.getValue()) { 288 for (OnHeadsUpPhoneListenerChange listener : mHeadsUpPhoneListeners) { 289 listener.onHeadsUpAnimatingAwayStateChanged(headsUpAnimatingAway); 290 } 291 mHeadsUpAnimatingAway.setValue(headsUpAnimatingAway); 292 } 293 } 294 295 /** 296 * Notifies that a remote input textbox in notification gets active or inactive. 297 * 298 * @param entry The entry of the target notification. 299 * @param remoteInputActive True to notify active, False to notify inactive. 300 */ setRemoteInputActive( @onNull NotificationEntry entry, boolean remoteInputActive)301 public void setRemoteInputActive( 302 @NonNull NotificationEntry entry, boolean remoteInputActive) { 303 HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(entry.getKey()); 304 if (headsUpEntry != null && headsUpEntry.mRemoteInputActive != remoteInputActive) { 305 headsUpEntry.mRemoteInputActive = remoteInputActive; 306 if (remoteInputActive) { 307 headsUpEntry.cancelAutoRemovalCallbacks("setRemoteInputActive(true)"); 308 } else { 309 headsUpEntry.updateEntry(false /* updatePostTime */, "setRemoteInputActive(false)"); 310 } 311 onEntryUpdated(headsUpEntry); 312 } 313 } 314 315 /** 316 * Sets whether an entry's guts are exposed and therefore it should stick in the heads up 317 * area if it's pinned until it's hidden again. 318 */ setGutsShown(@onNull NotificationEntry entry, boolean gutsShown)319 public void setGutsShown(@NonNull NotificationEntry entry, boolean gutsShown) { 320 HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey()); 321 if (!(headsUpEntry instanceof HeadsUpEntryPhone)) return; 322 HeadsUpEntryPhone headsUpEntryPhone = (HeadsUpEntryPhone)headsUpEntry; 323 if (entry.isRowPinned() || !gutsShown) { 324 headsUpEntryPhone.setGutsShownPinned(gutsShown); 325 } 326 } 327 328 /** 329 * Extends the lifetime of the currently showing pulsing notification so that the pulse lasts 330 * longer. 331 */ extendHeadsUp()332 public void extendHeadsUp() { 333 HeadsUpEntryPhone topEntry = getTopHeadsUpEntryPhone(); 334 if (topEntry == null) { 335 return; 336 } 337 topEntry.extendPulse(); 338 } 339 340 /////////////////////////////////////////////////////////////////////////////////////////////// 341 // HeadsUpManager public methods overrides and overloads: 342 343 @Override isTrackingHeadsUp()344 public boolean isTrackingHeadsUp() { 345 return mTrackingHeadsUp; 346 } 347 348 @Override snooze()349 public void snooze() { 350 super.snooze(); 351 mReleaseOnExpandFinish = true; 352 } 353 addSwipedOutNotification(@onNull String key)354 public void addSwipedOutNotification(@NonNull String key) { 355 mSwipedOutKeys.add(key); 356 } 357 358 @Override removeNotification(@onNull String key, boolean releaseImmediately, boolean animate)359 public boolean removeNotification(@NonNull String key, boolean releaseImmediately, 360 boolean animate) { 361 if (animate) { 362 return removeNotification(key, releaseImmediately); 363 } else { 364 mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(false); 365 boolean removed = removeNotification(key, releaseImmediately); 366 mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(true); 367 return removed; 368 } 369 } 370 371 /////////////////////////////////////////////////////////////////////////////////////////////// 372 // Dumpable overrides: 373 374 @Override dump(PrintWriter pw, String[] args)375 public void dump(PrintWriter pw, String[] args) { 376 pw.println("HeadsUpManagerPhone state:"); 377 dumpInternal(pw, args); 378 } 379 380 /////////////////////////////////////////////////////////////////////////////////////////////// 381 // OnReorderingAllowedListener: 382 383 private final OnReorderingAllowedListener mOnReorderingAllowedListener = () -> { 384 mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(false); 385 for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) { 386 if (isHeadsUpEntry(entry.getKey())) { 387 // Maybe the heads-up was removed already 388 removeEntry(entry.getKey(), "mOnReorderingAllowedListener"); 389 } 390 } 391 mEntriesToRemoveWhenReorderingAllowed.clear(); 392 mAnimationStateHandler.setHeadsUpGoingAwayAnimationsAllowed(true); 393 }; 394 395 /////////////////////////////////////////////////////////////////////////////////////////////// 396 // HeadsUpManager utility (protected) methods overrides: 397 398 @NonNull 399 @Override createHeadsUpEntry(NotificationEntry entry)400 protected HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) { 401 if (NotificationsHeadsUpRefactor.isEnabled()) { 402 return new HeadsUpEntryPhone(entry); 403 } else { 404 HeadsUpEntryPhone headsUpEntry = mEntryPool.acquire(); 405 headsUpEntry.setEntry(entry); 406 return headsUpEntry; 407 } 408 } 409 410 @Override onEntryAdded(HeadsUpEntry headsUpEntry)411 protected void onEntryAdded(HeadsUpEntry headsUpEntry) { 412 super.onEntryAdded(headsUpEntry); 413 updateTopHeadsUpFlow(); 414 updateHeadsUpFlow(); 415 } 416 417 @Override onEntryUpdated(HeadsUpEntry headsUpEntry)418 protected void onEntryUpdated(HeadsUpEntry headsUpEntry) { 419 super.onEntryUpdated(headsUpEntry); 420 // no need to update the list here 421 updateTopHeadsUpFlow(); 422 } 423 424 @Override onEntryRemoved(HeadsUpEntry headsUpEntry)425 protected void onEntryRemoved(HeadsUpEntry headsUpEntry) { 426 super.onEntryRemoved(headsUpEntry); 427 if (!NotificationsHeadsUpRefactor.isEnabled()) { 428 mEntryPool.release((HeadsUpEntryPhone) headsUpEntry); 429 } 430 updateTopHeadsUpFlow(); 431 updateHeadsUpFlow(); 432 } 433 updateTopHeadsUpFlow()434 private void updateTopHeadsUpFlow() { 435 mTopHeadsUpRow.setValue((HeadsUpRowRepository) getTopHeadsUpEntry()); 436 } 437 updateHeadsUpFlow()438 private void updateHeadsUpFlow() { 439 mHeadsUpNotificationRows.setValue(new HashSet<>(getHeadsUpEntryPhoneMap().values())); 440 } 441 442 @Override shouldHeadsUpBecomePinned(NotificationEntry entry)443 protected boolean shouldHeadsUpBecomePinned(NotificationEntry entry) { 444 boolean pin = mStatusBarState == StatusBarState.SHADE && !mIsExpanded; 445 if (mBypassController.getBypassEnabled()) { 446 pin |= mStatusBarState == StatusBarState.KEYGUARD; 447 } 448 return pin || super.shouldHeadsUpBecomePinned(entry); 449 } 450 451 @Override dumpInternal(PrintWriter pw, String[] args)452 protected void dumpInternal(PrintWriter pw, String[] args) { 453 super.dumpInternal(pw, args); 454 pw.print(" mBarState="); 455 pw.println(mStatusBarState); 456 pw.print(" mTouchableRegion="); 457 pw.println(mTouchableRegion); 458 } 459 460 /////////////////////////////////////////////////////////////////////////////////////////////// 461 // Private utility methods: 462 463 @NonNull getHeadsUpEntryPhoneMap()464 private ArrayMap<String, HeadsUpEntryPhone> getHeadsUpEntryPhoneMap() { 465 //noinspection unchecked 466 return (ArrayMap<String, HeadsUpEntryPhone>) ((ArrayMap) mHeadsUpEntryMap); 467 } 468 469 @Nullable getHeadsUpEntryPhone(@onNull String key)470 private HeadsUpEntryPhone getHeadsUpEntryPhone(@NonNull String key) { 471 return (HeadsUpEntryPhone) mHeadsUpEntryMap.get(key); 472 } 473 474 @Nullable getTopHeadsUpEntryPhone()475 private HeadsUpEntryPhone getTopHeadsUpEntryPhone() { 476 if (NotificationsHeadsUpRefactor.isEnabled()) { 477 return (HeadsUpEntryPhone) mTopHeadsUpRow.getValue(); 478 } else { 479 return (HeadsUpEntryPhone) getTopHeadsUpEntry(); 480 } 481 } 482 483 @Override canRemoveImmediately(@onNull String key)484 public boolean canRemoveImmediately(@NonNull String key) { 485 if (mSwipedOutKeys.contains(key)) { 486 // We always instantly dismiss views being manually swiped out. 487 mSwipedOutKeys.remove(key); 488 return true; 489 } 490 491 HeadsUpEntryPhone headsUpEntry = getHeadsUpEntryPhone(key); 492 HeadsUpEntryPhone topEntry = getTopHeadsUpEntryPhone(); 493 494 return headsUpEntry == null || headsUpEntry != topEntry || super.canRemoveImmediately(key); 495 } 496 497 @Override 498 @NonNull getTopHeadsUpRow()499 public Flow<HeadsUpRowRepository> getTopHeadsUpRow() { 500 return mTopHeadsUpRow; 501 } 502 503 @Override 504 @NonNull getActiveHeadsUpRows()505 public Flow<Set<HeadsUpRowRepository>> getActiveHeadsUpRows() { 506 return mHeadsUpNotificationRows; 507 } 508 509 @Override 510 @NonNull isHeadsUpAnimatingAway()511 public StateFlow<Boolean> isHeadsUpAnimatingAway() { 512 return mHeadsUpAnimatingAway; 513 } 514 515 @Override isHeadsUpAnimatingAwayValue()516 public boolean isHeadsUpAnimatingAwayValue() { 517 return mHeadsUpAnimatingAway.getValue(); 518 } 519 520 /////////////////////////////////////////////////////////////////////////////////////////////// 521 // HeadsUpEntryPhone: 522 523 protected class HeadsUpEntryPhone extends BaseHeadsUpManager.HeadsUpEntry implements 524 HeadsUpRowRepository { 525 526 private boolean mGutsShownPinned; 527 private final MutableStateFlow<Boolean> mIsPinned = StateFlowKt.MutableStateFlow(false); 528 529 /** 530 * If the time this entry has been on was extended 531 */ 532 private boolean extended; 533 534 @Override isSticky()535 public boolean isSticky() { 536 return super.isSticky() || mGutsShownPinned; 537 } 538 HeadsUpEntryPhone()539 public HeadsUpEntryPhone() { 540 super(); 541 } 542 HeadsUpEntryPhone(NotificationEntry entry)543 public HeadsUpEntryPhone(NotificationEntry entry) { 544 super(entry); 545 } 546 547 @Override 548 @NonNull getKey()549 public String getKey() { 550 return requireEntry().getKey(); 551 } 552 553 @Override 554 @NonNull isPinned()555 public StateFlow<Boolean> isPinned() { 556 return mIsPinned; 557 } 558 559 @Override setRowPinned(boolean pinned)560 protected void setRowPinned(boolean pinned) { 561 // TODO(b/327624082): replace this super call with a ViewBinder 562 super.setRowPinned(pinned); 563 mIsPinned.setValue(pinned); 564 } 565 566 @Override createRemoveRunnable(NotificationEntry entry)567 protected Runnable createRemoveRunnable(NotificationEntry entry) { 568 return () -> { 569 if (!mVisualStabilityProvider.isReorderingAllowed() 570 // We don't want to allow reordering while pulsing, but headsup need to 571 // time out anyway 572 && !entry.showingPulsing()) { 573 mEntriesToRemoveWhenReorderingAllowed.add(entry); 574 mVisualStabilityProvider.addTemporaryReorderingAllowedListener( 575 mOnReorderingAllowedListener); 576 } else if (mTrackingHeadsUp) { 577 mEntriesToRemoveAfterExpand.add(entry); 578 } else { 579 removeEntry(entry.getKey(), "createRemoveRunnable"); 580 } 581 }; 582 } 583 584 @Override updateEntry(boolean updatePostTime, String reason)585 public void updateEntry(boolean updatePostTime, String reason) { 586 super.updateEntry(updatePostTime, reason); 587 588 if (mEntriesToRemoveAfterExpand.contains(mEntry)) { 589 mEntriesToRemoveAfterExpand.remove(mEntry); 590 } 591 if (mEntriesToRemoveWhenReorderingAllowed.contains(mEntry)) { 592 mEntriesToRemoveWhenReorderingAllowed.remove(mEntry); 593 } 594 } 595 596 @Override setExpanded(boolean expanded)597 public void setExpanded(boolean expanded) { 598 if (this.mExpanded == expanded) { 599 return; 600 } 601 602 this.mExpanded = expanded; 603 if (expanded) { 604 cancelAutoRemovalCallbacks("setExpanded(true)"); 605 } else { 606 updateEntry(false /* updatePostTime */, "setExpanded(false)"); 607 } 608 } 609 setGutsShownPinned(boolean gutsShownPinned)610 public void setGutsShownPinned(boolean gutsShownPinned) { 611 if (mGutsShownPinned == gutsShownPinned) { 612 return; 613 } 614 615 mGutsShownPinned = gutsShownPinned; 616 if (gutsShownPinned) { 617 cancelAutoRemovalCallbacks("setGutsShownPinned(true)"); 618 } else { 619 updateEntry(false /* updatePostTime */, "setGutsShownPinned(false)"); 620 } 621 } 622 623 @Override reset()624 public void reset() { 625 super.reset(); 626 mGutsShownPinned = false; 627 extended = false; 628 } 629 extendPulse()630 private void extendPulse() { 631 if (!extended) { 632 extended = true; 633 updateEntry(false, "extendPulse()"); 634 } 635 } 636 637 @Override calculateFinishTime()638 protected long calculateFinishTime() { 639 return super.calculateFinishTime() + (extended ? mExtensionTime : 0); 640 } 641 642 @Override 643 @NonNull getElementKey()644 public Object getElementKey() { 645 return requireEntry().getRow(); 646 } 647 requireEntry()648 private NotificationEntry requireEntry() { 649 /* check if */ NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode(); 650 return Objects.requireNonNull(mEntry); 651 } 652 } 653 654 private final StateListener mStatusBarStateListener = new StateListener() { 655 @Override 656 public void onStateChanged(int newState) { 657 boolean wasKeyguard = mStatusBarState == StatusBarState.KEYGUARD; 658 boolean isKeyguard = newState == StatusBarState.KEYGUARD; 659 mStatusBarState = newState; 660 661 if (wasKeyguard && !isKeyguard && mBypassController.getBypassEnabled()) { 662 ArrayList<String> keysToRemove = new ArrayList<>(); 663 for (HeadsUpEntry entry : getHeadsUpEntryList()) { 664 if (entry.mEntry != null && entry.mEntry.isBubble() && !entry.isSticky()) { 665 keysToRemove.add(entry.mEntry.getKey()); 666 } 667 } 668 for (String key : keysToRemove) { 669 removeEntry(key, "mStatusBarStateListener"); 670 } 671 } 672 } 673 674 @Override 675 public void onDozingChanged(boolean isDozing) { 676 if (!isDozing) { 677 // Let's make sure all huns we got while dozing time out within the normal timeout 678 // duration. Otherwise they could get stuck for a very long time 679 for (HeadsUpEntry entry : getHeadsUpEntryList()) { 680 entry.updateEntry(true /* updatePostTime */, "onDozingChanged(false)"); 681 } 682 } 683 } 684 }; 685 } 686