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