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.Display
12 import android.view.Surface
13 import android.view.View
14 import android.view.WindowManager.fixScale
15 import com.android.internal.jank.InteractionJankMonitor
16 import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF
17 import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD
18 import com.android.systemui.DejankUtils
19 import com.android.app.animation.Interpolators
20 import com.android.systemui.dagger.SysUISingleton
21 import com.android.systemui.keyguard.KeyguardViewMediator
22 import com.android.systemui.keyguard.WakefulnessLifecycle
23 import com.android.systemui.shade.ShadeViewController
24 import com.android.systemui.statusbar.CircleReveal
25 import com.android.systemui.statusbar.LightRevealScrim
26 import com.android.systemui.statusbar.NotificationShadeWindowController
27 import com.android.systemui.statusbar.StatusBarState
28 import com.android.systemui.statusbar.StatusBarStateControllerImpl
29 import com.android.systemui.statusbar.notification.AnimatableProperty
30 import com.android.systemui.statusbar.notification.PropertyAnimator
31 import com.android.systemui.statusbar.notification.stack.AnimationProperties
32 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
33 import com.android.systemui.statusbar.policy.KeyguardStateController
34 import com.android.systemui.util.TraceUtils
35 import com.android.systemui.util.settings.GlobalSettings
36 import javax.inject.Inject
37 
38 /**
39  * When to show the keyguard (AOD) view. This should be once the light reveal scrim is barely
40  * visible, because the transition to KEYGUARD causes brief jank.
41  */
42 private const val ANIMATE_IN_KEYGUARD_DELAY = 600L
43 
44 /**
45  * Duration for the light reveal portion of the animation.
46  */
47 private const val LIGHT_REVEAL_ANIMATION_DURATION = 750L
48 
49 /**
50  * Controller for the unlocked screen off animation, which runs when the device is going to sleep
51  * and we're unlocked.
52  *
53  * This animation uses a [LightRevealScrim] that lives in the status bar to hide the screen contents
54  * and then animates in the AOD UI.
55  */
56 @SysUISingleton
57 class UnlockedScreenOffAnimationController @Inject constructor(
58     private val context: Context,
59     private val wakefulnessLifecycle: WakefulnessLifecycle,
60     private val statusBarStateControllerImpl: StatusBarStateControllerImpl,
61     private val keyguardViewMediatorLazy: dagger.Lazy<KeyguardViewMediator>,
62     private val keyguardStateController: KeyguardStateController,
63     private val dozeParameters: dagger.Lazy<DozeParameters>,
64     private val globalSettings: GlobalSettings,
65     private val notifShadeWindowControllerLazy: dagger.Lazy<NotificationShadeWindowController>,
66     private val interactionJankMonitor: InteractionJankMonitor,
67     private val powerManager: PowerManager,
68     private val handler: Handler = Handler()
69 ) : WakefulnessLifecycle.Observer, ScreenOffAnimation {
70     private lateinit var centralSurfaces: CentralSurfaces
71     private lateinit var shadeViewController: ShadeViewController
72     /**
73      * Whether or not [initialize] has been called to provide us with the StatusBar,
74      * NotificationPanelViewController, and LightRevealSrim so that we can run the unlocked screen
75      * off animation.
76      */
77     private var initialized = false
78 
79     private lateinit var lightRevealScrim: LightRevealScrim
80 
81     private var animatorDurationScale = 1f
82     private var shouldAnimateInKeyguard = false
83     private var lightRevealAnimationPlaying = false
84     private var aodUiAnimationPlaying = false
85 
86     /**
87      * The result of our decision whether to play the screen off animation in
88      * [onStartedGoingToSleep], or null if we haven't made that decision yet or aren't going to
89      * sleep.
90      */
91     private var decidedToAnimateGoingToSleep: Boolean? = null
92 
93     private val lightRevealAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
94         duration = LIGHT_REVEAL_ANIMATION_DURATION
95         interpolator = Interpolators.LINEAR
96         addUpdateListener {
97             if (lightRevealScrim.revealEffect !is CircleReveal) {
98                 lightRevealScrim.revealAmount = it.animatedValue as Float
99             }
100             if (lightRevealScrim.isScrimAlmostOccludes &&
101                     interactionJankMonitor.isInstrumenting(CUJ_SCREEN_OFF)) {
102                 // ends the instrument when the scrim almost occludes the screen.
103                 // because the following janky frames might not be perceptible.
104                 interactionJankMonitor.end(CUJ_SCREEN_OFF)
105             }
106         }
107         addListener(object : AnimatorListenerAdapter() {
108             override fun onAnimationCancel(animation: Animator) {
109                 if (lightRevealScrim.revealEffect !is CircleReveal) {
110                     lightRevealScrim.revealAmount = 1f
111                 }
112             }
113 
114             override fun onAnimationEnd(animation: Animator) {
115                 lightRevealAnimationPlaying = false
116                 interactionJankMonitor.end(CUJ_SCREEN_OFF)
117             }
118 
119             override fun onAnimationStart(animation: Animator) {
120                 interactionJankMonitor.begin(
121                         notifShadeWindowControllerLazy.get().windowRootView, CUJ_SCREEN_OFF)
122             }
123         })
124     }
125 
126     // FrameCallback used to delay starting the light reveal animation until the next frame
127     private val startLightRevealCallback = TraceUtils.namedRunnable("startLightReveal") {
128         lightRevealAnimationPlaying = true
129         lightRevealAnimator.start()
130     }
131 
132     private val animatorDurationScaleObserver = object : ContentObserver(null) {
133         override fun onChange(selfChange: Boolean) {
134             updateAnimatorDurationScale()
135         }
136     }
137 
138     override fun initialize(
139         centralSurfaces: CentralSurfaces,
140         shadeViewController: ShadeViewController,
141         lightRevealScrim: LightRevealScrim
142     ) {
143         this.initialized = true
144         this.lightRevealScrim = lightRevealScrim
145         this.centralSurfaces = centralSurfaces
146         this.shadeViewController = shadeViewController
147 
148         updateAnimatorDurationScale()
149         globalSettings.registerContentObserver(
150                 Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE),
151                 /* notify for descendants */ false,
152                 animatorDurationScaleObserver)
153         wakefulnessLifecycle.addObserver(this)
154     }
155 
156     fun updateAnimatorDurationScale() {
157         animatorDurationScale = fixScale(
158                 globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f))
159     }
160 
161     override fun shouldDelayKeyguardShow(): Boolean =
162         shouldPlayAnimation()
163 
164     override fun isKeyguardShowDelayed(): Boolean =
165         isAnimationPlaying()
166 
167     /**
168      * Animates in the provided keyguard view, ending in the same position that it will be in on
169      * AOD.
170      */
171     override fun animateInKeyguard(keyguardView: View, after: Runnable) {
172         shouldAnimateInKeyguard = false
173         keyguardView.alpha = 0f
174         keyguardView.visibility = View.VISIBLE
175 
176         val currentY = keyguardView.y
177 
178         // Move the keyguard up by 10% so we can animate it back down.
179         keyguardView.y = currentY - keyguardView.height * 0.1f
180 
181         val duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP
182 
183         // We animate the Y properly separately using the PropertyAnimator, as the panel
184         // view also needs to update the end position.
185         PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.Y)
186         PropertyAnimator.setProperty(keyguardView, AnimatableProperty.Y, currentY,
187                 AnimationProperties().setDuration(duration.toLong()),
188                 true /* animate */)
189 
190         // Cancel any existing CUJs before starting the animation
191         interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD)
192         PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.ALPHA)
193         PropertyAnimator.setProperty(
194             keyguardView, AnimatableProperty.ALPHA, 1f,
195             AnimationProperties()
196                 .setDelay(0)
197                 .setDuration(duration.toLong())
198                 .setAnimationEndAction {
199                     aodUiAnimationPlaying = false
200 
201                     // Lock the keyguard if it was waiting for the screen off animation to end.
202                     keyguardViewMediatorLazy.get().maybeHandlePendingLock()
203 
204                     // Tell the CentralSurfaces to become keyguard for real - we waited on that
205                     // since it is slow and would have caused the animation to jank.
206                     centralSurfaces.updateIsKeyguard()
207 
208                     // Run the callback given to us by the KeyguardVisibilityHelper.
209                     after.run()
210 
211                     // Done going to sleep, reset this flag.
212                     decidedToAnimateGoingToSleep = null
213 
214                     interactionJankMonitor.end(CUJ_SCREEN_OFF_SHOW_AOD)
215                 }
216                 .setAnimationCancelAction {
217                     // If we're cancelled, reset state flags/listeners. The end action above
218                     // will not be called, which is what we want since that will finish the
219                     // screen off animation and show the lockscreen, which we don't want if we
220                     // were cancelled.
221                     aodUiAnimationPlaying = false
222                     decidedToAnimateGoingToSleep = null
223                     interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD)
224                 }
225                 .setCustomInterpolator(View.ALPHA, Interpolators.FAST_OUT_SLOW_IN),
226             true /* animate */)
227         val builder = InteractionJankMonitor.Configuration.Builder
228             .withView(
229                     InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD,
230                     checkNotNull(notifShadeWindowControllerLazy.get().windowRootView)
231             )
232             .setTag(statusBarStateControllerImpl.getClockId())
233 
234         interactionJankMonitor.begin(builder)
235     }
236 
237     override fun onStartedWakingUp() {
238         // Waking up, so reset this flag.
239         decidedToAnimateGoingToSleep = null
240 
241         shouldAnimateInKeyguard = false
242         DejankUtils.removeCallbacks(startLightRevealCallback)
243         lightRevealAnimator.cancel()
244         handler.removeCallbacksAndMessages(null)
245     }
246 
247     override fun onFinishedWakingUp() {
248         // Set this to false in onFinishedWakingUp rather than onStartedWakingUp so that other
249         // observers (such as CentralSurfaces) can ask us whether we were playing the screen off
250         // animation and reset accordingly.
251         aodUiAnimationPlaying = false
252 
253         // If we can't control the screen off animation, we shouldn't mess with the
254         // CentralSurfaces's keyguard state unnecessarily.
255         if (dozeParameters.get().canControlUnlockedScreenOff()) {
256             // Make sure the status bar is in the correct keyguard state, forcing it if necessary.
257             // This is required if the screen off animation is cancelled, since it might be
258             // incorrectly left in the KEYGUARD or SHADE states depending on when it was cancelled
259             // and whether 'lock instantly' is enabled. We need to force it so that the state is set
260             // even if we're going from SHADE to SHADE or KEYGUARD to KEYGUARD, since we might have
261             // changed parts of the UI (such as showing AOD in the shade) without actually changing
262             // the StatusBarState. This ensures that the UI definitely reflects the desired state.
263             centralSurfaces.updateIsKeyguard(true /* forceStateChange */)
264         }
265     }
266 
267     override fun startAnimation(): Boolean {
268         if (shouldPlayUnlockedScreenOffAnimation()) {
269             decidedToAnimateGoingToSleep = true
270 
271             shouldAnimateInKeyguard = true
272 
273             // Start the animation on the next frame. startAnimation() is called after
274             // PhoneWindowManager makes a binder call to System UI on
275             // IKeyguardService#onStartedGoingToSleep(). By the time we get here, system_server is
276             // already busy making changes to PowerManager and DisplayManager. This increases our
277             // chance of missing the first frame, so to mitigate this we should start the animation
278             // on the next frame.
279             DejankUtils.postAfterTraversal(startLightRevealCallback)
280             handler.postDelayed({
281                 // Only run this callback if the device is sleeping (not interactive). This callback
282                 // is removed in onStartedWakingUp, but since that event is asynchronously
283                 // dispatched, a race condition could make it possible for this callback to be run
284                 // as the device is waking up. That results in the AOD UI being shown while we wake
285                 // up, with unpredictable consequences.
286                 if (!powerManager.isInteractive(Display.DEFAULT_DISPLAY) &&
287                         shouldAnimateInKeyguard) {
288                     aodUiAnimationPlaying = true
289 
290                     // Show AOD. That'll cause the KeyguardVisibilityHelper to call
291                     // #animateInKeyguard.
292                     shadeViewController.showAodUi()
293                 }
294             }, (ANIMATE_IN_KEYGUARD_DELAY * animatorDurationScale).toLong())
295 
296             return true
297         } else {
298             decidedToAnimateGoingToSleep = false
299             return false
300         }
301     }
302 
303     /**
304      * Whether we want to play the screen off animation when the phone starts going to sleep, based
305      * on the current state of the device.
306      */
307     fun shouldPlayUnlockedScreenOffAnimation(): Boolean {
308         // If we haven't been initialized yet, we don't have a StatusBar/LightRevealScrim yet, so we
309         // can't perform the animation.
310         if (!initialized) {
311             return false
312         }
313 
314         // If the device isn't in a state where we can control unlocked screen off (no AOD enabled,
315         // power save, etc.) then we shouldn't try to do so.
316         if (!dozeParameters.get().canControlUnlockedScreenOff()) {
317             return false
318         }
319 
320         // If we explicitly already decided not to play the screen off animation, then never change
321         // our mind.
322         if (decidedToAnimateGoingToSleep == false) {
323             return false
324         }
325 
326         // If animations are disabled system-wide, don't play this one either.
327         if (Settings.Global.getString(
328                 context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE) == "0") {
329             return false
330         }
331 
332         // We only play the unlocked screen off animation if we are... unlocked.
333         if (statusBarStateControllerImpl.state != StatusBarState.SHADE) {
334             return false
335         }
336 
337         // We currently draw both the light reveal scrim, and the AOD UI, in the shade. If it's
338         // already expanded and showing notifications/QS, the animation looks really messy. For now,
339         // disable it if the notification panel is expanded.
340         if ((!this::centralSurfaces.isInitialized ||
341                 shadeViewController.isPanelExpanded) &&
342                 // Status bar might be expanded because we have started
343                 // playing the animation already
344                 !isAnimationPlaying()
345         ) {
346             return false
347         }
348 
349         // If we're not allowed to rotate the keyguard, it can only be displayed in zero-degree
350         // portrait. If we're in another orientation, disable the screen off animation so we don't
351         // animate in the keyguard AOD UI sideways or upside down.
352         if (!keyguardStateController.isKeyguardScreenRotationAllowed &&
353             context.display?.rotation != Surface.ROTATION_0) {
354             return false
355         }
356 
357         // Otherwise, good to go.
358         return true
359     }
360 
361     override fun shouldDelayDisplayDozeTransition(): Boolean =
362         shouldPlayUnlockedScreenOffAnimation()
363 
364     /**
365      * Whether we're doing the light reveal animation or we're done with that and animating in the
366      * AOD UI.
367      */
368     override fun isAnimationPlaying(): Boolean {
369         return lightRevealAnimationPlaying || aodUiAnimationPlaying
370     }
371 
372     override fun shouldAnimateInKeyguard(): Boolean =
373         shouldAnimateInKeyguard
374 
375     override fun shouldHideScrimOnWakeUp(): Boolean =
376         isScreenOffLightRevealAnimationPlaying()
377 
378     override fun overrideNotificationsDozeAmount(): Boolean =
379         shouldPlayUnlockedScreenOffAnimation() && isAnimationPlaying()
380 
381     override fun shouldShowAodIconsWhenShade(): Boolean =
382         isAnimationPlaying()
383 
384     override fun shouldAnimateAodIcons(): Boolean =
385         shouldPlayUnlockedScreenOffAnimation()
386 
387     override fun shouldPlayAnimation(): Boolean =
388         shouldPlayUnlockedScreenOffAnimation()
389 
390     /**
391      * Whether the light reveal animation is playing. The second part of the screen off animation,
392      * where AOD animates in, might still be playing if this returns false.
393      */
394     fun isScreenOffLightRevealAnimationPlaying(): Boolean {
395         return lightRevealAnimationPlaying
396     }
397 }
398