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