1 /* <lambda>null2 * Copyright (C) 2020 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 18 19 import android.animation.Animator 20 import android.animation.AnimatorListenerAdapter 21 import android.animation.ValueAnimator 22 import android.content.Context 23 import android.content.res.Configuration 24 import android.os.SystemClock 25 import android.os.Trace 26 import android.util.IndentingPrintWriter 27 import android.util.Log 28 import android.util.MathUtils 29 import android.view.Choreographer 30 import android.view.View 31 import androidx.annotation.VisibleForTesting 32 import androidx.dynamicanimation.animation.FloatPropertyCompat 33 import androidx.dynamicanimation.animation.SpringAnimation 34 import androidx.dynamicanimation.animation.SpringForce 35 import com.android.systemui.Dumpable 36 import com.android.app.animation.Interpolators 37 import com.android.systemui.animation.ShadeInterpolation 38 import com.android.systemui.dagger.SysUISingleton 39 import com.android.systemui.dump.DumpManager 40 import com.android.systemui.plugins.statusbar.StatusBarStateController 41 import com.android.systemui.shade.ShadeExpansionChangeEvent 42 import com.android.systemui.shade.ShadeExpansionListener 43 import com.android.systemui.statusbar.phone.BiometricUnlockController 44 import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK 45 import com.android.systemui.statusbar.phone.DozeParameters 46 import com.android.systemui.statusbar.phone.ScrimController 47 import com.android.systemui.statusbar.policy.ConfigurationController 48 import com.android.systemui.statusbar.policy.KeyguardStateController 49 import com.android.systemui.statusbar.policy.SplitShadeStateController 50 import com.android.systemui.util.WallpaperController 51 import java.io.PrintWriter 52 import javax.inject.Inject 53 import kotlin.math.max 54 import kotlin.math.sign 55 56 /** 57 * Controller responsible for statusbar window blur. 58 */ 59 @SysUISingleton 60 class NotificationShadeDepthController @Inject constructor( 61 private val statusBarStateController: StatusBarStateController, 62 private val blurUtils: BlurUtils, 63 private val biometricUnlockController: BiometricUnlockController, 64 private val keyguardStateController: KeyguardStateController, 65 private val choreographer: Choreographer, 66 private val wallpaperController: WallpaperController, 67 private val notificationShadeWindowController: NotificationShadeWindowController, 68 private val dozeParameters: DozeParameters, 69 private val context: Context, 70 private val splitShadeStateController: SplitShadeStateController, 71 dumpManager: DumpManager, 72 configurationController: ConfigurationController 73 ) : ShadeExpansionListener, Dumpable { 74 companion object { 75 private const val WAKE_UP_ANIMATION_ENABLED = true 76 private const val VELOCITY_SCALE = 100f 77 private const val MAX_VELOCITY = 3000f 78 private const val MIN_VELOCITY = -MAX_VELOCITY 79 private const val INTERACTION_BLUR_FRACTION = 0.8f 80 private const val ANIMATION_BLUR_FRACTION = 1f - INTERACTION_BLUR_FRACTION 81 private const val TAG = "DepthController" 82 } 83 84 lateinit var root: View 85 private var keyguardAnimator: Animator? = null 86 private var notificationAnimator: Animator? = null 87 private var updateScheduled: Boolean = false 88 @VisibleForTesting 89 var shadeExpansion = 0f 90 private var isClosed: Boolean = true 91 private var isOpen: Boolean = false 92 private var isBlurred: Boolean = false 93 private var listeners = mutableListOf<DepthListener>() 94 private var inSplitShade: Boolean = false 95 96 private var prevTracking: Boolean = false 97 private var prevTimestamp: Long = -1 98 private var prevShadeDirection = 0 99 private var prevShadeVelocity = 0f 100 101 // Only for dumpsys 102 private var lastAppliedBlur = 0 103 104 // Shade expansion offset that happens when pulling down on a HUN. 105 var panelPullDownMinFraction = 0f 106 107 var shadeAnimation = DepthAnimation() 108 109 @VisibleForTesting 110 var brightnessMirrorSpring = DepthAnimation() 111 var brightnessMirrorVisible: Boolean = false 112 set(value) { 113 field = value 114 brightnessMirrorSpring.animateTo(if (value) blurUtils.blurRadiusOfRatio(1f).toInt() 115 else 0) 116 } 117 118 var qsPanelExpansion = 0f 119 set(value) { 120 if (value.isNaN()) { 121 Log.w(TAG, "Invalid qs expansion") 122 return 123 } 124 if (field == value) return 125 field = value 126 scheduleUpdate() 127 } 128 129 /** 130 * How much we're transitioning to the full shade 131 */ 132 var transitionToFullShadeProgress = 0f 133 set(value) { 134 if (field == value) return 135 field = value 136 scheduleUpdate() 137 } 138 139 /** 140 * When launching an app from the shade, the animations progress should affect how blurry the 141 * shade is, overriding the expansion amount. 142 */ 143 var blursDisabledForAppLaunch: Boolean = false 144 set(value) { 145 if (field == value) { 146 return 147 } 148 field = value 149 scheduleUpdate() 150 151 if (shadeExpansion == 0f && shadeAnimation.radius == 0f) { 152 return 153 } 154 // Do not remove blurs when we're re-enabling them 155 if (!value) { 156 return 157 } 158 159 shadeAnimation.animateTo(0) 160 shadeAnimation.finishIfRunning() 161 } 162 163 /** 164 * We're unlocking, and should not blur as the panel expansion changes. 165 */ 166 var blursDisabledForUnlock: Boolean = false 167 set(value) { 168 if (field == value) return 169 field = value 170 scheduleUpdate() 171 } 172 173 /** 174 * Force stop blur effect when necessary. 175 */ 176 private var scrimsVisible: Boolean = false 177 set(value) { 178 if (field == value) return 179 field = value 180 scheduleUpdate() 181 } 182 183 /** 184 * Blur radius of the wake-up animation on this frame. 185 */ 186 private var wakeAndUnlockBlurRadius = 0f 187 set(value) { 188 if (field == value) return 189 field = value 190 scheduleUpdate() 191 } 192 193 private fun computeBlurAndZoomOut(): Pair<Int, Float> { 194 val animationRadius = MathUtils.constrain(shadeAnimation.radius, 195 blurUtils.minBlurRadius.toFloat(), blurUtils.maxBlurRadius.toFloat()) 196 val expansionRadius = blurUtils.blurRadiusOfRatio( 197 ShadeInterpolation.getNotificationScrimAlpha( 198 if (shouldApplyShadeBlur()) shadeExpansion else 0f)) 199 var combinedBlur = (expansionRadius * INTERACTION_BLUR_FRACTION + 200 animationRadius * ANIMATION_BLUR_FRACTION) 201 val qsExpandedRatio = ShadeInterpolation.getNotificationScrimAlpha(qsPanelExpansion) * 202 shadeExpansion 203 combinedBlur = max(combinedBlur, blurUtils.blurRadiusOfRatio(qsExpandedRatio)) 204 combinedBlur = max(combinedBlur, blurUtils.blurRadiusOfRatio(transitionToFullShadeProgress)) 205 var shadeRadius = max(combinedBlur, wakeAndUnlockBlurRadius) 206 207 if (blursDisabledForAppLaunch || blursDisabledForUnlock) { 208 shadeRadius = 0f 209 } 210 211 var zoomOut = MathUtils.saturate(blurUtils.ratioOfBlurRadius(shadeRadius)) 212 var blur = shadeRadius.toInt() 213 214 if (inSplitShade) { 215 zoomOut = 0f 216 } 217 218 // Make blur be 0 if it is necessary to stop blur effect. 219 if (scrimsVisible) { 220 blur = 0 221 zoomOut = 0f 222 } 223 224 if (!blurUtils.supportsBlursOnWindows()) { 225 blur = 0 226 } 227 228 // Brightness slider removes blur, but doesn't affect zooms 229 blur = (blur * (1f - brightnessMirrorSpring.ratio)).toInt() 230 231 return Pair(blur, zoomOut) 232 } 233 234 /** 235 * Callback that updates the window blur value and is called only once per frame. 236 */ 237 @VisibleForTesting 238 val updateBlurCallback = Choreographer.FrameCallback { 239 updateScheduled = false 240 val (blur, zoomOut) = computeBlurAndZoomOut() 241 val opaque = scrimsVisible && !blursDisabledForAppLaunch 242 Trace.traceCounter(Trace.TRACE_TAG_APP, "shade_blur_radius", blur) 243 blurUtils.applyBlur(root.viewRootImpl, blur, opaque) 244 lastAppliedBlur = blur 245 wallpaperController.setNotificationShadeZoom(zoomOut) 246 listeners.forEach { 247 it.onWallpaperZoomOutChanged(zoomOut) 248 it.onBlurRadiusChanged(blur) 249 } 250 notificationShadeWindowController.setBackgroundBlurRadius(blur) 251 } 252 253 /** 254 * Animate blurs when unlocking. 255 */ 256 private val keyguardStateCallback = object : KeyguardStateController.Callback { 257 override fun onKeyguardFadingAwayChanged() { 258 if (!keyguardStateController.isKeyguardFadingAway || 259 biometricUnlockController.mode != MODE_WAKE_AND_UNLOCK) { 260 return 261 } 262 263 keyguardAnimator?.cancel() 264 keyguardAnimator = ValueAnimator.ofFloat(1f, 0f).apply { 265 // keyguardStateController.keyguardFadingAwayDuration might be zero when unlock by 266 // fingerprint due to there is no window container, see AppTransition#goodToGo. 267 // We use DozeParameters.wallpaperFadeOutDuration as an alternative. 268 duration = dozeParameters.wallpaperFadeOutDuration 269 startDelay = keyguardStateController.keyguardFadingAwayDelay 270 interpolator = Interpolators.FAST_OUT_SLOW_IN 271 addUpdateListener { animation: ValueAnimator -> 272 wakeAndUnlockBlurRadius = 273 blurUtils.blurRadiusOfRatio(animation.animatedValue as Float) 274 } 275 addListener(object : AnimatorListenerAdapter() { 276 override fun onAnimationEnd(animation: Animator) { 277 keyguardAnimator = null 278 wakeAndUnlockBlurRadius = 0f 279 } 280 }) 281 start() 282 } 283 } 284 285 override fun onKeyguardShowingChanged() { 286 if (keyguardStateController.isShowing) { 287 keyguardAnimator?.cancel() 288 notificationAnimator?.cancel() 289 } 290 } 291 } 292 293 private val statusBarStateCallback = object : StatusBarStateController.StateListener { 294 override fun onStateChanged(newState: Int) { 295 updateShadeAnimationBlur( 296 shadeExpansion, prevTracking, prevShadeVelocity, prevShadeDirection) 297 scheduleUpdate() 298 } 299 300 override fun onDozingChanged(isDozing: Boolean) { 301 if (isDozing) { 302 shadeAnimation.finishIfRunning() 303 brightnessMirrorSpring.finishIfRunning() 304 } 305 } 306 307 override fun onDozeAmountChanged(linear: Float, eased: Float) { 308 wakeAndUnlockBlurRadius = blurUtils.blurRadiusOfRatio(eased) 309 } 310 } 311 312 init { 313 dumpManager.registerCriticalDumpable(javaClass.name, this) 314 if (WAKE_UP_ANIMATION_ENABLED) { 315 keyguardStateController.addCallback(keyguardStateCallback) 316 } 317 statusBarStateController.addCallback(statusBarStateCallback) 318 notificationShadeWindowController.setScrimsVisibilityListener { 319 // Stop blur effect when scrims is opaque to avoid unnecessary GPU composition. 320 visibility -> scrimsVisible = visibility == ScrimController.OPAQUE 321 } 322 shadeAnimation.setStiffness(SpringForce.STIFFNESS_LOW) 323 shadeAnimation.setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY) 324 updateResources() 325 configurationController.addCallback(object : ConfigurationController.ConfigurationListener { 326 override fun onConfigChanged(newConfig: Configuration?) { 327 updateResources() 328 } 329 }) 330 } 331 332 private fun updateResources() { 333 inSplitShade = splitShadeStateController.shouldUseSplitNotificationShade(context.resources) 334 } 335 336 fun addListener(listener: DepthListener) { 337 listeners.add(listener) 338 } 339 340 fun removeListener(listener: DepthListener) { 341 listeners.remove(listener) 342 } 343 344 /** 345 * Update blurs when pulling down the shade 346 */ 347 override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) { 348 val rawFraction = event.fraction 349 val tracking = event.tracking 350 val timestamp = SystemClock.elapsedRealtimeNanos() 351 val expansion = MathUtils.saturate( 352 (rawFraction - panelPullDownMinFraction) / (1f - panelPullDownMinFraction)) 353 354 if (shadeExpansion == expansion && prevTracking == tracking) { 355 prevTimestamp = timestamp 356 return 357 } 358 359 var deltaTime = 1f 360 if (prevTimestamp < 0) { 361 prevTimestamp = timestamp 362 } else { 363 deltaTime = MathUtils.constrain( 364 ((timestamp - prevTimestamp) / 1E9).toFloat(), 0.00001f, 1f) 365 } 366 367 val diff = expansion - shadeExpansion 368 val shadeDirection = sign(diff).toInt() 369 val shadeVelocity = MathUtils.constrain( 370 VELOCITY_SCALE * diff / deltaTime, MIN_VELOCITY, MAX_VELOCITY) 371 updateShadeAnimationBlur(expansion, tracking, shadeVelocity, shadeDirection) 372 373 prevShadeDirection = shadeDirection 374 prevShadeVelocity = shadeVelocity 375 shadeExpansion = expansion 376 prevTracking = tracking 377 prevTimestamp = timestamp 378 379 scheduleUpdate() 380 } 381 382 private fun updateShadeAnimationBlur( 383 expansion: Float, 384 tracking: Boolean, 385 velocity: Float, 386 direction: Int 387 ) { 388 if (shouldApplyShadeBlur()) { 389 if (expansion > 0f) { 390 // Blur view if user starts animating in the shade. 391 if (isClosed) { 392 animateBlur(true, velocity) 393 isClosed = false 394 } 395 396 // If we were blurring out and the user stopped the animation, blur view. 397 if (tracking && !isBlurred) { 398 animateBlur(true, 0f) 399 } 400 401 // If shade is being closed and the user isn't interacting with it, un-blur. 402 if (!tracking && direction < 0 && isBlurred) { 403 animateBlur(false, velocity) 404 } 405 406 if (expansion == 1f) { 407 if (!isOpen) { 408 isOpen = true 409 // If shade is open and view is not blurred, blur. 410 if (!isBlurred) { 411 animateBlur(true, velocity) 412 } 413 } 414 } else { 415 isOpen = false 416 } 417 // Automatic animation when the user closes the shade. 418 } else if (!isClosed) { 419 isClosed = true 420 // If shade is closed and view is not blurred, blur. 421 if (isBlurred) { 422 animateBlur(false, velocity) 423 } 424 } 425 } else { 426 animateBlur(false, 0f) 427 isClosed = true 428 isOpen = false 429 } 430 } 431 432 private fun animateBlur(blur: Boolean, velocity: Float) { 433 isBlurred = blur 434 435 val targetBlurNormalized = if (blur && shouldApplyShadeBlur()) { 436 1f 437 } else { 438 0f 439 } 440 441 shadeAnimation.setStartVelocity(velocity) 442 shadeAnimation.animateTo(blurUtils.blurRadiusOfRatio(targetBlurNormalized).toInt()) 443 } 444 445 private fun scheduleUpdate() { 446 if (updateScheduled) { 447 return 448 } 449 updateScheduled = true 450 val (blur, _) = computeBlurAndZoomOut() 451 blurUtils.prepareBlur(root.viewRootImpl, blur) 452 choreographer.postFrameCallback(updateBlurCallback) 453 } 454 455 /** 456 * Should blur be applied to the shade currently. This is mainly used to make sure that 457 * on the lockscreen, the wallpaper isn't blurred. 458 */ 459 private fun shouldApplyShadeBlur(): Boolean { 460 val state = statusBarStateController.state 461 return (state == StatusBarState.SHADE || state == StatusBarState.SHADE_LOCKED) && 462 !keyguardStateController.isKeyguardFadingAway 463 } 464 465 override fun dump(pw: PrintWriter, args: Array<out String>) { 466 IndentingPrintWriter(pw, " ").let { 467 it.println("StatusBarWindowBlurController:") 468 it.increaseIndent() 469 it.println("shadeExpansion: $shadeExpansion") 470 it.println("shouldApplyShadeBlur: ${shouldApplyShadeBlur()}") 471 it.println("shadeAnimation: ${shadeAnimation.radius}") 472 it.println("brightnessMirrorRadius: ${brightnessMirrorSpring.radius}") 473 it.println("wakeAndUnlockBlur: $wakeAndUnlockBlurRadius") 474 it.println("blursDisabledForAppLaunch: $blursDisabledForAppLaunch") 475 it.println("qsPanelExpansion: $qsPanelExpansion") 476 it.println("transitionToFullShadeProgress: $transitionToFullShadeProgress") 477 it.println("lastAppliedBlur: $lastAppliedBlur") 478 } 479 } 480 481 /** 482 * Animation helper that smoothly animates the depth using a spring and deals with frame 483 * invalidation. 484 */ 485 inner class DepthAnimation() { 486 /** 487 * Blur radius visible on the UI, in pixels. 488 */ 489 var radius = 0f 490 491 /** 492 * Depth ratio of the current blur radius. 493 */ 494 val ratio 495 get() = blurUtils.ratioOfBlurRadius(radius) 496 497 /** 498 * Radius that we're animating to. 499 */ 500 private var pendingRadius = -1 501 502 private var springAnimation = SpringAnimation(this, object : 503 FloatPropertyCompat<DepthAnimation>("blurRadius") { 504 override fun setValue(rect: DepthAnimation?, value: Float) { 505 radius = value 506 scheduleUpdate() 507 } 508 509 override fun getValue(rect: DepthAnimation?): Float { 510 return radius 511 } 512 }) 513 514 init { 515 springAnimation.spring = SpringForce(0.0f) 516 springAnimation.spring.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY 517 springAnimation.spring.stiffness = SpringForce.STIFFNESS_HIGH 518 springAnimation.addEndListener { _, _, _, _ -> pendingRadius = -1 } 519 } 520 521 fun animateTo(newRadius: Int) { 522 if (pendingRadius == newRadius) { 523 return 524 } 525 pendingRadius = newRadius 526 springAnimation.animateToFinalPosition(newRadius.toFloat()) 527 } 528 529 fun finishIfRunning() { 530 if (springAnimation.isRunning) { 531 springAnimation.skipToEnd() 532 } 533 } 534 535 fun setStiffness(stiffness: Float) { 536 springAnimation.spring.stiffness = stiffness 537 } 538 539 fun setDampingRatio(dampingRation: Float) { 540 springAnimation.spring.dampingRatio = dampingRation 541 } 542 543 fun setStartVelocity(velocity: Float) { 544 springAnimation.setStartVelocity(velocity) 545 } 546 } 547 548 /** 549 * Invoked when changes are needed in z-space 550 */ 551 interface DepthListener { 552 /** 553 * Current wallpaper zoom out, where 0 is the closest, and 1 the farthest 554 */ 555 fun onWallpaperZoomOutChanged(zoomOut: Float) 556 557 fun onBlurRadiusChanged(blurRadius: Int) {} 558 } 559 } 560