• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.os.SystemClock
23 import android.util.IndentingPrintWriter
24 import android.util.Log
25 import android.util.MathUtils
26 import android.view.Choreographer
27 import android.view.Display
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.app.animation.Interpolators
34 import com.android.app.tracing.coroutines.TrackTracer
35 import com.android.systemui.Dumpable
36 import com.android.systemui.Flags
37 import com.android.systemui.Flags.spatialModelAppPushback
38 import com.android.systemui.animation.ShadeInterpolation
39 import com.android.systemui.dagger.SysUISingleton
40 import com.android.systemui.dagger.qualifiers.Application
41 import com.android.systemui.dump.DumpManager
42 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
43 import com.android.systemui.plugins.statusbar.StatusBarStateController
44 import com.android.systemui.shade.ShadeExpansionChangeEvent
45 import com.android.systemui.shade.ShadeExpansionListener
46 import com.android.systemui.shade.data.repository.ShadeDisplaysRepository
47 import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
48 import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
49 import com.android.systemui.statusbar.phone.BiometricUnlockController
50 import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK
51 import com.android.systemui.statusbar.phone.DozeParameters
52 import com.android.systemui.statusbar.phone.ScrimController
53 import com.android.systemui.statusbar.policy.KeyguardStateController
54 import com.android.systemui.util.WallpaperController
55 import com.android.systemui.wallpapers.domain.interactor.WallpaperInteractor
56 import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor
57 import com.android.wm.shell.appzoomout.AppZoomOut
58 import dagger.Lazy
59 import java.io.PrintWriter
60 import java.util.Optional
61 import javax.inject.Inject
62 import kotlin.math.max
63 import kotlin.math.sign
64 import kotlinx.coroutines.CoroutineScope
65 import kotlinx.coroutines.launch
66 
67 /**
68  * Responsible for blurring the notification shade window, and applying a zoom effect to the
69  * wallpaper.
70  */
71 @SysUISingleton
72 class NotificationShadeDepthController
73 @Inject
74 constructor(
75     private val statusBarStateController: StatusBarStateController,
76     private val blurUtils: BlurUtils,
77     private val biometricUnlockController: BiometricUnlockController,
78     private val keyguardStateController: KeyguardStateController,
79     private val keyguardInteractor: KeyguardInteractor,
80     private val choreographer: Choreographer,
81     private val wallpaperController: WallpaperController,
82     private val wallpaperInteractor: WallpaperInteractor,
83     private val notificationShadeWindowController: NotificationShadeWindowController,
84     private val dozeParameters: DozeParameters,
85     private val shadeModeInteractor: ShadeModeInteractor,
86     private val windowRootViewBlurInteractor: WindowRootViewBlurInteractor,
87     private val appZoomOutOptional: Optional<AppZoomOut>,
88     @Application private val applicationScope: CoroutineScope,
89     dumpManager: DumpManager,
90     private val shadeDisplaysRepository: Lazy<ShadeDisplaysRepository>,
91 ) : ShadeExpansionListener, Dumpable {
92     companion object {
93         private const val WAKE_UP_ANIMATION_ENABLED = true
94         private const val VELOCITY_SCALE = 100f
95         private const val MAX_VELOCITY = 3000f
96         private const val MIN_VELOCITY = -MAX_VELOCITY
97         private const val INTERACTION_BLUR_FRACTION = 0.8f
98         private const val ANIMATION_BLUR_FRACTION = 1f - INTERACTION_BLUR_FRACTION
99         private const val TRANSITION_THRESHOLD = 0.98f
100         private const val TAG = "DepthController"
101     }
102 
103     lateinit var root: View
104     private var keyguardAnimator: Animator? = null
105     private var notificationAnimator: Animator? = null
106     private var updateScheduled: Boolean = false
107     @VisibleForTesting var shadeExpansion = 0f
108     private var isClosed: Boolean = true
109     private var isOpen: Boolean = false
110     private var isBlurred: Boolean = false
111     private var listeners = mutableListOf<DepthListener>()
112 
113     private var prevTracking: Boolean = false
114     private var prevTimestamp: Long = -1
115     private var prevShadeDirection = 0
116     private var prevShadeVelocity = 0f
117     private var prevDozeAmount: Float = 0f
118     @VisibleForTesting var wallpaperSupportsAmbientMode: Boolean = false
119     // tracks whether app launch transition is in progress. This involves two independent factors
120     // that control blur, shade expansion and app launch animation from outside sysui.
121     // They can complete out of order, this flag will be reset by the animation that finishes later.
122     private var appLaunchTransitionIsInProgress = false
123 
124     // Only for dumpsys
125     private var lastAppliedBlur = 0
126 
127     val maxBlurRadiusPx = blurUtils.maxBlurRadius
128 
129     // Shade expansion offset that happens when pulling down on a HUN.
130     var panelPullDownMinFraction = 0f
131 
132     var shadeAnimation = DepthAnimation()
133 
134     @VisibleForTesting var brightnessMirrorSpring = DepthAnimation()
135     var brightnessMirrorVisible: Boolean = false
136         set(value) {
137             field = value
138             brightnessMirrorSpring.animateTo(
139                 if (value) blurUtils.blurRadiusOfRatio(1f).toInt() else 0
140             )
141         }
142 
143     var qsPanelExpansion = 0f
144         set(value) {
145             if (value.isNaN()) {
146                 Log.w(TAG, "Invalid qs expansion")
147                 return
148             }
149             if (field == value) return
150             field = value
151             scheduleUpdate()
152         }
153 
154     /** How much we're transitioning to the full shade */
155     var transitionToFullShadeProgress = 0f
156         set(value) {
157             if (field == value) return
158             field = value
159             scheduleUpdate()
160         }
161 
162     /**
163      * When launching an app from the shade, the animations progress should affect how blurry the
164      * shade is, overriding the expansion amount.
165      *
166      * TODO(b/399617511): remove this once [Flags.notificationShadeBlur] is launched and the Shade
167      *   closing is actually instantaneous.
168      */
169     var blursDisabledForAppLaunch: Boolean = false
170         set(value) {
171             if (field == value) {
172                 return
173             }
174             // Set this to true now, this will be reset when the next shade expansion finishes or
175             // when the app launch finishes, whichever happens later.
176             if (value) {
177                 appLaunchTransitionIsInProgress = true
178             } else {
179                 // App was launching and now it has finished launching
180                 if (shadeExpansion == 0.0f) {
181                     // this means shade expansion finished before app launch was done.
182                     // reset the flag here
183                     appLaunchTransitionIsInProgress = false
184                 }
185             }
186             field = value
187             scheduleUpdate()
188 
189             if (shadeExpansion == 0f && shadeAnimation.radius == 0f) {
190                 return
191             }
192             // Do not remove blurs when we're re-enabling them
193             if (!value) {
194                 return
195             }
196 
197             if (Flags.notificationShadeBlur()) {
198                 shadeAnimation.skipTo(0)
199             } else {
200                 shadeAnimation.animateTo(0)
201                 shadeAnimation.finishIfRunning()
202             }
203         }
204         @Deprecated(
205             message =
206                 "This might get reset to false before shade expansion is fully done, " +
207                     "consider using areBlursDisabledForAppLaunch"
208         )
209         get() = field
210 
211     private var zoomOutCalculatedFromShadeRadius: Float = 0.0f
212 
213     /** We're unlocking, and should not blur as the panel expansion changes. */
214     var blursDisabledForUnlock: Boolean = false
215         set(value) {
216             if (field == value) return
217             field = value
218             scheduleUpdate()
219         }
220 
221     private val areBlursDisabledForAppLaunch: Boolean
222         get() =
223             blursDisabledForAppLaunch ||
224                 (Flags.bouncerUiRevamp() && appLaunchTransitionIsInProgress)
225 
226     /** Force stop blur effect when necessary. */
227     private var scrimsVisible: Boolean = false
228         set(value) {
229             if (field == value) return
230             field = value
231             scheduleUpdate()
232         }
233 
234     private data class WakeAndUnlockBlurData(val radius: Float, val useZoom: Boolean = true)
235 
236     private val isShadeOnDefaultDisplay: Boolean
237         get() =
238             if (ShadeWindowGoesAround.isEnabled) {
239                 shadeDisplaysRepository.get().displayId.value == Display.DEFAULT_DISPLAY
240             } else {
241                 true
242             }
243 
244     /** Blur radius of the wake and unlock animation on this frame, and whether to zoom out. */
245     private var wakeAndUnlockBlurData = WakeAndUnlockBlurData(0f)
246         set(value) {
247             if (field == value) return
248             field = value
249             scheduleUpdate()
250         }
251 
252     private fun computeBlurAndZoomOut(): Pair<Int, Float> {
253         val animationRadius =
254             MathUtils.constrain(
255                 shadeAnimation.radius,
256                 blurUtils.minBlurRadius,
257                 blurUtils.maxBlurRadius,
258             )
259         val expansionRadius =
260             blurUtils.blurRadiusOfRatio(
261                 ShadeInterpolation.getNotificationScrimAlpha(
262                     if (shouldApplyShadeBlur()) shadeExpansion else 0f
263                 )
264             )
265         var combinedBlur =
266             (expansionRadius * INTERACTION_BLUR_FRACTION +
267                 animationRadius * ANIMATION_BLUR_FRACTION)
268         val qsExpandedRatio =
269             ShadeInterpolation.getNotificationScrimAlpha(qsPanelExpansion) * shadeExpansion
270         combinedBlur = max(combinedBlur, blurUtils.blurRadiusOfRatio(qsExpandedRatio))
271         combinedBlur = max(combinedBlur, blurUtils.blurRadiusOfRatio(transitionToFullShadeProgress))
272         var shadeRadius = max(combinedBlur, wakeAndUnlockBlurData.radius)
273 
274         if (areBlursDisabledForAppLaunch || blursDisabledForUnlock) {
275             shadeRadius = 0f
276         }
277 
278         var blur = shadeRadius.toInt()
279         // If the blur comes from waking up, we don't want to zoom out the background
280         val zoomOut =
281             when {
282                 // When the shade is in another display, we don't want to zoom out the background.
283                 // Only the default display is supported right now.
284                 !isShadeOnDefaultDisplay -> 0f
285                 shadeRadius != wakeAndUnlockBlurData.radius || wakeAndUnlockBlurData.useZoom ->
286                     blurRadiusToZoomOut(blurRadius = shadeRadius)
287                 else -> 0f
288             }
289         // Make blur be 0 if it is necessary to stop blur effect.
290         if (scrimsVisible) {
291             if (!Flags.notificationShadeBlur()) {
292                 blur = 0
293             }
294         }
295 
296         if (!blurUtils.supportsBlursOnWindows()) {
297             blur = 0
298         }
299 
300         // Brightness slider removes blur, but doesn't affect zooms
301         blur = (blur * (1f - brightnessMirrorSpring.ratio)).toInt()
302 
303         return Pair(blur, zoomOut)
304     }
305 
306     private fun blurRadiusToZoomOut(blurRadius: Float): Float {
307         var zoomOut = MathUtils.saturate(blurUtils.ratioOfBlurRadius(blurRadius))
308         if (shadeModeInteractor.isSplitShade) {
309             zoomOut = 0f
310         }
311 
312         if (scrimsVisible) {
313             zoomOut = 0f
314         }
315         return zoomOut
316     }
317 
318     private val shouldBlurBeOpaque: Boolean
319         get() =
320             if (Flags.notificationShadeBlur()) false
321             else scrimsVisible && !areBlursDisabledForAppLaunch
322 
323     /** Callback that updates the window blur value and is called only once per frame. */
324     @VisibleForTesting
325     val updateBlurCallback =
326         Choreographer.FrameCallback {
327             updateScheduled = false
328             val (blur, zoomOutFromShadeRadius) = computeBlurAndZoomOut()
329             val opaque = shouldBlurBeOpaque
330             TrackTracer.instantForGroup("shade", "shade_blur_radius", blur)
331             blurUtils.applyBlur(root.viewRootImpl, blur, opaque)
332             onBlurApplied(blur, zoomOutFromShadeRadius)
333         }
334 
335     private fun onBlurApplied(appliedBlurRadius: Int, zoomOutFromShadeRadius: Float) {
336         lastAppliedBlur = appliedBlurRadius
337         onZoomOutChanged(zoomOutFromShadeRadius)
338         listeners.forEach { it.onBlurRadiusChanged(appliedBlurRadius) }
339         notificationShadeWindowController.setBackgroundBlurRadius(appliedBlurRadius)
340     }
341 
342     private fun onZoomOutChanged(zoomOutFromShadeRadius: Float) {
343         TrackTracer.instantForGroup("shade", "zoom_out", zoomOutFromShadeRadius)
344         Log.v(TAG, "onZoomOutChanged $zoomOutFromShadeRadius")
345         wallpaperController.setNotificationShadeZoom(zoomOutFromShadeRadius)
346         if (spatialModelAppPushback()) {
347             appZoomOutOptional.ifPresent { appZoomOut ->
348                 appZoomOut.setProgress(zoomOutFromShadeRadius)
349             }
350             keyguardInteractor.setZoomOut(zoomOutFromShadeRadius)
351         }
352     }
353 
354     private val applyZoomOutForFrame =
355         Choreographer.FrameCallback {
356             updateScheduled = false
357             val (_, zoomOutFromShadeRadius) = computeBlurAndZoomOut()
358             onZoomOutChanged(zoomOutFromShadeRadius)
359         }
360 
361     /** Animate blurs when unlocking. */
362     private val keyguardStateCallback =
363         object : KeyguardStateController.Callback {
364             override fun onKeyguardFadingAwayChanged() {
365                 if (
366                     !keyguardStateController.isKeyguardFadingAway ||
367                         biometricUnlockController.mode != MODE_WAKE_AND_UNLOCK
368                 ) {
369                     return
370                 }
371 
372                 keyguardAnimator?.cancel()
373                 keyguardAnimator =
374                     ValueAnimator.ofFloat(1f, 0f).apply {
375                         // keyguardStateController.keyguardFadingAwayDuration might be zero when
376                         // unlock by fingerprint due to there is no window container, see
377                         // AppTransition#goodToGo. We use DozeParameters.wallpaperFadeOutDuration as
378                         // an alternative.
379                         duration = dozeParameters.wallpaperFadeOutDuration
380                         startDelay = keyguardStateController.keyguardFadingAwayDelay
381                         interpolator = Interpolators.FAST_OUT_SLOW_IN
382                         addUpdateListener { animation: ValueAnimator ->
383                             wakeAndUnlockBlurData =
384                                 WakeAndUnlockBlurData(
385                                     blurUtils.blurRadiusOfRatio(animation.animatedValue as Float)
386                                 )
387                         }
388                         addListener(
389                             object : AnimatorListenerAdapter() {
390                                 override fun onAnimationEnd(animation: Animator) {
391                                     keyguardAnimator = null
392                                     wakeAndUnlockBlurData = WakeAndUnlockBlurData(0f)
393                                 }
394                             }
395                         )
396                         start()
397                     }
398             }
399 
400             override fun onKeyguardShowingChanged() {
401                 if (keyguardStateController.isShowing) {
402                     keyguardAnimator?.cancel()
403                     notificationAnimator?.cancel()
404                 }
405             }
406         }
407 
408     private val statusBarStateCallback =
409         object : StatusBarStateController.StateListener {
410             override fun onStateChanged(newState: Int) {
411                 updateShadeAnimationBlur(
412                     shadeExpansion,
413                     prevTracking,
414                     prevShadeVelocity,
415                     prevShadeDirection,
416                 )
417                 scheduleUpdate()
418             }
419 
420             override fun onDozingChanged(isDozing: Boolean) {
421                 if (isDozing) {
422                     shadeAnimation.finishIfRunning()
423                     brightnessMirrorSpring.finishIfRunning()
424                 }
425             }
426 
427             override fun onDozeAmountChanged(linear: Float, eased: Float) {
428                 prevDozeAmount = eased
429                 updateWakeBlurRadius(prevDozeAmount)
430             }
431         }
432 
433     private fun updateWakeBlurRadius(ratio: Float) {
434         wakeAndUnlockBlurData = WakeAndUnlockBlurData(getNewWakeBlurRadius(ratio), false)
435     }
436 
437     private fun getNewWakeBlurRadius(ratio: Float): Float {
438         return if (!wallpaperSupportsAmbientMode) {
439             0f
440         } else {
441             blurUtils.blurRadiusOfRatio(ratio)
442         }
443     }
444 
445     init {
446         dumpManager.registerCriticalDumpable(javaClass.name, this)
447         if (WAKE_UP_ANIMATION_ENABLED) {
448             keyguardStateController.addCallback(keyguardStateCallback)
449         }
450         statusBarStateController.addCallback(statusBarStateCallback)
451         notificationShadeWindowController.setScrimsVisibilityListener {
452             // Stop blur effect when scrims is opaque to avoid unnecessary GPU composition.
453             visibility ->
454             scrimsVisible = visibility == ScrimController.OPAQUE
455         }
456         shadeAnimation.setStiffness(SpringForce.STIFFNESS_LOW)
457         shadeAnimation.setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
458         applicationScope.launch {
459             wallpaperInteractor.wallpaperSupportsAmbientMode.collect { supported ->
460                 wallpaperSupportsAmbientMode = supported
461                 if (
462                     getNewWakeBlurRadius(prevDozeAmount) == wakeAndUnlockBlurData.radius &&
463                         !wakeAndUnlockBlurData.useZoom
464                 ) {
465                     // Update wake and unlock radius only if the previous value comes from wake-up.
466                     updateWakeBlurRadius(prevDozeAmount)
467                 }
468             }
469         }
470         initBlurListeners()
471     }
472 
473     private fun initBlurListeners() {
474         if (!Flags.bouncerUiRevamp()) return
475 
476         windowRootViewBlurInteractor.registerBlurAppliedListener { appliedBlurRadius ->
477             if (updateScheduled) {
478                 // Process the blur applied event only if we scheduled the update
479                 TrackTracer.instantForGroup("shade", "shade_blur_radius", appliedBlurRadius)
480                 updateScheduled = false
481                 onBlurApplied(appliedBlurRadius, zoomOutCalculatedFromShadeRadius)
482             } else {
483                 // Try scheduling an update now, maybe our blur request will be scheduled now.
484                 scheduleUpdate()
485             }
486         }
487 
488         applicationScope.launch {
489             windowRootViewBlurInteractor.isBlurCurrentlySupported.collect { supported ->
490                 if (supported) {
491                     // when battery saver changes, try scheduling an update.
492                     scheduleUpdate()
493                 } else {
494                     // when blur becomes unsupported, no more updates will be scheduled,
495                     // reset updateScheduled state.
496                     updateScheduled = false
497                     // reset blur and internal state to 0
498                     onBlurApplied(0, 0.0f)
499                 }
500             }
501         }
502     }
503 
504     fun addListener(listener: DepthListener) {
505         listeners.add(listener)
506     }
507 
508     fun removeListener(listener: DepthListener) {
509         listeners.remove(listener)
510     }
511 
512     /** Update blurs when pulling down the shade */
513     override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) {
514         val rawFraction = event.fraction
515         val tracking = event.tracking
516         val timestamp = SystemClock.elapsedRealtimeNanos()
517         val expansion =
518             MathUtils.saturate(
519                 (rawFraction - panelPullDownMinFraction) / (1f - panelPullDownMinFraction)
520             )
521 
522         if (shadeExpansion == expansion && prevTracking == tracking) {
523             prevTimestamp = timestamp
524             return
525         }
526 
527         var deltaTime = 1f
528         if (prevTimestamp < 0) {
529             prevTimestamp = timestamp
530         } else {
531             deltaTime =
532                 MathUtils.constrain(((timestamp - prevTimestamp) / 1E9).toFloat(), 0.00001f, 1f)
533         }
534 
535         val diff = expansion - shadeExpansion
536         val shadeDirection = sign(diff).toInt()
537         val shadeVelocity =
538             MathUtils.constrain(VELOCITY_SCALE * diff / deltaTime, MIN_VELOCITY, MAX_VELOCITY)
539         if (expansion == 0.0f && appLaunchTransitionIsInProgress && !blursDisabledForAppLaunch) {
540             // Shade expansion finished but the app launch is already done, then this should mark
541             // the transition as done.
542             Log.d(TAG, "appLaunchTransitionIsInProgress is now false from shade expansion event")
543             appLaunchTransitionIsInProgress = false
544         }
545 
546         updateShadeAnimationBlur(expansion, tracking, shadeVelocity, shadeDirection)
547 
548         prevShadeDirection = shadeDirection
549         prevShadeVelocity = shadeVelocity
550         shadeExpansion = expansion
551         prevTracking = tracking
552         prevTimestamp = timestamp
553 
554         scheduleUpdate()
555     }
556 
557     fun onTransitionAnimationProgress(progress: Float) {
558         if (!Flags.notificationShadeBlur() || !Flags.moveTransitionAnimationLayer()) return
559         // Because the Shade takes a few frames to actually trigger the unblur after a transition
560         // has ended, we need to disable it manually, or the opening window itself will be blurred
561         // for a few frames due to relative ordering. We do this towards the end, so that the
562         // window is already covering the background and the unblur is not visible.
563         if (progress >= TRANSITION_THRESHOLD && shadeAnimation.radius > 0) {
564             blursDisabledForAppLaunch = true
565         }
566     }
567 
568     fun onTransitionAnimationEnd() {
569         if (!Flags.notificationShadeBlur() || !Flags.moveTransitionAnimationLayer()) return
570         blursDisabledForAppLaunch = false
571     }
572 
573     private fun updateShadeAnimationBlur(
574         expansion: Float,
575         tracking: Boolean,
576         velocity: Float,
577         direction: Int,
578     ) {
579         if (shouldApplyShadeBlur()) {
580             if (expansion > 0f) {
581                 // Blur view if user starts animating in the shade.
582                 if (isClosed) {
583                     animateBlur(true, velocity)
584                     isClosed = false
585                 }
586 
587                 // If we were blurring out and the user stopped the animation, blur view.
588                 if (tracking && !isBlurred) {
589                     animateBlur(true, 0f)
590                 }
591 
592                 // If shade is being closed and the user isn't interacting with it, un-blur.
593                 if (!tracking && direction < 0 && isBlurred) {
594                     animateBlur(false, velocity)
595                 }
596 
597                 if (expansion == 1f) {
598                     if (!isOpen) {
599                         isOpen = true
600                         // If shade is open and view is not blurred, blur.
601                         if (!isBlurred) {
602                             animateBlur(true, velocity)
603                         }
604                     }
605                 } else {
606                     isOpen = false
607                 }
608                 // Automatic animation when the user closes the shade.
609             } else if (!isClosed) {
610                 isClosed = true
611                 // If shade is closed and view is not blurred, blur.
612                 if (isBlurred) {
613                     animateBlur(false, velocity)
614                 }
615             }
616         } else {
617             animateBlur(false, 0f)
618             isClosed = true
619             isOpen = false
620         }
621     }
622 
623     private fun animateBlur(blur: Boolean, velocity: Float) {
624         isBlurred = blur
625 
626         val targetBlurNormalized =
627             if (blur && shouldApplyShadeBlur()) {
628                 1f
629             } else {
630                 0f
631             }
632 
633         shadeAnimation.setStartVelocity(velocity)
634         shadeAnimation.animateTo(blurUtils.blurRadiusOfRatio(targetBlurNormalized).toInt())
635     }
636 
637     private fun scheduleUpdate() {
638         val (blur, zoomOutFromShadeRadius) = computeBlurAndZoomOut()
639         zoomOutCalculatedFromShadeRadius = zoomOutFromShadeRadius
640         if (Flags.bouncerUiRevamp() || Flags.glanceableHubBlurredBackground()) {
641             if (windowRootViewBlurInteractor.isBlurCurrentlySupported.value) {
642                 updateScheduled =
643                     windowRootViewBlurInteractor.requestBlurForShade(blur, shouldBlurBeOpaque)
644                 return
645             }
646             // When blur is not supported, zoom out still needs to happen when scheduleUpdate
647             // is invoked and a separate frame callback has to be wired-up to support that.
648             if (!updateScheduled) {
649                 updateScheduled = true
650                 choreographer.postFrameCallback(applyZoomOutForFrame)
651             }
652             return
653         }
654         if (updateScheduled) {
655             return
656         }
657         updateScheduled = true
658         blurUtils.prepareBlur(root.viewRootImpl, blur)
659         choreographer.postFrameCallback(updateBlurCallback)
660     }
661 
662     /**
663      * Should blur be applied to the shade currently. This is mainly used to make sure that on the
664      * lockscreen, the wallpaper isn't blurred.
665      */
666     private fun shouldApplyShadeBlur(): Boolean {
667         val state = statusBarStateController.state
668         return (state == StatusBarState.SHADE || state == StatusBarState.SHADE_LOCKED) &&
669             !keyguardStateController.isKeyguardFadingAway
670     }
671 
672     override fun dump(pw: PrintWriter, args: Array<out String>) {
673         IndentingPrintWriter(pw, "  ").let {
674             it.println("StatusBarWindowBlurController:")
675             it.increaseIndent()
676             it.println("shadeExpansion: $shadeExpansion")
677             it.println("shouldApplyShadeBlur: ${shouldApplyShadeBlur()}")
678             it.println("shadeAnimation: ${shadeAnimation.radius}")
679             it.println("brightnessMirrorRadius: ${brightnessMirrorSpring.radius}")
680             it.println("wakeAndUnlockBlurRadius: ${wakeAndUnlockBlurData.radius}")
681             it.println("wakeAndUnlockBlurUsesZoom: ${wakeAndUnlockBlurData.useZoom}")
682             it.println("blursDisabledForAppLaunch: $blursDisabledForAppLaunch")
683             it.println("appLaunchTransitionIsInProgress: $appLaunchTransitionIsInProgress")
684             it.println("qsPanelExpansion: $qsPanelExpansion")
685             it.println("transitionToFullShadeProgress: $transitionToFullShadeProgress")
686             it.println("lastAppliedBlur: $lastAppliedBlur")
687         }
688     }
689 
690     /**
691      * Animation helper that smoothly animates the depth using a spring and deals with frame
692      * invalidation.
693      */
694     inner class DepthAnimation() {
695         /** Blur radius visible on the UI, in pixels. */
696         var radius = 0f
697 
698         /** Depth ratio of the current blur radius. */
699         val ratio
700             get() = blurUtils.ratioOfBlurRadius(radius)
701 
702         /** Radius that we're animating to. */
703         private var pendingRadius = -1
704 
705         private var springAnimation =
706             SpringAnimation(
707                 this,
708                 object : FloatPropertyCompat<DepthAnimation>("blurRadius") {
709                     override fun setValue(rect: DepthAnimation?, value: Float) {
710                         radius = value
711                         scheduleUpdate()
712                     }
713 
714                     override fun getValue(rect: DepthAnimation?): Float {
715                         return radius
716                     }
717                 },
718             )
719 
720         init {
721             springAnimation.spring = SpringForce(0.0f)
722             springAnimation.spring.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
723             springAnimation.spring.stiffness = SpringForce.STIFFNESS_HIGH
724             springAnimation.addEndListener { _, _, _, _ -> pendingRadius = -1 }
725         }
726 
727         /**
728          * Starts an animation to [newRadius], or updates the current one if already ongoing.
729          * IMPORTANT: do NOT use this method + [finishIfRunning] to instantaneously change the value
730          * of the animation. The change will NOT be instantaneous. Use [skipTo] instead.
731          *
732          * Explanation:
733          * 1. If idle, [SpringAnimation.animateToFinalPosition] requests a start to the animation.
734          * 2. On the first frame after an idle animation is requested to start, the animation simply
735          *    acquires the starting value and does nothing else.
736          * 3. [SpringAnimation.skipToEnd] requests a fast-forward to the end value, but this happens
737          *    during calculation of the next animation value. Because on the first frame no such
738          *    calculation happens (point #2), there is one lagging frame where we still see the old
739          *    value.
740          */
741         fun animateTo(newRadius: Int) {
742             if (pendingRadius == newRadius) {
743                 return
744             }
745             pendingRadius = newRadius
746             springAnimation.animateToFinalPosition(newRadius.toFloat())
747         }
748 
749         /**
750          * Instantaneously set a new blur radius to this animation. Always use this instead of
751          * [animateTo] and [finishIfRunning] to make sure that the change takes effect in the next
752          * frame. See the doc for [animateTo] for an explanation.
753          */
754         fun skipTo(newRadius: Int) {
755             if (pendingRadius == newRadius) return
756             pendingRadius = newRadius
757             springAnimation.cancel()
758             springAnimation.setStartValue(newRadius.toFloat())
759             springAnimation.animateToFinalPosition(newRadius.toFloat())
760         }
761 
762         fun finishIfRunning() {
763             if (springAnimation.isRunning) {
764                 springAnimation.skipToEnd()
765             }
766         }
767 
768         fun setStiffness(stiffness: Float) {
769             springAnimation.spring.stiffness = stiffness
770         }
771 
772         fun setDampingRatio(dampingRation: Float) {
773             springAnimation.spring.dampingRatio = dampingRation
774         }
775 
776         fun setStartVelocity(velocity: Float) {
777             springAnimation.setStartVelocity(velocity)
778         }
779     }
780 
781     /** Invoked when changes are needed in z-space */
782     interface DepthListener {
783         fun onBlurRadiusChanged(blurRadius: Int) {}
784     }
785 }
786