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