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