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