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