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