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