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