• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.dagger.SysUISingleton
22 import com.android.systemui.dagger.qualifiers.Main
23 import com.android.systemui.keyguard.KeyguardViewMediator
24 import com.android.systemui.keyguard.WakefulnessLifecycle
25 import com.android.systemui.shade.ShadeViewController
26 import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor
27 import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor
28 import com.android.systemui.shared.Flags.ambientAod
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     @Main private val 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 
91     /**
92      * The result of our decision whether to play the screen off animation in
93      * [onStartedGoingToSleep], or null if we haven't made that decision yet or aren't going to
94      * sleep.
95      */
96     private var decidedToAnimateGoingToSleep: Boolean? = null
97 
98     private val lightRevealAnimator =
<lambda>null99         ValueAnimator.ofFloat(1f, 0f).apply {
100             duration = LIGHT_REVEAL_ANIMATION_DURATION
101             interpolator = Interpolators.LINEAR
102             addUpdateListener {
103                 if (ambientAod()) return@addUpdateListener
104                 if (lightRevealScrim.revealEffect !is CircleReveal) {
105                     lightRevealScrim.revealAmount = it.animatedValue as Float
106                 }
107                 if (
108                     lightRevealScrim.isScrimAlmostOccludes &&
109                         interactionJankMonitor.isInstrumenting(CUJ_SCREEN_OFF)
110                 ) {
111                     // ends the instrument when the scrim almost occludes the screen.
112                     // because the following janky frames might not be perceptible.
113                     interactionJankMonitor.end(CUJ_SCREEN_OFF)
114                 }
115             }
116             addListener(
117                 object : AnimatorListenerAdapter() {
118                     override fun onAnimationCancel(animation: Animator) {
119                         if (ambientAod()) return
120                         if (lightRevealScrim.revealEffect !is CircleReveal) {
121                             lightRevealScrim.revealAmount = 1f
122                         }
123                     }
124 
125                     override fun onAnimationEnd(animation: Animator) {
126                         lightRevealAnimationPlaying = false
127                         interactionJankMonitor.end(CUJ_SCREEN_OFF)
128                     }
129 
130                     override fun onAnimationStart(animation: Animator) {
131                         interactionJankMonitor.begin(
132                             notifShadeWindowControllerLazy.get().windowRootView,
133                             CUJ_SCREEN_OFF,
134                         )
135                     }
136                 }
137             )
138         }
139 
140     // FrameCallback used to delay starting the light reveal animation until the next frame
141     private val startLightRevealCallback =
<lambda>null142         namedRunnable("startLightReveal") {
143             lightRevealAnimationPlaying = true
144             lightRevealAnimator.start()
145         }
146 
147     private val animatorDurationScaleObserver =
148         object : ContentObserver(null) {
onChangenull149             override fun onChange(selfChange: Boolean) {
150                 updateAnimatorDurationScale()
151             }
152         }
153 
initializenull154     override fun initialize(
155         centralSurfaces: CentralSurfaces,
156         shadeViewController: ShadeViewController,
157         lightRevealScrim: LightRevealScrim,
158     ) {
159         this.initialized = true
160         this.lightRevealScrim = lightRevealScrim
161         this.centralSurfaces = centralSurfaces
162 
163         updateAnimatorDurationScale()
164         globalSettings.registerContentObserverSync(
165             Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE),
166             /* notify for descendants */ false,
167             animatorDurationScaleObserver,
168         )
169         wakefulnessLifecycle.addObserver(this)
170     }
171 
updateAnimatorDurationScalenull172     fun updateAnimatorDurationScale() {
173         animatorDurationScale =
174             fixScale(globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f))
175     }
176 
shouldDelayKeyguardShownull177     override fun shouldDelayKeyguardShow(): Boolean = shouldPlayAnimation()
178 
179     override fun isKeyguardShowDelayed(): Boolean = isAnimationPlaying()
180 
181     /**
182      * Animates in the provided keyguard view, ending in the same position that it will be in on
183      * AOD.
184      */
185     override fun animateInKeyguard(keyguardView: View, after: Runnable) {
186         shouldAnimateInKeyguard = false
187         keyguardView.alpha = 0f
188         keyguardView.visibility = View.VISIBLE
189 
190         val currentY = keyguardView.y
191 
192         // Move the keyguard up by 10% so we can animate it back down.
193         keyguardView.y = currentY - keyguardView.height * 0.1f
194 
195         val duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP
196 
197         // We animate the Y properly separately using the PropertyAnimator, as the panel
198         // view also needs to update the end position.
199         PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.Y)
200         PropertyAnimator.setProperty(
201             keyguardView,
202             AnimatableProperty.Y,
203             currentY,
204             AnimationProperties().setDuration(duration.toLong()),
205             true, /* animate */
206         )
207 
208         // Cancel any existing CUJs before starting the animation
209         interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD)
210         PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.ALPHA)
211         PropertyAnimator.setProperty(
212             keyguardView,
213             AnimatableProperty.ALPHA,
214             1f,
215             AnimationProperties()
216                 .setDelay(0)
217                 .setDuration(duration.toLong())
218                 .setAnimationEndAction {
219                     // Lock the keyguard if it was waiting for the screen off animation to end.
220                     keyguardViewMediatorLazy.get().maybeHandlePendingLock()
221 
222                     // Tell the CentralSurfaces to become keyguard for real - we waited on that
223                     // since it is slow and would have caused the animation to jank.
224                     centralSurfaces.updateIsKeyguard()
225 
226                     // Run the callback given to us by the KeyguardVisibilityHelper.
227                     after.run()
228 
229                     // Done going to sleep, reset this flag.
230                     decidedToAnimateGoingToSleep = null
231 
232                     interactionJankMonitor.end(CUJ_SCREEN_OFF_SHOW_AOD)
233                 }
234                 .setAnimationCancelAction {
235                     // If we're cancelled, reset state flags/listeners. The end action above
236                     // will not be called, which is what we want since that will finish the
237                     // screen off animation and show the lockscreen, which we don't want if we
238                     // were cancelled.
239                     decidedToAnimateGoingToSleep = null
240                     interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD)
241                 }
242                 .setCustomInterpolator(View.ALPHA, Interpolators.FAST_OUT_SLOW_IN),
243             true, /* animate */
244         )
245         val builder =
246             InteractionJankMonitor.Configuration.Builder.withView(
247                     InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD,
248                     checkNotNull(notifShadeWindowControllerLazy.get().windowRootView),
249                 )
250                 .setTag(statusBarStateControllerImpl.getClockId())
251 
252         interactionJankMonitor.begin(builder)
253     }
254 
onStartedWakingUpnull255     override fun onStartedWakingUp() {
256         // Waking up, so reset this flag.
257         decidedToAnimateGoingToSleep = null
258 
259         shouldAnimateInKeyguard = false
260         DejankUtils.removeCallbacks(startLightRevealCallback)
261         lightRevealAnimator.cancel()
262         handler.removeCallbacksAndMessages(null)
263     }
264 
onFinishedWakingUpnull265     override fun onFinishedWakingUp() {
266         // If we can't control the screen off animation, we shouldn't mess with the
267         // CentralSurfaces's keyguard state unnecessarily.
268         if (dozeParameters.get().canControlUnlockedScreenOff()) {
269             // Make sure the status bar is in the correct keyguard state, forcing it if necessary.
270             // This is required if the screen off animation is cancelled, since it might be
271             // incorrectly left in the KEYGUARD or SHADE states depending on when it was cancelled
272             // and whether 'lock instantly' is enabled. We need to force it so that the state is set
273             // even if we're going from SHADE to SHADE or KEYGUARD to KEYGUARD, since we might have
274             // changed parts of the UI (such as showing AOD in the shade) without actually changing
275             // the StatusBarState. This ensures that the UI definitely reflects the desired state.
276             centralSurfaces.updateIsKeyguard(true /* forceStateChange */)
277         }
278     }
279 
startAnimationnull280     override fun startAnimation(): Boolean {
281         if (shouldPlayUnlockedScreenOffAnimation()) {
282             decidedToAnimateGoingToSleep = true
283 
284             shouldAnimateInKeyguard = true
285 
286             // Start the animation on the next frame. startAnimation() is called after
287             // PhoneWindowManager makes a binder call to System UI on
288             // IKeyguardService#onStartedGoingToSleep(). By the time we get here, system_server is
289             // already busy making changes to PowerManager and DisplayManager. This increases our
290             // chance of missing the first frame, so to mitigate this we should start the animation
291             // on the next frame.
292             DejankUtils.postAfterTraversal(startLightRevealCallback)
293             handler.postDelayed(
294                 {
295                     // Only run this callback if the device is sleeping (not interactive). This
296                     // callback
297                     // is removed in onStartedWakingUp, but since that event is asynchronously
298                     // dispatched, a race condition could make it possible for this callback to be
299                     // run
300                     // as the device is waking up. That results in the AOD UI being shown while we
301                     // wake
302                     // up, with unpredictable consequences.
303                     if (
304                         !powerManager.isInteractive(Display.DEFAULT_DISPLAY) &&
305                             shouldAnimateInKeyguard
306                     ) {
307                         // Show AOD. That'll cause the KeyguardVisibilityHelper to call
308                         // #animateInKeyguard.
309                         shadeLockscreenInteractorLazy.get().showAodUi()
310                     }
311                 },
312                 (ANIMATE_IN_KEYGUARD_DELAY * animatorDurationScale).toLong(),
313             )
314 
315             return true
316         } else {
317             decidedToAnimateGoingToSleep = false
318             return false
319         }
320     }
321 
322     /**
323      * Whether we want to play the screen off animation when the phone starts going to sleep, based
324      * on the current state of the device.
325      */
shouldPlayUnlockedScreenOffAnimationnull326     fun shouldPlayUnlockedScreenOffAnimation(): Boolean {
327         // If we haven't been initialized yet, we don't have a StatusBar/LightRevealScrim yet, so we
328         // can't perform the animation.
329         if (!initialized) {
330             return false
331         }
332 
333         // If the device isn't in a state where we can control unlocked screen off (no AOD enabled,
334         // power save, etc.) then we shouldn't try to do so.
335         if (!dozeParameters.get().canControlUnlockedScreenOff()) {
336             return false
337         }
338 
339         // If we explicitly already decided not to play the screen off animation, then never change
340         // our mind.
341         if (decidedToAnimateGoingToSleep == false) {
342             return false
343         }
344 
345         // If animations are disabled system-wide, don't play this one either.
346         if (
347             Settings.Global.getString(
348                 context.contentResolver,
349                 Settings.Global.ANIMATOR_DURATION_SCALE,
350             ) == "0"
351         ) {
352             return false
353         }
354 
355         // We only play the unlocked screen off animation if we are... unlocked.
356         if (statusBarStateControllerImpl.state != StatusBarState.SHADE) {
357             return false
358         }
359 
360         // We currently draw both the light reveal scrim, and the AOD UI, in the shade. If it's
361         // already expanded and showing notifications/QS, the animation looks really messy. For now,
362         // disable it if the notification panel is expanded.
363         if (
364             (!this::centralSurfaces.isInitialized ||
365                 panelExpansionInteractorLazy.get().isPanelExpanded) &&
366                 // Status bar might be expanded because we have started
367                 // playing the animation already
368                 !isAnimationPlaying()
369         ) {
370             return false
371         }
372 
373         // If we're not allowed to rotate the keyguard, it can only be displayed in zero-degree
374         // portrait. If we're in another orientation, disable the screen off animation so we don't
375         // animate in the keyguard AOD UI sideways or upside down.
376         if (
377             !keyguardStateController.isKeyguardScreenRotationAllowed &&
378                 context.display?.rotation != Surface.ROTATION_0
379         ) {
380             return false
381         }
382 
383         // Otherwise, good to go.
384         return true
385     }
386 
shouldDelayDisplayDozeTransitionnull387     override fun shouldDelayDisplayDozeTransition(): Boolean =
388         shouldPlayUnlockedScreenOffAnimation()
389 
390     /**
391      * Whether we're doing the light reveal animation or we're done with that and animating in the
392      * AOD UI.
393      */
394     override fun isAnimationPlaying(): Boolean {
395         return isScreenOffLightRevealAnimationPlaying()
396     }
397 
shouldAnimateInKeyguardnull398     override fun shouldAnimateInKeyguard(): Boolean = shouldAnimateInKeyguard
399 
400     override fun shouldHideScrimOnWakeUp(): Boolean = isScreenOffLightRevealAnimationPlaying()
401 
402     override fun overrideNotificationsDozeAmount(): Boolean =
403         shouldPlayUnlockedScreenOffAnimation() && isAnimationPlaying()
404 
405     override fun shouldShowAodIconsWhenShade(): Boolean = isAnimationPlaying()
406 
407     override fun shouldAnimateAodIcons(): Boolean = shouldPlayUnlockedScreenOffAnimation()
408 
409     override fun shouldPlayAnimation(): Boolean = shouldPlayUnlockedScreenOffAnimation()
410 
411     /**
412      * Whether the light reveal animation is playing. The second part of the screen off animation,
413      * where AOD animates in, might still be playing if this returns false.
414      *
415      * Note: This only refers to the specific light reveal animation that is playing during lock
416      * therefore LightRevealScrimInteractor.isAnimating is not the desired response.
417      */
418     fun isScreenOffLightRevealAnimationPlaying(): Boolean {
419         return lightRevealAnimationPlaying
420     }
421 }
422