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