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