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