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