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.content.res.Configuration 8 import android.os.Handler 9 import android.view.View 10 import com.android.systemui.animation.Interpolators 11 import com.android.systemui.dagger.SysUISingleton 12 import com.android.systemui.keyguard.KeyguardViewMediator 13 import com.android.systemui.keyguard.WakefulnessLifecycle 14 import com.android.systemui.statusbar.LightRevealScrim 15 import com.android.systemui.statusbar.StatusBarState 16 import com.android.systemui.statusbar.StatusBarStateControllerImpl 17 import com.android.systemui.statusbar.notification.AnimatableProperty 18 import com.android.systemui.statusbar.notification.PropertyAnimator 19 import com.android.systemui.statusbar.notification.stack.AnimationProperties 20 import com.android.systemui.statusbar.notification.stack.StackStateAnimator 21 import com.android.systemui.statusbar.policy.KeyguardStateController 22 import javax.inject.Inject 23 24 /** 25 * When to show the keyguard (AOD) view. This should be once the light reveal scrim is barely 26 * visible, because the transition to KEYGUARD causes brief jank. 27 */ 28 private const val ANIMATE_IN_KEYGUARD_DELAY = 600L 29 30 /** 31 * Duration for the light reveal portion of the animation. 32 */ 33 private const val LIGHT_REVEAL_ANIMATION_DURATION = 750L 34 35 /** 36 * Controller for the unlocked screen off animation, which runs when the device is going to sleep 37 * and we're unlocked. 38 * 39 * This animation uses a [LightRevealScrim] that lives in the status bar to hide the screen contents 40 * and then animates in the AOD UI. 41 */ 42 @SysUISingleton 43 class UnlockedScreenOffAnimationController @Inject constructor( 44 private val context: Context, 45 private val wakefulnessLifecycle: WakefulnessLifecycle, 46 private val statusBarStateControllerImpl: StatusBarStateControllerImpl, 47 private val keyguardViewMediatorLazy: dagger.Lazy<KeyguardViewMediator>, 48 private val keyguardStateController: KeyguardStateController, 49 private val dozeParameters: dagger.Lazy<DozeParameters> 50 ) : WakefulnessLifecycle.Observer { 51 private val handler = Handler() 52 53 private lateinit var statusBar: StatusBar 54 private lateinit var lightRevealScrim: LightRevealScrim 55 56 private var lightRevealAnimationPlaying = false 57 private var aodUiAnimationPlaying = false 58 59 /** 60 * The result of our decision whether to play the screen off animation in 61 * [onStartedGoingToSleep], or null if we haven't made that decision yet or aren't going to 62 * sleep. 63 */ 64 private var decidedToAnimateGoingToSleep: Boolean? = null 65 <lambda>null66 private val lightRevealAnimator = ValueAnimator.ofFloat(1f, 0f).apply { 67 duration = LIGHT_REVEAL_ANIMATION_DURATION 68 interpolator = Interpolators.LINEAR 69 addUpdateListener { lightRevealScrim.revealAmount = it.animatedValue as Float } 70 addListener(object : AnimatorListenerAdapter() { 71 override fun onAnimationCancel(animation: Animator?) { 72 lightRevealScrim.revealAmount = 1f 73 lightRevealAnimationPlaying = false 74 } 75 76 override fun onAnimationEnd(animation: Animator?) { 77 lightRevealAnimationPlaying = false 78 } 79 }) 80 } 81 initializenull82 fun initialize( 83 statusBar: StatusBar, 84 lightRevealScrim: LightRevealScrim 85 ) { 86 this.lightRevealScrim = lightRevealScrim 87 this.statusBar = statusBar 88 89 wakefulnessLifecycle.addObserver(this) 90 } 91 92 /** 93 * Animates in the provided keyguard view, ending in the same position that it will be in on 94 * AOD. 95 */ animateInKeyguardnull96 fun animateInKeyguard(keyguardView: View, after: Runnable) { 97 keyguardView.alpha = 0f 98 keyguardView.visibility = View.VISIBLE 99 100 val currentY = keyguardView.y 101 102 // Move the keyguard up by 10% so we can animate it back down. 103 keyguardView.y = currentY - keyguardView.height * 0.1f 104 105 val duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP 106 107 // We animate the Y properly separately using the PropertyAnimator, as the panel 108 // view also needs to update the end position. 109 PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.Y) 110 PropertyAnimator.setProperty<View>(keyguardView, AnimatableProperty.Y, currentY, 111 AnimationProperties().setDuration(duration.toLong()), 112 true /* animate */) 113 114 keyguardView.animate() 115 .setDuration(duration.toLong()) 116 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) 117 .alpha(1f) 118 .withEndAction { 119 aodUiAnimationPlaying = false 120 121 // Lock the keyguard if it was waiting for the screen off animation to end. 122 keyguardViewMediatorLazy.get().maybeHandlePendingLock() 123 124 // Tell the StatusBar to become keyguard for real - we waited on that since it 125 // is slow and would have caused the animation to jank. 126 statusBar.updateIsKeyguard() 127 128 // Run the callback given to us by the KeyguardVisibilityHelper. 129 after.run() 130 131 // Done going to sleep, reset this flag. 132 decidedToAnimateGoingToSleep = null 133 } 134 .start() 135 } 136 onStartedWakingUpnull137 override fun onStartedWakingUp() { 138 // Waking up, so reset this flag. 139 decidedToAnimateGoingToSleep = null 140 141 lightRevealAnimator.cancel() 142 handler.removeCallbacksAndMessages(null) 143 } 144 onFinishedWakingUpnull145 override fun onFinishedWakingUp() { 146 // Set this to false in onFinishedWakingUp rather than onStartedWakingUp so that other 147 // observers (such as StatusBar) can ask us whether we were playing the screen off animation 148 // and reset accordingly. 149 lightRevealAnimationPlaying = false 150 aodUiAnimationPlaying = false 151 152 // If we can't control the screen off animation, we shouldn't mess with the StatusBar's 153 // keyguard state unnecessarily. 154 if (dozeParameters.get().canControlUnlockedScreenOff()) { 155 // Make sure the status bar is in the correct keyguard state, forcing it if necessary. 156 // This is required if the screen off animation is cancelled, since it might be 157 // incorrectly left in the KEYGUARD or SHADE states depending on when it was cancelled 158 // and whether 'lock instantly' is enabled. We need to force it so that the state is set 159 // even if we're going from SHADE to SHADE or KEYGUARD to KEYGUARD, since we might have 160 // changed parts of the UI (such as showing AOD in the shade) without actually changing 161 // the StatusBarState. This ensures that the UI definitely reflects the desired state. 162 statusBar.updateIsKeyguard(true /* force */) 163 } 164 } 165 onStartedGoingToSleepnull166 override fun onStartedGoingToSleep() { 167 if (dozeParameters.get().shouldControlUnlockedScreenOff()) { 168 decidedToAnimateGoingToSleep = true 169 170 lightRevealAnimationPlaying = true 171 lightRevealAnimator.start() 172 173 handler.postDelayed({ 174 aodUiAnimationPlaying = true 175 176 // Show AOD. That'll cause the KeyguardVisibilityHelper to call #animateInKeyguard. 177 statusBar.notificationPanelViewController.showAodUi() 178 }, ANIMATE_IN_KEYGUARD_DELAY) 179 } else { 180 decidedToAnimateGoingToSleep = false 181 } 182 } 183 184 /** 185 * Whether we want to play the screen off animation when the phone starts going to sleep, based 186 * on the current state of the device. 187 */ shouldPlayUnlockedScreenOffAnimationnull188 fun shouldPlayUnlockedScreenOffAnimation(): Boolean { 189 // If we explicitly already decided not to play the screen off animation, then never change 190 // our mind. 191 if (decidedToAnimateGoingToSleep == false) { 192 return false 193 } 194 195 if (!dozeParameters.get().canControlUnlockedScreenOff()) { 196 return false 197 } 198 199 // We only play the unlocked screen off animation if we are... unlocked. 200 if (statusBarStateControllerImpl.state != StatusBarState.SHADE) { 201 return false 202 } 203 204 // We currently draw both the light reveal scrim, and the AOD UI, in the shade. If it's 205 // already expanded and showing notifications/QS, the animation looks really messy. For now, 206 // disable it if the notification panel is not fully collapsed. 207 if (!this::statusBar.isInitialized || 208 !statusBar.notificationPanelViewController.isFullyCollapsed) { 209 return false 210 } 211 212 // If we're not allowed to rotate the keyguard, then only do the screen off animation if 213 // we're in portrait. Otherwise, AOD will animate in sideways, which looks weird. 214 if (!keyguardStateController.isKeyguardScreenRotationAllowed && 215 context.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) { 216 return false 217 } 218 219 // Otherwise, good to go. 220 return true 221 } 222 223 /** 224 * Whether we're doing the light reveal animation or we're done with that and animating in the 225 * AOD UI. 226 */ isScreenOffAnimationPlayingnull227 fun isScreenOffAnimationPlaying(): Boolean { 228 return lightRevealAnimationPlaying || aodUiAnimationPlaying 229 } 230 231 /** 232 * Whether the light reveal animation is playing. The second part of the screen off animation, 233 * where AOD animates in, might still be playing if this returns false. 234 */ isScreenOffLightRevealAnimationPlayingnull235 fun isScreenOffLightRevealAnimationPlaying(): Boolean { 236 return lightRevealAnimationPlaying 237 } 238 }