• 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.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.systemui.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     /**
193      * Callback that updates the window blur value and is called only once per frame.
194      */
195     @VisibleForTesting
196     val updateBlurCallback = Choreographer.FrameCallback {
197         updateScheduled = false
198         val animationRadius = MathUtils.constrain(shadeAnimation.radius,
199                 blurUtils.minBlurRadius.toFloat(), blurUtils.maxBlurRadius.toFloat())
200         val expansionRadius = blurUtils.blurRadiusOfRatio(
201                 ShadeInterpolation.getNotificationScrimAlpha(
202                         if (shouldApplyShadeBlur()) shadeExpansion else 0f))
203         var combinedBlur = (expansionRadius * INTERACTION_BLUR_FRACTION +
204                 animationRadius * ANIMATION_BLUR_FRACTION)
205         val qsExpandedRatio = ShadeInterpolation.getNotificationScrimAlpha(qsPanelExpansion) *
206                 shadeExpansion
207         combinedBlur = max(combinedBlur, blurUtils.blurRadiusOfRatio(qsExpandedRatio))
208         combinedBlur = max(combinedBlur, blurUtils.blurRadiusOfRatio(transitionToFullShadeProgress))
209         var shadeRadius = max(combinedBlur, wakeAndUnlockBlurRadius)
210 
211         if (blursDisabledForAppLaunch || blursDisabledForUnlock) {
212             shadeRadius = 0f
213         }
214 
215         var zoomOut = MathUtils.saturate(blurUtils.ratioOfBlurRadius(shadeRadius))
216         var blur = shadeRadius.toInt()
217 
218         if (inSplitShade) {
219             zoomOut = 0f
220         }
221 
222         // Make blur be 0 if it is necessary to stop blur effect.
223         if (scrimsVisible) {
224             blur = 0
225             zoomOut = 0f
226         }
227 
228         if (!blurUtils.supportsBlursOnWindows()) {
229             blur = 0
230         }
231 
232         // Brightness slider removes blur, but doesn't affect zooms
233         blur = (blur * (1f - brightnessMirrorSpring.ratio)).toInt()
234 
235         val opaque = scrimsVisible && !blursDisabledForAppLaunch
236         Trace.traceCounter(Trace.TRACE_TAG_APP, "shade_blur_radius", blur)
237         blurUtils.applyBlur(root.viewRootImpl, blur, opaque)
238         lastAppliedBlur = blur
239         wallpaperController.setNotificationShadeZoom(zoomOut)
240         listeners.forEach {
241             it.onWallpaperZoomOutChanged(zoomOut)
242             it.onBlurRadiusChanged(blur)
243         }
244         notificationShadeWindowController.setBackgroundBlurRadius(blur)
245     }
246 
247     /**
248      * Animate blurs when unlocking.
249      */
250     private val keyguardStateCallback = object : KeyguardStateController.Callback {
251         override fun onKeyguardFadingAwayChanged() {
252             if (!keyguardStateController.isKeyguardFadingAway ||
253                     biometricUnlockController.mode != MODE_WAKE_AND_UNLOCK) {
254                 return
255             }
256 
257             keyguardAnimator?.cancel()
258             keyguardAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
259                 // keyguardStateController.keyguardFadingAwayDuration might be zero when unlock by
260                 // fingerprint due to there is no window container, see AppTransition#goodToGo.
261                 // We use DozeParameters.wallpaperFadeOutDuration as an alternative.
262                 duration = dozeParameters.wallpaperFadeOutDuration
263                 startDelay = keyguardStateController.keyguardFadingAwayDelay
264                 interpolator = Interpolators.FAST_OUT_SLOW_IN
265                 addUpdateListener { animation: ValueAnimator ->
266                     wakeAndUnlockBlurRadius =
267                             blurUtils.blurRadiusOfRatio(animation.animatedValue as Float)
268                 }
269                 addListener(object : AnimatorListenerAdapter() {
270                     override fun onAnimationEnd(animation: Animator?) {
271                         keyguardAnimator = null
272                         wakeAndUnlockBlurRadius = 0f
273                     }
274                 })
275                 start()
276             }
277         }
278 
279         override fun onKeyguardShowingChanged() {
280             if (keyguardStateController.isShowing) {
281                 keyguardAnimator?.cancel()
282                 notificationAnimator?.cancel()
283             }
284         }
285     }
286 
287     private val statusBarStateCallback = object : StatusBarStateController.StateListener {
288         override fun onStateChanged(newState: Int) {
289             updateShadeAnimationBlur(
290                     shadeExpansion, prevTracking, prevShadeVelocity, prevShadeDirection)
291             scheduleUpdate()
292         }
293 
294         override fun onDozingChanged(isDozing: Boolean) {
295             if (isDozing) {
296                 shadeAnimation.finishIfRunning()
297                 brightnessMirrorSpring.finishIfRunning()
298             }
299         }
300 
301         override fun onDozeAmountChanged(linear: Float, eased: Float) {
302             wakeAndUnlockBlurRadius = blurUtils.blurRadiusOfRatio(eased)
303         }
304     }
305 
306     init {
307         dumpManager.registerCriticalDumpable(javaClass.name, this)
308         if (WAKE_UP_ANIMATION_ENABLED) {
309             keyguardStateController.addCallback(keyguardStateCallback)
310         }
311         statusBarStateController.addCallback(statusBarStateCallback)
312         notificationShadeWindowController.setScrimsVisibilityListener {
313             // Stop blur effect when scrims is opaque to avoid unnecessary GPU composition.
314             visibility -> scrimsVisible = visibility == ScrimController.OPAQUE
315         }
316         shadeAnimation.setStiffness(SpringForce.STIFFNESS_LOW)
317         shadeAnimation.setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
318         updateResources()
319         configurationController.addCallback(object : ConfigurationController.ConfigurationListener {
320             override fun onConfigChanged(newConfig: Configuration?) {
321                 updateResources()
322             }
323         })
324     }
325 
326     private fun updateResources() {
327         inSplitShade = LargeScreenUtils.shouldUseSplitNotificationShade(context.resources)
328     }
329 
330     fun addListener(listener: DepthListener) {
331         listeners.add(listener)
332     }
333 
334     fun removeListener(listener: DepthListener) {
335         listeners.remove(listener)
336     }
337 
338     /**
339      * Update blurs when pulling down the shade
340      */
341     override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) {
342         val rawFraction = event.fraction
343         val tracking = event.tracking
344         val timestamp = SystemClock.elapsedRealtimeNanos()
345         val expansion = MathUtils.saturate(
346                 (rawFraction - panelPullDownMinFraction) / (1f - panelPullDownMinFraction))
347 
348         if (shadeExpansion == expansion && prevTracking == tracking) {
349             prevTimestamp = timestamp
350             return
351         }
352 
353         var deltaTime = 1f
354         if (prevTimestamp < 0) {
355             prevTimestamp = timestamp
356         } else {
357             deltaTime = MathUtils.constrain(
358                     ((timestamp - prevTimestamp) / 1E9).toFloat(), 0.00001f, 1f)
359         }
360 
361         val diff = expansion - shadeExpansion
362         val shadeDirection = sign(diff).toInt()
363         val shadeVelocity = MathUtils.constrain(
364             VELOCITY_SCALE * diff / deltaTime, MIN_VELOCITY, MAX_VELOCITY)
365         updateShadeAnimationBlur(expansion, tracking, shadeVelocity, shadeDirection)
366 
367         prevShadeDirection = shadeDirection
368         prevShadeVelocity = shadeVelocity
369         shadeExpansion = expansion
370         prevTracking = tracking
371         prevTimestamp = timestamp
372 
373         scheduleUpdate()
374     }
375 
376     private fun updateShadeAnimationBlur(
377         expansion: Float,
378         tracking: Boolean,
379         velocity: Float,
380         direction: Int
381     ) {
382         if (shouldApplyShadeBlur()) {
383             if (expansion > 0f) {
384                 // Blur view if user starts animating in the shade.
385                 if (isClosed) {
386                     animateBlur(true, velocity)
387                     isClosed = false
388                 }
389 
390                 // If we were blurring out and the user stopped the animation, blur view.
391                 if (tracking && !isBlurred) {
392                     animateBlur(true, 0f)
393                 }
394 
395                 // If shade is being closed and the user isn't interacting with it, un-blur.
396                 if (!tracking && direction < 0 && isBlurred) {
397                     animateBlur(false, velocity)
398                 }
399 
400                 if (expansion == 1f) {
401                     if (!isOpen) {
402                         isOpen = true
403                         // If shade is open and view is not blurred, blur.
404                         if (!isBlurred) {
405                             animateBlur(true, velocity)
406                         }
407                     }
408                 } else {
409                     isOpen = false
410                 }
411                 // Automatic animation when the user closes the shade.
412             } else if (!isClosed) {
413                 isClosed = true
414                 // If shade is closed and view is not blurred, blur.
415                 if (isBlurred) {
416                     animateBlur(false, velocity)
417                 }
418             }
419         } else {
420             animateBlur(false, 0f)
421             isClosed = true
422             isOpen = false
423         }
424     }
425 
426     private fun animateBlur(blur: Boolean, velocity: Float) {
427         isBlurred = blur
428 
429         val targetBlurNormalized = if (blur && shouldApplyShadeBlur()) {
430             1f
431         } else {
432             0f
433         }
434 
435         shadeAnimation.setStartVelocity(velocity)
436         shadeAnimation.animateTo(blurUtils.blurRadiusOfRatio(targetBlurNormalized).toInt())
437     }
438 
439     private fun scheduleUpdate() {
440         if (updateScheduled) {
441             return
442         }
443         updateScheduled = true
444         choreographer.postFrameCallback(updateBlurCallback)
445     }
446 
447     /**
448      * Should blur be applied to the shade currently. This is mainly used to make sure that
449      * on the lockscreen, the wallpaper isn't blurred.
450      */
451     private fun shouldApplyShadeBlur(): Boolean {
452         val state = statusBarStateController.state
453         return (state == StatusBarState.SHADE || state == StatusBarState.SHADE_LOCKED) &&
454                 !keyguardStateController.isKeyguardFadingAway
455     }
456 
457     override fun dump(pw: PrintWriter, args: Array<out String>) {
458         IndentingPrintWriter(pw, "  ").let {
459             it.println("StatusBarWindowBlurController:")
460             it.increaseIndent()
461             it.println("shadeExpansion: $shadeExpansion")
462             it.println("shouldApplyShadeBlur: ${shouldApplyShadeBlur()}")
463             it.println("shadeAnimation: ${shadeAnimation.radius}")
464             it.println("brightnessMirrorRadius: ${brightnessMirrorSpring.radius}")
465             it.println("wakeAndUnlockBlur: $wakeAndUnlockBlurRadius")
466             it.println("blursDisabledForAppLaunch: $blursDisabledForAppLaunch")
467             it.println("qsPanelExpansion: $qsPanelExpansion")
468             it.println("transitionToFullShadeProgress: $transitionToFullShadeProgress")
469             it.println("lastAppliedBlur: $lastAppliedBlur")
470         }
471     }
472 
473     /**
474      * Animation helper that smoothly animates the depth using a spring and deals with frame
475      * invalidation.
476      */
477     inner class DepthAnimation() {
478         /**
479          * Blur radius visible on the UI, in pixels.
480          */
481         var radius = 0f
482 
483         /**
484          * Depth ratio of the current blur radius.
485          */
486         val ratio
487             get() = blurUtils.ratioOfBlurRadius(radius)
488 
489         /**
490          * Radius that we're animating to.
491          */
492         private var pendingRadius = -1
493 
494         private var springAnimation = SpringAnimation(this, object :
495                 FloatPropertyCompat<DepthAnimation>("blurRadius") {
496             override fun setValue(rect: DepthAnimation?, value: Float) {
497                 radius = value
498                 scheduleUpdate()
499             }
500 
501             override fun getValue(rect: DepthAnimation?): Float {
502                 return radius
503             }
504         })
505 
506         init {
507             springAnimation.spring = SpringForce(0.0f)
508             springAnimation.spring.dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
509             springAnimation.spring.stiffness = SpringForce.STIFFNESS_HIGH
510             springAnimation.addEndListener { _, _, _, _ -> pendingRadius = -1 }
511         }
512 
513         fun animateTo(newRadius: Int) {
514             if (pendingRadius == newRadius) {
515                 return
516             }
517             pendingRadius = newRadius
518             springAnimation.animateToFinalPosition(newRadius.toFloat())
519         }
520 
521         fun finishIfRunning() {
522             if (springAnimation.isRunning) {
523                 springAnimation.skipToEnd()
524             }
525         }
526 
527         fun setStiffness(stiffness: Float) {
528             springAnimation.spring.stiffness = stiffness
529         }
530 
531         fun setDampingRatio(dampingRation: Float) {
532             springAnimation.spring.dampingRatio = dampingRation
533         }
534 
535         fun setStartVelocity(velocity: Float) {
536             springAnimation.setStartVelocity(velocity)
537         }
538     }
539 
540     /**
541      * Invoked when changes are needed in z-space
542      */
543     interface DepthListener {
544         /**
545          * Current wallpaper zoom out, where 0 is the closest, and 1 the farthest
546          */
547         fun onWallpaperZoomOutChanged(zoomOut: Float)
548 
549         @JvmDefault
550         fun onBlurRadiusChanged(blurRadius: Int) {}
551     }
552 }
553