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