package com.android.systemui.statusbar.phone import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.content.Context import android.database.ContentObserver import android.os.Handler import android.os.PowerManager import android.provider.Settings import android.view.Display import android.view.Surface import android.view.View import android.view.WindowManager.fixScale import com.android.app.animation.Interpolators import com.android.app.tracing.namedRunnable import com.android.internal.jank.InteractionJankMonitor import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD import com.android.systemui.DejankUtils import com.android.systemui.Flags.lightRevealMigration import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.shade.ShadeViewController import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import com.android.systemui.statusbar.CircleReveal import com.android.systemui.statusbar.LightRevealScrim import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.StatusBarStateControllerImpl import com.android.systemui.statusbar.notification.AnimatableProperty import com.android.systemui.statusbar.notification.PropertyAnimator import com.android.systemui.statusbar.notification.stack.AnimationProperties import com.android.systemui.statusbar.notification.stack.StackStateAnimator import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.settings.GlobalSettings import dagger.Lazy import javax.inject.Inject /** * When to show the keyguard (AOD) view. This should be once the light reveal scrim is barely * visible, because the transition to KEYGUARD causes brief jank. */ private const val ANIMATE_IN_KEYGUARD_DELAY = 600L /** Duration for the light reveal portion of the animation. */ private const val LIGHT_REVEAL_ANIMATION_DURATION = 500L /** * Controller for the unlocked screen off animation, which runs when the device is going to sleep * and we're unlocked. * * This animation uses a [LightRevealScrim] that lives in the status bar to hide the screen contents * and then animates in the AOD UI. */ @SysUISingleton class UnlockedScreenOffAnimationController @Inject constructor( private val context: Context, private val wakefulnessLifecycle: WakefulnessLifecycle, private val statusBarStateControllerImpl: StatusBarStateControllerImpl, private val keyguardViewMediatorLazy: Lazy, private val keyguardStateController: KeyguardStateController, private val dozeParameters: Lazy, private val globalSettings: GlobalSettings, private val notifShadeWindowControllerLazy: Lazy, private val interactionJankMonitor: InteractionJankMonitor, private val powerManager: PowerManager, private val shadeLockscreenInteractorLazy: Lazy, private val panelExpansionInteractorLazy: Lazy, private val handler: Handler = Handler(), ) : WakefulnessLifecycle.Observer, ScreenOffAnimation { private lateinit var centralSurfaces: CentralSurfaces /** * Whether or not [initialize] has been called to provide us with the StatusBar, * NotificationPanelViewController, and LightRevealSrim so that we can run the unlocked screen * off animation. */ private var initialized = false private lateinit var lightRevealScrim: LightRevealScrim private var animatorDurationScale = 1f private var shouldAnimateInKeyguard = false private var lightRevealAnimationPlaying = false private var aodUiAnimationPlaying = false /** * The result of our decision whether to play the screen off animation in * [onStartedGoingToSleep], or null if we haven't made that decision yet or aren't going to * sleep. */ private var decidedToAnimateGoingToSleep: Boolean? = null private val lightRevealAnimator = ValueAnimator.ofFloat(1f, 0f).apply { duration = LIGHT_REVEAL_ANIMATION_DURATION interpolator = Interpolators.LINEAR addUpdateListener { if (lightRevealMigration()) return@addUpdateListener if (lightRevealScrim.revealEffect !is CircleReveal) { lightRevealScrim.revealAmount = it.animatedValue as Float } if ( lightRevealScrim.isScrimAlmostOccludes && interactionJankMonitor.isInstrumenting(CUJ_SCREEN_OFF) ) { // ends the instrument when the scrim almost occludes the screen. // because the following janky frames might not be perceptible. interactionJankMonitor.end(CUJ_SCREEN_OFF) } } addListener( object : AnimatorListenerAdapter() { override fun onAnimationCancel(animation: Animator) { if (lightRevealMigration()) return if (lightRevealScrim.revealEffect !is CircleReveal) { lightRevealScrim.revealAmount = 1f } } override fun onAnimationEnd(animation: Animator) { lightRevealAnimationPlaying = false interactionJankMonitor.end(CUJ_SCREEN_OFF) } override fun onAnimationStart(animation: Animator) { interactionJankMonitor.begin( notifShadeWindowControllerLazy.get().windowRootView, CUJ_SCREEN_OFF ) } } ) } // FrameCallback used to delay starting the light reveal animation until the next frame private val startLightRevealCallback = namedRunnable("startLightReveal") { lightRevealAnimationPlaying = true lightRevealAnimator.start() } private val animatorDurationScaleObserver = object : ContentObserver(null) { override fun onChange(selfChange: Boolean) { updateAnimatorDurationScale() } } override fun initialize( centralSurfaces: CentralSurfaces, shadeViewController: ShadeViewController, lightRevealScrim: LightRevealScrim ) { this.initialized = true this.lightRevealScrim = lightRevealScrim this.centralSurfaces = centralSurfaces updateAnimatorDurationScale() globalSettings.registerContentObserverSync( Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE), /* notify for descendants */ false, animatorDurationScaleObserver ) wakefulnessLifecycle.addObserver(this) } fun updateAnimatorDurationScale() { animatorDurationScale = fixScale(globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f)) } override fun shouldDelayKeyguardShow(): Boolean = shouldPlayAnimation() override fun isKeyguardShowDelayed(): Boolean = isAnimationPlaying() /** * Animates in the provided keyguard view, ending in the same position that it will be in on * AOD. */ override fun animateInKeyguard(keyguardView: View, after: Runnable) { shouldAnimateInKeyguard = false keyguardView.alpha = 0f keyguardView.visibility = View.VISIBLE val currentY = keyguardView.y // Move the keyguard up by 10% so we can animate it back down. keyguardView.y = currentY - keyguardView.height * 0.1f val duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP // We animate the Y properly separately using the PropertyAnimator, as the panel // view also needs to update the end position. PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.Y) PropertyAnimator.setProperty( keyguardView, AnimatableProperty.Y, currentY, AnimationProperties().setDuration(duration.toLong()), true /* animate */ ) // Cancel any existing CUJs before starting the animation interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD) PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.ALPHA) PropertyAnimator.setProperty( keyguardView, AnimatableProperty.ALPHA, 1f, AnimationProperties() .setDelay(0) .setDuration(duration.toLong()) .setAnimationEndAction { aodUiAnimationPlaying = false // Lock the keyguard if it was waiting for the screen off animation to end. keyguardViewMediatorLazy.get().maybeHandlePendingLock() // Tell the CentralSurfaces to become keyguard for real - we waited on that // since it is slow and would have caused the animation to jank. centralSurfaces.updateIsKeyguard() // Run the callback given to us by the KeyguardVisibilityHelper. after.run() // Done going to sleep, reset this flag. decidedToAnimateGoingToSleep = null interactionJankMonitor.end(CUJ_SCREEN_OFF_SHOW_AOD) } .setAnimationCancelAction { // If we're cancelled, reset state flags/listeners. The end action above // will not be called, which is what we want since that will finish the // screen off animation and show the lockscreen, which we don't want if we // were cancelled. aodUiAnimationPlaying = false decidedToAnimateGoingToSleep = null interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD) } .setCustomInterpolator(View.ALPHA, Interpolators.FAST_OUT_SLOW_IN), true /* animate */ ) val builder = InteractionJankMonitor.Configuration.Builder.withView( InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD, checkNotNull(notifShadeWindowControllerLazy.get().windowRootView) ) .setTag(statusBarStateControllerImpl.getClockId()) interactionJankMonitor.begin(builder) } override fun onStartedWakingUp() { // Waking up, so reset this flag. decidedToAnimateGoingToSleep = null shouldAnimateInKeyguard = false DejankUtils.removeCallbacks(startLightRevealCallback) lightRevealAnimator.cancel() handler.removeCallbacksAndMessages(null) } override fun onFinishedWakingUp() { // Set this to false in onFinishedWakingUp rather than onStartedWakingUp so that other // observers (such as CentralSurfaces) can ask us whether we were playing the screen off // animation and reset accordingly. aodUiAnimationPlaying = false // If we can't control the screen off animation, we shouldn't mess with the // CentralSurfaces's keyguard state unnecessarily. if (dozeParameters.get().canControlUnlockedScreenOff()) { // Make sure the status bar is in the correct keyguard state, forcing it if necessary. // This is required if the screen off animation is cancelled, since it might be // incorrectly left in the KEYGUARD or SHADE states depending on when it was cancelled // and whether 'lock instantly' is enabled. We need to force it so that the state is set // even if we're going from SHADE to SHADE or KEYGUARD to KEYGUARD, since we might have // changed parts of the UI (such as showing AOD in the shade) without actually changing // the StatusBarState. This ensures that the UI definitely reflects the desired state. centralSurfaces.updateIsKeyguard(true /* forceStateChange */) } } override fun startAnimation(): Boolean { if (shouldPlayUnlockedScreenOffAnimation()) { decidedToAnimateGoingToSleep = true shouldAnimateInKeyguard = true // Start the animation on the next frame. startAnimation() is called after // PhoneWindowManager makes a binder call to System UI on // IKeyguardService#onStartedGoingToSleep(). By the time we get here, system_server is // already busy making changes to PowerManager and DisplayManager. This increases our // chance of missing the first frame, so to mitigate this we should start the animation // on the next frame. DejankUtils.postAfterTraversal(startLightRevealCallback) handler.postDelayed( { // Only run this callback if the device is sleeping (not interactive). This // callback // is removed in onStartedWakingUp, but since that event is asynchronously // dispatched, a race condition could make it possible for this callback to be // run // as the device is waking up. That results in the AOD UI being shown while we // wake // up, with unpredictable consequences. if ( !powerManager.isInteractive(Display.DEFAULT_DISPLAY) && shouldAnimateInKeyguard ) { if (!MigrateClocksToBlueprint.isEnabled) { // Tracking this state should no longer be relevant, as the // isInteractive // check covers it aodUiAnimationPlaying = true } // Show AOD. That'll cause the KeyguardVisibilityHelper to call // #animateInKeyguard. shadeLockscreenInteractorLazy.get().showAodUi() } }, (ANIMATE_IN_KEYGUARD_DELAY * animatorDurationScale).toLong() ) return true } else { decidedToAnimateGoingToSleep = false return false } } /** * Whether we want to play the screen off animation when the phone starts going to sleep, based * on the current state of the device. */ fun shouldPlayUnlockedScreenOffAnimation(): Boolean { // If we haven't been initialized yet, we don't have a StatusBar/LightRevealScrim yet, so we // can't perform the animation. if (!initialized) { return false } // If the device isn't in a state where we can control unlocked screen off (no AOD enabled, // power save, etc.) then we shouldn't try to do so. if (!dozeParameters.get().canControlUnlockedScreenOff()) { return false } // If we explicitly already decided not to play the screen off animation, then never change // our mind. if (decidedToAnimateGoingToSleep == false) { return false } // If animations are disabled system-wide, don't play this one either. if ( Settings.Global.getString( context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE ) == "0" ) { return false } // We only play the unlocked screen off animation if we are... unlocked. if (statusBarStateControllerImpl.state != StatusBarState.SHADE) { return false } // We currently draw both the light reveal scrim, and the AOD UI, in the shade. If it's // already expanded and showing notifications/QS, the animation looks really messy. For now, // disable it if the notification panel is expanded. if ( (!this::centralSurfaces.isInitialized || panelExpansionInteractorLazy.get().isPanelExpanded) && // Status bar might be expanded because we have started // playing the animation already !isAnimationPlaying() ) { return false } // If we're not allowed to rotate the keyguard, it can only be displayed in zero-degree // portrait. If we're in another orientation, disable the screen off animation so we don't // animate in the keyguard AOD UI sideways or upside down. if ( !keyguardStateController.isKeyguardScreenRotationAllowed && context.display?.rotation != Surface.ROTATION_0 ) { return false } // Otherwise, good to go. return true } override fun shouldDelayDisplayDozeTransition(): Boolean = shouldPlayUnlockedScreenOffAnimation() /** * Whether we're doing the light reveal animation or we're done with that and animating in the * AOD UI. */ override fun isAnimationPlaying(): Boolean { return isScreenOffLightRevealAnimationPlaying() || aodUiAnimationPlaying } override fun shouldAnimateInKeyguard(): Boolean = shouldAnimateInKeyguard override fun shouldHideScrimOnWakeUp(): Boolean = isScreenOffLightRevealAnimationPlaying() override fun overrideNotificationsDozeAmount(): Boolean = shouldPlayUnlockedScreenOffAnimation() && isAnimationPlaying() override fun shouldShowAodIconsWhenShade(): Boolean = isAnimationPlaying() override fun shouldAnimateAodIcons(): Boolean = shouldPlayUnlockedScreenOffAnimation() override fun shouldPlayAnimation(): Boolean = shouldPlayUnlockedScreenOffAnimation() /** * Whether the light reveal animation is playing. The second part of the screen off animation, * where AOD animates in, might still be playing if this returns false. * * Note: This only refers to the specific light reveal animation that is playing during lock * therefore LightRevealScrimInteractor.isAnimating is not the desired response. */ fun isScreenOffLightRevealAnimationPlaying(): Boolean { return lightRevealAnimationPlaying } }