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 static com.android.systemui.statusbar.phone.fragment.dagger.HomeStatusBarModule.OPERATOR_NAME_FRAME_VIEW; 20 21 import android.graphics.Rect; 22 import android.util.MathUtils; 23 import android.view.View; 24 25 import androidx.annotation.NonNull; 26 import androidx.annotation.Nullable; 27 28 import com.android.internal.annotations.VisibleForTesting; 29 import com.android.internal.widget.ViewClippingUtil; 30 import com.android.systemui.dagger.qualifiers.DisplaySpecific; 31 import com.android.systemui.plugins.DarkIconDispatcher; 32 import com.android.systemui.plugins.statusbar.StatusBarStateController; 33 import com.android.systemui.res.R; 34 import com.android.systemui.shade.ShadeHeadsUpTracker; 35 import com.android.systemui.shade.ShadeViewController; 36 import com.android.systemui.statusbar.CommandQueue; 37 import com.android.systemui.statusbar.CrossFadeHelper; 38 import com.android.systemui.statusbar.HeadsUpStatusBarView; 39 import com.android.systemui.statusbar.StatusBarState; 40 import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; 41 import com.android.systemui.statusbar.core.StatusBarRootModernization; 42 import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior; 43 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; 44 import com.android.systemui.statusbar.notification.SourceType; 45 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 46 import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationIconInteractor; 47 import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; 48 import com.android.systemui.statusbar.notification.headsup.OnHeadsUpChangedListener; 49 import com.android.systemui.statusbar.notification.headsup.PinnedStatus; 50 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 51 import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation; 52 import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager; 53 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; 54 import com.android.systemui.statusbar.phone.fragment.dagger.HomeStatusBarScope; 55 import com.android.systemui.statusbar.policy.Clock; 56 import com.android.systemui.statusbar.policy.KeyguardStateController; 57 import com.android.systemui.util.ViewController; 58 59 import java.util.ArrayList; 60 import java.util.Optional; 61 import java.util.function.BiConsumer; 62 import java.util.function.Consumer; 63 64 import javax.inject.Inject; 65 import javax.inject.Named; 66 67 /** 68 * Controls the appearance of heads up notifications in the icon area and the header itself. 69 * It also controls the roundness of the heads up notifications and the pulsing notifications. 70 */ 71 @HomeStatusBarScope 72 public class HeadsUpAppearanceController extends ViewController<HeadsUpStatusBarView> 73 implements OnHeadsUpChangedListener, 74 DarkIconDispatcher.DarkReceiver, 75 NotificationWakeUpCoordinator.WakeUpListener { 76 public static final int CONTENT_FADE_DURATION = 110; 77 public static final int CONTENT_FADE_DELAY = 100; 78 79 private static final SourceType HEADS_UP = SourceType.from("HeadsUp"); 80 private static final SourceType PULSING = SourceType.from("Pulsing"); 81 private final HeadsUpManager mHeadsUpManager; 82 private final NotificationStackScrollLayoutController mStackScrollerController; 83 84 private final DarkIconDispatcher mDarkIconDispatcher; 85 private final ShadeViewController mShadeViewController; 86 private final NotificationRoundnessManager mNotificationRoundnessManager; 87 private final Consumer<ExpandableNotificationRow> 88 mSetTrackingHeadsUp = this::setTrackingHeadsUp; 89 private final BiConsumer<Float, Float> mSetExpandedHeight = this::setAppearFraction; 90 private final KeyguardBypassController mBypassController; 91 private final StatusBarStateController mStatusBarStateController; 92 private final PhoneStatusBarTransitions mPhoneStatusBarTransitions; 93 private final CommandQueue mCommandQueue; 94 private final NotificationWakeUpCoordinator mWakeUpCoordinator; 95 96 private final View mClockView; 97 private final Optional<View> mOperatorNameViewOptional; 98 99 @VisibleForTesting 100 float mExpandedHeight; 101 @VisibleForTesting 102 float mAppearFraction; 103 private ExpandableNotificationRow mTrackedChild; 104 private PinnedStatus mPinnedStatus = PinnedStatus.NotPinned; 105 private final ViewClippingUtil.ClippingParameters mParentClippingParams = 106 new ViewClippingUtil.ClippingParameters() { 107 @Override 108 public boolean shouldFinish(View view) { 109 return view.getId() == R.id.status_bar; 110 } 111 }; 112 private boolean mAnimationsEnabled = true; 113 private final KeyguardStateController mKeyguardStateController; 114 private final HeadsUpNotificationIconInteractor mHeadsUpNotificationIconInteractor; 115 116 @VisibleForTesting 117 @Inject HeadsUpAppearanceController( HeadsUpManager headsUpManager, StatusBarStateController stateController, PhoneStatusBarTransitions phoneStatusBarTransitions, KeyguardBypassController bypassController, NotificationWakeUpCoordinator wakeUpCoordinator, @DisplaySpecific DarkIconDispatcher darkIconDispatcher, KeyguardStateController keyguardStateController, CommandQueue commandQueue, NotificationStackScrollLayoutController stackScrollerController, ShadeViewController shadeViewController, NotificationRoundnessManager notificationRoundnessManager, HeadsUpStatusBarView headsUpStatusBarView, Clock clockView, HeadsUpNotificationIconInteractor headsUpNotificationIconInteractor, @Named(OPERATOR_NAME_FRAME_VIEW) Optional<View> operatorNameViewOptional)118 public HeadsUpAppearanceController( 119 HeadsUpManager headsUpManager, 120 StatusBarStateController stateController, 121 PhoneStatusBarTransitions phoneStatusBarTransitions, 122 KeyguardBypassController bypassController, 123 NotificationWakeUpCoordinator wakeUpCoordinator, 124 @DisplaySpecific DarkIconDispatcher darkIconDispatcher, 125 KeyguardStateController keyguardStateController, 126 CommandQueue commandQueue, 127 NotificationStackScrollLayoutController stackScrollerController, 128 ShadeViewController shadeViewController, 129 NotificationRoundnessManager notificationRoundnessManager, 130 HeadsUpStatusBarView headsUpStatusBarView, 131 Clock clockView, 132 HeadsUpNotificationIconInteractor headsUpNotificationIconInteractor, 133 @Named(OPERATOR_NAME_FRAME_VIEW) Optional<View> operatorNameViewOptional) { 134 super(headsUpStatusBarView); 135 mNotificationRoundnessManager = notificationRoundnessManager; 136 mHeadsUpManager = headsUpManager; 137 138 // We may be mid-HUN-expansion when this controller is re-created (for example, if the user 139 // has started pulling down the notification shade from the HUN and then the font size 140 // changes). We need to re-fetch these values since they're used to correctly display the 141 // HUN during this shade expansion. 142 mTrackedChild = shadeViewController.getShadeHeadsUpTracker() 143 .getTrackedHeadsUpNotification(); 144 mAppearFraction = stackScrollerController.getAppearFraction(); 145 mExpandedHeight = stackScrollerController.getExpandedHeight(); 146 147 mStackScrollerController = stackScrollerController; 148 mShadeViewController = shadeViewController; 149 mHeadsUpNotificationIconInteractor = headsUpNotificationIconInteractor; 150 mStackScrollerController.setHeadsUpAppearanceController(this); 151 mClockView = clockView; 152 mOperatorNameViewOptional = operatorNameViewOptional; 153 mDarkIconDispatcher = darkIconDispatcher; 154 155 if (!StatusBarNoHunBehavior.isEnabled()) { 156 mView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 157 @Override 158 public void onLayoutChange(View v, int left, int top, int right, int bottom, 159 int oldLeft, int oldTop, int oldRight, int oldBottom) { 160 if (shouldHeadsUpStatusBarBeVisible()) { 161 updatePinnedStatus(); 162 163 // trigger scroller to notify the latest panel translation 164 mStackScrollerController.requestLayout(); 165 } 166 mView.removeOnLayoutChangeListener(this); 167 } 168 }); 169 } 170 mBypassController = bypassController; 171 mStatusBarStateController = stateController; 172 mPhoneStatusBarTransitions = phoneStatusBarTransitions; 173 mWakeUpCoordinator = wakeUpCoordinator; 174 mCommandQueue = commandQueue; 175 mKeyguardStateController = keyguardStateController; 176 } 177 178 @Override onViewAttached()179 protected void onViewAttached() { 180 mHeadsUpManager.addListener(this); 181 if (!StatusBarNoHunBehavior.isEnabled()) { 182 mView.setOnDrawingRectChangedListener(this::updateIsolatedIconLocation); 183 updateIsolatedIconLocation(); 184 mDarkIconDispatcher.addDarkReceiver(this); 185 mWakeUpCoordinator.addListener(this); 186 } 187 getShadeHeadsUpTracker().addTrackingHeadsUpListener(mSetTrackingHeadsUp); 188 getShadeHeadsUpTracker().setHeadsUpAppearanceController(this); 189 mStackScrollerController.addOnExpandedHeightChangedListener(mSetExpandedHeight); 190 } 191 getShadeHeadsUpTracker()192 private ShadeHeadsUpTracker getShadeHeadsUpTracker() { 193 return mShadeViewController.getShadeHeadsUpTracker(); 194 } 195 196 @Override onViewDetached()197 protected void onViewDetached() { 198 mHeadsUpManager.removeListener(this); 199 if (!StatusBarNoHunBehavior.isEnabled()) { 200 mView.setOnDrawingRectChangedListener(null); 201 mHeadsUpNotificationIconInteractor.setIsolatedIconLocation(null); 202 mDarkIconDispatcher.removeDarkReceiver(this); 203 mWakeUpCoordinator.removeListener(this); 204 } 205 getShadeHeadsUpTracker().removeTrackingHeadsUpListener(mSetTrackingHeadsUp); 206 getShadeHeadsUpTracker().setHeadsUpAppearanceController(null); 207 mStackScrollerController.removeOnExpandedHeightChangedListener(mSetExpandedHeight); 208 } 209 updateIsolatedIconLocation()210 private void updateIsolatedIconLocation() { 211 StatusBarNoHunBehavior.assertInLegacyMode(); 212 mHeadsUpNotificationIconInteractor.setIsolatedIconLocation(mView.getIconDrawingRect()); 213 } 214 215 @Override onHeadsUpPinned(NotificationEntry entry)216 public void onHeadsUpPinned(NotificationEntry entry) { 217 updatePinnedStatus(); 218 updateHeader(entry.getRow()); 219 updateHeadsUpAndPulsingRoundness(entry.getRow()); 220 } 221 222 @Override onHeadsUpStateChanged(@onNull NotificationEntry entry, boolean isHeadsUp)223 public void onHeadsUpStateChanged(@NonNull NotificationEntry entry, boolean isHeadsUp) { 224 updateHeadsUpAndPulsingRoundness(entry.getRow()); 225 mPhoneStatusBarTransitions.onHeadsUpStateChanged(isHeadsUp); 226 } 227 updatePinnedStatus()228 private void updatePinnedStatus() { 229 if (StatusBarNoHunBehavior.isEnabled()) { 230 return; 231 } 232 NotificationEntry newEntry = null; 233 if (shouldHeadsUpStatusBarBeVisible()) { 234 newEntry = mHeadsUpManager.getTopEntry(); 235 } 236 NotificationEntry previousEntry = mView.getShowingEntry(); 237 mView.setEntry(newEntry); 238 if (newEntry != previousEntry) { 239 if (newEntry == null) { 240 // No longer heads up 241 setPinnedStatus(PinnedStatus.NotPinned); 242 } else if (previousEntry == null) { 243 // We now have a heads up when we didn't have one before 244 setPinnedStatus(newEntry.getPinnedStatus()); 245 } 246 247 mHeadsUpNotificationIconInteractor.setIsolatedIconNotificationKey( 248 getIsolatedIconKey(newEntry)); 249 } 250 } 251 getIsolatedIconKey(NotificationEntry newEntry)252 private static @Nullable String getIsolatedIconKey(NotificationEntry newEntry) { 253 StatusBarNoHunBehavior.assertInLegacyMode(); 254 if (newEntry == null) { 255 return null; 256 } 257 if (StatusBarNotifChips.isEnabled()) { 258 // If the flag is on, only show the isolated icon if the HUN is pinned by the 259 // *system*. (If the HUN was pinned by the user, then the user tapped the 260 // notification status bar chip and we want to keep the chip showing.) 261 if (newEntry.getPinnedStatus() == PinnedStatus.PinnedBySystem) { 262 return newEntry.getRepresentativeEntry().getKey(); 263 } else { 264 return null; 265 } 266 } else { 267 // If the flag is off, we know all HUNs are pinned by the system and should show 268 // the isolated icon 269 return newEntry.getRepresentativeEntry().getKey(); 270 } 271 } 272 setPinnedStatus(PinnedStatus pinnedStatus)273 private void setPinnedStatus(PinnedStatus pinnedStatus) { 274 if (StatusBarNoHunBehavior.isEnabled()) { 275 return; 276 } 277 if (mPinnedStatus != pinnedStatus) { 278 mPinnedStatus = pinnedStatus; 279 280 boolean shouldShowHunStatusBar = StatusBarNotifChips.isEnabled() 281 ? mPinnedStatus == PinnedStatus.PinnedBySystem 282 // If the flag isn't enabled, all HUNs get the normal treatment. 283 : mPinnedStatus.isPinned(); 284 if (shouldShowHunStatusBar) { 285 updateParentClipping(false /* shouldClip */); 286 mView.setVisibility(View.VISIBLE); 287 show(mView); 288 if (!StatusBarRootModernization.isEnabled()) { 289 hide(mClockView, View.INVISIBLE); 290 } 291 mOperatorNameViewOptional.ifPresent(view -> hide(view, View.INVISIBLE)); 292 } else { 293 if (!StatusBarRootModernization.isEnabled()) { 294 show(mClockView); 295 } 296 mOperatorNameViewOptional.ifPresent(this::show); 297 hide(mView, View.GONE, () -> { 298 updateParentClipping(true /* shouldClip */); 299 }); 300 } 301 // Show the status bar icons when the view gets shown / hidden 302 if (mStatusBarStateController.getState() != StatusBarState.SHADE) { 303 mCommandQueue.recomputeDisableFlags( 304 mView.getContext().getDisplayId(), false); 305 } 306 } 307 } 308 updateParentClipping(boolean shouldClip)309 private void updateParentClipping(boolean shouldClip) { 310 StatusBarNoHunBehavior.assertInLegacyMode(); 311 ViewClippingUtil.setClippingDeactivated( 312 mView, !shouldClip, mParentClippingParams); 313 } 314 315 /** 316 * Hides the view and sets the state to endState when finished. 317 * 318 * @param view The view to hide. 319 * @param endState One of {@link View#INVISIBLE} or {@link View#GONE}. 320 * @see HeadsUpAppearanceController#hide(View, int, Runnable) 321 * @see View#setVisibility(int) 322 * 323 */ hide(View view, int endState)324 private void hide(View view, int endState) { 325 hide(view, endState, null); 326 } 327 328 /** 329 * Hides the view and sets the state to endState when finished. 330 * 331 * @param view The view to hide. 332 * @param endState One of {@link View#INVISIBLE} or {@link View#GONE}. 333 * @param callback Runnable to be executed after the view has been hidden. 334 * @see View#setVisibility(int) 335 * 336 */ hide(View view, int endState, Runnable callback)337 private void hide(View view, int endState, Runnable callback) { 338 StatusBarNoHunBehavior.assertInLegacyMode(); 339 340 if (mAnimationsEnabled) { 341 CrossFadeHelper.fadeOut(view, CONTENT_FADE_DURATION /* duration */, 342 0 /* delay */, () -> { 343 view.setVisibility(endState); 344 if (callback != null) { 345 callback.run(); 346 } 347 }); 348 } else { 349 view.setVisibility(endState); 350 if (callback != null) { 351 callback.run(); 352 } 353 } 354 } 355 show(View view)356 private void show(View view) { 357 StatusBarNoHunBehavior.assertInLegacyMode(); 358 359 if (mAnimationsEnabled) { 360 CrossFadeHelper.fadeIn(view, CONTENT_FADE_DURATION /* duration */, 361 CONTENT_FADE_DELAY /* delay */); 362 } else { 363 view.setVisibility(View.VISIBLE); 364 } 365 } 366 367 @VisibleForTesting setAnimationsEnabled(boolean enabled)368 void setAnimationsEnabled(boolean enabled) { 369 mAnimationsEnabled = enabled; 370 } 371 372 @VisibleForTesting getPinnedStatus()373 public PinnedStatus getPinnedStatus() { 374 if (StatusBarNoHunBehavior.isEnabled()) { 375 return PinnedStatus.NotPinned; 376 } 377 return mPinnedStatus; 378 } 379 380 /** True if the device's current state allows us to show HUNs and false otherwise. */ canShowHeadsUp()381 private boolean canShowHeadsUp() { 382 boolean notificationsShown = !mWakeUpCoordinator.getNotificationsFullyHidden(); 383 if (mBypassController.getBypassEnabled() && 384 (mStatusBarStateController.getState() == StatusBarState.KEYGUARD 385 || mKeyguardStateController.isKeyguardGoingAway()) 386 && notificationsShown) { 387 return true; 388 } 389 return !isExpanded() && notificationsShown; 390 } 391 392 /** 393 * True if the headsup status bar view (which has just the HUN icon and app name) should be 394 * visible right now and false otherwise. 395 * 396 * @deprecated use {@link com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor#getStatusBarHeadsUpState()} 397 * instead. 398 */ 399 @Deprecated shouldHeadsUpStatusBarBeVisible()400 public boolean shouldHeadsUpStatusBarBeVisible() { 401 if (StatusBarNoHunBehavior.isEnabled()) { 402 return false; 403 } 404 405 if (StatusBarNotifChips.isEnabled()) { 406 return canShowHeadsUp() 407 && mHeadsUpManager.pinnedHeadsUpStatus() == PinnedStatus.PinnedBySystem; 408 // Note: This means that if mHeadsUpManager.pinnedHeadsUpStatus() == PinnedByUser, 409 // #updateTopEntry won't do anything, so mPinnedStatus will remain as NotPinned and will 410 // *not* update to PinnedByUser. 411 } else { 412 return canShowHeadsUp() && mHeadsUpManager.hasPinnedHeadsUp(); 413 } 414 } 415 416 @Override onHeadsUpUnPinned(NotificationEntry entry)417 public void onHeadsUpUnPinned(NotificationEntry entry) { 418 updatePinnedStatus(); 419 updateHeader(entry.getRow()); 420 updateHeadsUpAndPulsingRoundness(entry.getRow()); 421 } 422 setAppearFraction(float expandedHeight, float appearFraction)423 public void setAppearFraction(float expandedHeight, float appearFraction) { 424 boolean changed = expandedHeight != mExpandedHeight; 425 boolean oldIsExpanded = isExpanded(); 426 427 mExpandedHeight = expandedHeight; 428 mAppearFraction = appearFraction; 429 // We only notify if the expandedHeight changed and not on the appearFraction, since 430 // otherwise we may run into an infinite loop where the panel and this are constantly 431 // updating themselves over just a small fraction 432 if (changed) { 433 updateHeadsUpHeaders(); 434 } 435 if (isExpanded() != oldIsExpanded) { 436 updatePinnedStatus(); 437 } 438 } 439 440 /** 441 * Set a headsUp to be tracked, meaning that it is currently being pulled down after being 442 * in a pinned state on the top. The expand animation is different in that case and we need 443 * to update the header constantly afterwards. 444 * 445 * @param trackedChild the tracked headsUp or null if it's not tracking anymore. 446 */ setTrackingHeadsUp(ExpandableNotificationRow trackedChild)447 public void setTrackingHeadsUp(ExpandableNotificationRow trackedChild) { 448 ExpandableNotificationRow previousTracked = mTrackedChild; 449 mTrackedChild = trackedChild; 450 if (previousTracked != null) { 451 updateHeader(previousTracked); 452 updateHeadsUpAndPulsingRoundness(previousTracked); 453 } 454 } 455 isExpanded()456 private boolean isExpanded() { 457 return mExpandedHeight > 0; 458 } 459 updateHeadsUpHeaders()460 private void updateHeadsUpHeaders() { 461 mHeadsUpManager.getAllEntries().forEach(entry -> { 462 updateHeader(entry.getRow()); 463 updateHeadsUpAndPulsingRoundness(entry.getRow()); 464 }); 465 } 466 updateHeader(ExpandableNotificationRow row)467 public void updateHeader(ExpandableNotificationRow row) { 468 float headerVisibleAmount = 1.0f; 469 // To fix the invisible HUN group header issue 470 if (!AsyncGroupHeaderViewInflation.isEnabled()) { 471 if (row.isPinned() || row.isHeadsUpAnimatingAway() || row == mTrackedChild 472 || row.showingPulsing()) { 473 headerVisibleAmount = mAppearFraction; 474 } 475 } 476 row.setHeaderVisibleAmount(headerVisibleAmount); 477 } 478 479 /** 480 * Update the HeadsUp and the Pulsing roundness based on current state 481 * @param row target notification row 482 */ updateHeadsUpAndPulsingRoundness(ExpandableNotificationRow row)483 public void updateHeadsUpAndPulsingRoundness(ExpandableNotificationRow row) { 484 boolean isTrackedChild = row == mTrackedChild; 485 if (row.isPinned() || row.isHeadsUpAnimatingAway() || isTrackedChild) { 486 float roundness = MathUtils.saturate(1f - mAppearFraction); 487 row.requestRoundness(roundness, roundness, HEADS_UP); 488 } else { 489 row.requestRoundnessReset(HEADS_UP); 490 } 491 if (mNotificationRoundnessManager.shouldRoundNotificationPulsing()) { 492 if (row.showingPulsing()) { 493 row.requestRoundness(/* top = */ 1f, /* bottom = */ 1f, PULSING); 494 } else { 495 row.requestRoundnessReset(PULSING); 496 } 497 } 498 } 499 500 501 @Override onDarkChanged(ArrayList<Rect> areas, float darkIntensity, int tint)502 public void onDarkChanged(ArrayList<Rect> areas, float darkIntensity, int tint) { 503 StatusBarNoHunBehavior.assertInLegacyMode(); 504 mView.onDarkChanged(areas, darkIntensity, tint); 505 } 506 onStateChanged()507 public void onStateChanged() { 508 StatusBarNoHunBehavior.assertInLegacyMode(); 509 updatePinnedStatus(); 510 } 511 512 @Override onFullyHiddenChanged(boolean isFullyHidden)513 public void onFullyHiddenChanged(boolean isFullyHidden) { 514 StatusBarNoHunBehavior.assertInLegacyMode(); 515 updatePinnedStatus(); 516 } 517 } 518