1 package com.android.systemui.statusbar.phone
2 
3 import android.animation.Animator
4 import android.animation.AnimatorListenerAdapter
5 import android.animation.ValueAnimator
6 import android.content.Context
7 import android.database.ContentObserver
8 import android.os.Handler
9 import android.os.PowerManager
10 import android.provider.Settings
11 import android.view.Surface
12 import android.view.View
13 import com.android.systemui.animation.Interpolators
14 import com.android.systemui.dagger.SysUISingleton
15 import com.android.systemui.keyguard.KeyguardViewMediator
16 import com.android.systemui.keyguard.WakefulnessLifecycle
17 import com.android.systemui.statusbar.LightRevealScrim
18 import com.android.systemui.statusbar.StatusBarState
19 import com.android.systemui.statusbar.StatusBarStateControllerImpl
20 import com.android.systemui.statusbar.notification.AnimatableProperty
21 import com.android.systemui.statusbar.notification.PropertyAnimator
22 import com.android.systemui.statusbar.notification.stack.AnimationProperties
23 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
24 import com.android.systemui.statusbar.policy.KeyguardStateController
25 import com.android.systemui.util.settings.GlobalSettings
26 import javax.inject.Inject
27 
28 /**
29  * When to show the keyguard (AOD) view. This should be once the light reveal scrim is barely
30  * visible, because the transition to KEYGUARD causes brief jank.
31  */
32 private const val ANIMATE_IN_KEYGUARD_DELAY = 600L
33 
34 /**
35  * Duration for the light reveal portion of the animation.
36  */
37 private const val LIGHT_REVEAL_ANIMATION_DURATION = 750L
38 
39 /**
40  * Controller for the unlocked screen off animation, which runs when the device is going to sleep
41  * and we're unlocked.
42  *
43  * This animation uses a [LightRevealScrim] that lives in the status bar to hide the screen contents
44  * and then animates in the AOD UI.
45  */
46 @SysUISingleton
47 class UnlockedScreenOffAnimationController @Inject constructor(
48     private val context: Context,
49     private val wakefulnessLifecycle: WakefulnessLifecycle,
50     private val statusBarStateControllerImpl: StatusBarStateControllerImpl,
51     private val keyguardViewMediatorLazy: dagger.Lazy<KeyguardViewMediator>,
52     private val keyguardStateController: KeyguardStateController,
53     private val dozeParameters: dagger.Lazy<DozeParameters>,
54     private val globalSettings: GlobalSettings,
55     private val powerManager: PowerManager,
56     private val handler: Handler = Handler()
57 ) : WakefulnessLifecycle.Observer {
58 
59     private lateinit var statusBar: StatusBar
60     private lateinit var lightRevealScrim: LightRevealScrim
61 
62     private var animatorDurationScale = 1f
63     private var shouldAnimateInKeyguard = false
64     private var lightRevealAnimationPlaying = false
65     private var aodUiAnimationPlaying = false
66     private var callbacks = HashSet<Callback>()
67 
68     /**
69      * The result of our decision whether to play the screen off animation in
70      * [onStartedGoingToSleep], or null if we haven't made that decision yet or aren't going to
71      * sleep.
72      */
73     private var decidedToAnimateGoingToSleep: Boolean? = null
74 
75     private val lightRevealAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
76         duration = LIGHT_REVEAL_ANIMATION_DURATION
77         interpolator = Interpolators.LINEAR
78         addUpdateListener {
79             lightRevealScrim.revealAmount = it.animatedValue as Float
80             sendUnlockedScreenOffProgressUpdate(
81                     1f - (it.animatedFraction as Float),
82                     1f - (it.animatedValue as Float))
83         }
84         addListener(object : AnimatorListenerAdapter() {
85             override fun onAnimationCancel(animation: Animator?) {
86                 lightRevealScrim.revealAmount = 1f
87                 lightRevealAnimationPlaying = false
88                 sendUnlockedScreenOffProgressUpdate(0f, 0f)
89             }
90 
91             override fun onAnimationEnd(animation: Animator?) {
92                 lightRevealAnimationPlaying = false
93             }
94         })
95     }
96 
97     val animatorDurationScaleObserver = object : ContentObserver(null) {
98         override fun onChange(selfChange: Boolean) {
99             updateAnimatorDurationScale()
100         }
101     }
102 
103     fun initialize(
104         statusBar: StatusBar,
105         lightRevealScrim: LightRevealScrim
106     ) {
107         this.lightRevealScrim = lightRevealScrim
108         this.statusBar = statusBar
109 
110         updateAnimatorDurationScale()
111         globalSettings.registerContentObserver(
112                 Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE),
113                 /* notify for descendants */ false,
114                 animatorDurationScaleObserver)
115         wakefulnessLifecycle.addObserver(this)
116     }
117 
118     fun updateAnimatorDurationScale() {
119         animatorDurationScale =
120                 globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f)
121     }
122 
123     /**
124      * Animates in the provided keyguard view, ending in the same position that it will be in on
125      * AOD.
126      */
127     fun animateInKeyguard(keyguardView: View, after: Runnable) {
128         shouldAnimateInKeyguard = false
129         keyguardView.alpha = 0f
130         keyguardView.visibility = View.VISIBLE
131 
132         val currentY = keyguardView.y
133 
134         // Move the keyguard up by 10% so we can animate it back down.
135         keyguardView.y = currentY - keyguardView.height * 0.1f
136 
137         val duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP
138 
139         // We animate the Y properly separately using the PropertyAnimator, as the panel
140         // view also needs to update the end position.
141         PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.Y)
142         PropertyAnimator.setProperty<View>(keyguardView, AnimatableProperty.Y, currentY,
143                 AnimationProperties().setDuration(duration.toLong()),
144                 true /* animate */)
145 
146         keyguardView.animate()
147                 .setDuration(duration.toLong())
148                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
149                 .alpha(1f)
150                 .setListener(object : AnimatorListenerAdapter() {
151                     override fun onAnimationEnd(animation: Animator?) {
152                         aodUiAnimationPlaying = false
153 
154                         // Lock the keyguard if it was waiting for the screen off animation to end.
155                         keyguardViewMediatorLazy.get().maybeHandlePendingLock()
156 
157                         // Tell the StatusBar to become keyguard for real - we waited on that since
158                         // it is slow and would have caused the animation to jank.
159                         statusBar.updateIsKeyguard()
160 
161                         // Run the callback given to us by the KeyguardVisibilityHelper.
162                         after.run()
163 
164                         // Done going to sleep, reset this flag.
165                         decidedToAnimateGoingToSleep = null
166                         // We need to unset the listener. These are persistent for future animators
167                         keyguardView.animate().setListener(null)
168                     }
169                 })
170                 .start()
171     }
172 
173     override fun onStartedWakingUp() {
174         // Waking up, so reset this flag.
175         decidedToAnimateGoingToSleep = null
176 
177         shouldAnimateInKeyguard = false
178         lightRevealAnimator.cancel()
179         handler.removeCallbacksAndMessages(null)
180     }
181 
182     override fun onFinishedWakingUp() {
183         // Set this to false in onFinishedWakingUp rather than onStartedWakingUp so that other
184         // observers (such as StatusBar) can ask us whether we were playing the screen off animation
185         // and reset accordingly.
186         aodUiAnimationPlaying = false
187 
188         // If we can't control the screen off animation, we shouldn't mess with the StatusBar's
189         // keyguard state unnecessarily.
190         if (dozeParameters.get().canControlUnlockedScreenOff()) {
191             // Make sure the status bar is in the correct keyguard state, forcing it if necessary.
192             // This is required if the screen off animation is cancelled, since it might be
193             // incorrectly left in the KEYGUARD or SHADE states depending on when it was cancelled
194             // and whether 'lock instantly' is enabled. We need to force it so that the state is set
195             // even if we're going from SHADE to SHADE or KEYGUARD to KEYGUARD, since we might have
196             // changed parts of the UI (such as showing AOD in the shade) without actually changing
197             // the StatusBarState. This ensures that the UI definitely reflects the desired state.
198             statusBar.updateIsKeyguard(true /* force */)
199         }
200     }
201 
202     override fun onStartedGoingToSleep() {
203         if (dozeParameters.get().shouldControlUnlockedScreenOff()) {
204             decidedToAnimateGoingToSleep = true
205 
206             shouldAnimateInKeyguard = true
207             lightRevealAnimationPlaying = true
208             lightRevealAnimator.start()
209             handler.postDelayed({
210                 // Only run this callback if the device is sleeping (not interactive). This callback
211                 // is removed in onStartedWakingUp, but since that event is asynchronously
212                 // dispatched, a race condition could make it possible for this callback to be run
213                 // as the device is waking up. That results in the AOD UI being shown while we wake
214                 // up, with unpredictable consequences.
215                 if (!powerManager.isInteractive) {
216                     aodUiAnimationPlaying = true
217 
218                     // Show AOD. That'll cause the KeyguardVisibilityHelper to call
219                     // #animateInKeyguard.
220                     statusBar.notificationPanelViewController.showAodUi()
221                 }
222             }, (ANIMATE_IN_KEYGUARD_DELAY * animatorDurationScale).toLong())
223         } else {
224             decidedToAnimateGoingToSleep = false
225         }
226     }
227 
228     /**
229      * Whether we want to play the screen off animation when the phone starts going to sleep, based
230      * on the current state of the device.
231      */
232     fun shouldPlayUnlockedScreenOffAnimation(): Boolean {
233         // If we explicitly already decided not to play the screen off animation, then never change
234         // our mind.
235         if (decidedToAnimateGoingToSleep == false) {
236             return false
237         }
238 
239         // If animations are disabled system-wide, don't play this one either.
240         if (Settings.Global.getString(
241                 context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE) == "0") {
242             return false
243         }
244 
245         // We only play the unlocked screen off animation if we are... unlocked.
246         if (statusBarStateControllerImpl.state != StatusBarState.SHADE) {
247             return false
248         }
249 
250         // We currently draw both the light reveal scrim, and the AOD UI, in the shade. If it's
251         // already expanded and showing notifications/QS, the animation looks really messy. For now,
252         // disable it if the notification panel is not fully collapsed.
253         if (!this::statusBar.isInitialized ||
254                 !statusBar.notificationPanelViewController.isFullyCollapsed) {
255             return false
256         }
257 
258         // If we're not allowed to rotate the keyguard, it can only be displayed in zero-degree
259         // portrait. If we're in another orientation, disable the screen off animation so we don't
260         // animate in the keyguard AOD UI sideways or upside down.
261         if (!keyguardStateController.isKeyguardScreenRotationAllowed &&
262             context.display.rotation != Surface.ROTATION_0) {
263             return false
264         }
265 
266         // Otherwise, good to go.
267         return true
268     }
269 
270     fun addCallback(callback: Callback) {
271         callbacks.add(callback)
272     }
273 
274     fun removeCallback(callback: Callback) {
275         callbacks.remove(callback)
276     }
277 
278     fun sendUnlockedScreenOffProgressUpdate(linear: Float, eased: Float) {
279         callbacks.forEach {
280             it.onUnlockedScreenOffProgressUpdate(linear, eased)
281         }
282     }
283 
284 /**
285      * Whether we're doing the light reveal animation or we're done with that and animating in the
286      * AOD UI.
287      */
288     fun isScreenOffAnimationPlaying(): Boolean {
289         return lightRevealAnimationPlaying || aodUiAnimationPlaying
290     }
291 
292     fun shouldAnimateInKeyguard(): Boolean {
293         return shouldAnimateInKeyguard
294     }
295 
296     /**
297      * Whether the light reveal animation is playing. The second part of the screen off animation,
298      * where AOD animates in, might still be playing if this returns false.
299      */
300     fun isScreenOffLightRevealAnimationPlaying(): Boolean {
301         return lightRevealAnimationPlaying
302     }
303 
304     interface Callback {
305         fun onUnlockedScreenOffProgressUpdate(linear: Float, eased: Float)
306     }
307 }
308