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.Surface 12 import android.view.View 13 import com.android.systemui.animation.Interpolators 14 import com.android.systemui.dagger.SysUISingleton 15 import com.android.systemui.keyguard.KeyguardViewMediator 16 import com.android.systemui.keyguard.WakefulnessLifecycle 17 import com.android.systemui.statusbar.LightRevealScrim 18 import com.android.systemui.statusbar.StatusBarState 19 import com.android.systemui.statusbar.StatusBarStateControllerImpl 20 import com.android.systemui.statusbar.notification.AnimatableProperty 21 import com.android.systemui.statusbar.notification.PropertyAnimator 22 import com.android.systemui.statusbar.notification.stack.AnimationProperties 23 import com.android.systemui.statusbar.notification.stack.StackStateAnimator 24 import com.android.systemui.statusbar.policy.KeyguardStateController 25 import com.android.systemui.util.settings.GlobalSettings 26 import javax.inject.Inject 27 28 /** 29 * When to show the keyguard (AOD) view. This should be once the light reveal scrim is barely 30 * visible, because the transition to KEYGUARD causes brief jank. 31 */ 32 private const val ANIMATE_IN_KEYGUARD_DELAY = 600L 33 34 /** 35 * Duration for the light reveal portion of the animation. 36 */ 37 private const val LIGHT_REVEAL_ANIMATION_DURATION = 750L 38 39 /** 40 * Controller for the unlocked screen off animation, which runs when the device is going to sleep 41 * and we're unlocked. 42 * 43 * This animation uses a [LightRevealScrim] that lives in the status bar to hide the screen contents 44 * and then animates in the AOD UI. 45 */ 46 @SysUISingleton 47 class UnlockedScreenOffAnimationController @Inject constructor( 48 private val context: Context, 49 private val wakefulnessLifecycle: WakefulnessLifecycle, 50 private val statusBarStateControllerImpl: StatusBarStateControllerImpl, 51 private val keyguardViewMediatorLazy: dagger.Lazy<KeyguardViewMediator>, 52 private val keyguardStateController: KeyguardStateController, 53 private val dozeParameters: dagger.Lazy<DozeParameters>, 54 private val globalSettings: GlobalSettings, 55 private val powerManager: PowerManager, 56 private val handler: Handler = Handler() 57 ) : WakefulnessLifecycle.Observer { 58 59 private lateinit var statusBar: StatusBar 60 private lateinit var lightRevealScrim: LightRevealScrim 61 62 private var animatorDurationScale = 1f 63 private var shouldAnimateInKeyguard = false 64 private var lightRevealAnimationPlaying = false 65 private var aodUiAnimationPlaying = false 66 private var callbacks = HashSet<Callback>() 67 68 /** 69 * The result of our decision whether to play the screen off animation in 70 * [onStartedGoingToSleep], or null if we haven't made that decision yet or aren't going to 71 * sleep. 72 */ 73 private var decidedToAnimateGoingToSleep: Boolean? = null 74 75 private val lightRevealAnimator = ValueAnimator.ofFloat(1f, 0f).apply { 76 duration = LIGHT_REVEAL_ANIMATION_DURATION 77 interpolator = Interpolators.LINEAR 78 addUpdateListener { 79 lightRevealScrim.revealAmount = it.animatedValue as Float 80 sendUnlockedScreenOffProgressUpdate( 81 1f - (it.animatedFraction as Float), 82 1f - (it.animatedValue as Float)) 83 } 84 addListener(object : AnimatorListenerAdapter() { 85 override fun onAnimationCancel(animation: Animator?) { 86 lightRevealScrim.revealAmount = 1f 87 lightRevealAnimationPlaying = false 88 sendUnlockedScreenOffProgressUpdate(0f, 0f) 89 } 90 91 override fun onAnimationEnd(animation: Animator?) { 92 lightRevealAnimationPlaying = false 93 } 94 }) 95 } 96 97 val animatorDurationScaleObserver = object : ContentObserver(null) { 98 override fun onChange(selfChange: Boolean) { 99 updateAnimatorDurationScale() 100 } 101 } 102 103 fun initialize( 104 statusBar: StatusBar, 105 lightRevealScrim: LightRevealScrim 106 ) { 107 this.lightRevealScrim = lightRevealScrim 108 this.statusBar = statusBar 109 110 updateAnimatorDurationScale() 111 globalSettings.registerContentObserver( 112 Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE), 113 /* notify for descendants */ false, 114 animatorDurationScaleObserver) 115 wakefulnessLifecycle.addObserver(this) 116 } 117 118 fun updateAnimatorDurationScale() { 119 animatorDurationScale = 120 globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f) 121 } 122 123 /** 124 * Animates in the provided keyguard view, ending in the same position that it will be in on 125 * AOD. 126 */ 127 fun animateInKeyguard(keyguardView: View, after: Runnable) { 128 shouldAnimateInKeyguard = false 129 keyguardView.alpha = 0f 130 keyguardView.visibility = View.VISIBLE 131 132 val currentY = keyguardView.y 133 134 // Move the keyguard up by 10% so we can animate it back down. 135 keyguardView.y = currentY - keyguardView.height * 0.1f 136 137 val duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP 138 139 // We animate the Y properly separately using the PropertyAnimator, as the panel 140 // view also needs to update the end position. 141 PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.Y) 142 PropertyAnimator.setProperty<View>(keyguardView, AnimatableProperty.Y, currentY, 143 AnimationProperties().setDuration(duration.toLong()), 144 true /* animate */) 145 146 keyguardView.animate() 147 .setDuration(duration.toLong()) 148 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) 149 .alpha(1f) 150 .setListener(object : AnimatorListenerAdapter() { 151 override fun onAnimationEnd(animation: Animator?) { 152 aodUiAnimationPlaying = false 153 154 // Lock the keyguard if it was waiting for the screen off animation to end. 155 keyguardViewMediatorLazy.get().maybeHandlePendingLock() 156 157 // Tell the StatusBar to become keyguard for real - we waited on that since 158 // it is slow and would have caused the animation to jank. 159 statusBar.updateIsKeyguard() 160 161 // Run the callback given to us by the KeyguardVisibilityHelper. 162 after.run() 163 164 // Done going to sleep, reset this flag. 165 decidedToAnimateGoingToSleep = null 166 // We need to unset the listener. These are persistent for future animators 167 keyguardView.animate().setListener(null) 168 } 169 }) 170 .start() 171 } 172 173 override fun onStartedWakingUp() { 174 // Waking up, so reset this flag. 175 decidedToAnimateGoingToSleep = null 176 177 shouldAnimateInKeyguard = false 178 lightRevealAnimator.cancel() 179 handler.removeCallbacksAndMessages(null) 180 } 181 182 override fun onFinishedWakingUp() { 183 // Set this to false in onFinishedWakingUp rather than onStartedWakingUp so that other 184 // observers (such as StatusBar) can ask us whether we were playing the screen off animation 185 // and reset accordingly. 186 aodUiAnimationPlaying = false 187 188 // If we can't control the screen off animation, we shouldn't mess with the StatusBar's 189 // keyguard state unnecessarily. 190 if (dozeParameters.get().canControlUnlockedScreenOff()) { 191 // Make sure the status bar is in the correct keyguard state, forcing it if necessary. 192 // This is required if the screen off animation is cancelled, since it might be 193 // incorrectly left in the KEYGUARD or SHADE states depending on when it was cancelled 194 // and whether 'lock instantly' is enabled. We need to force it so that the state is set 195 // even if we're going from SHADE to SHADE or KEYGUARD to KEYGUARD, since we might have 196 // changed parts of the UI (such as showing AOD in the shade) without actually changing 197 // the StatusBarState. This ensures that the UI definitely reflects the desired state. 198 statusBar.updateIsKeyguard(true /* force */) 199 } 200 } 201 202 override fun onStartedGoingToSleep() { 203 if (dozeParameters.get().shouldControlUnlockedScreenOff()) { 204 decidedToAnimateGoingToSleep = true 205 206 shouldAnimateInKeyguard = true 207 lightRevealAnimationPlaying = true 208 lightRevealAnimator.start() 209 handler.postDelayed({ 210 // Only run this callback if the device is sleeping (not interactive). This callback 211 // is removed in onStartedWakingUp, but since that event is asynchronously 212 // dispatched, a race condition could make it possible for this callback to be run 213 // as the device is waking up. That results in the AOD UI being shown while we wake 214 // up, with unpredictable consequences. 215 if (!powerManager.isInteractive) { 216 aodUiAnimationPlaying = true 217 218 // Show AOD. That'll cause the KeyguardVisibilityHelper to call 219 // #animateInKeyguard. 220 statusBar.notificationPanelViewController.showAodUi() 221 } 222 }, (ANIMATE_IN_KEYGUARD_DELAY * animatorDurationScale).toLong()) 223 } else { 224 decidedToAnimateGoingToSleep = false 225 } 226 } 227 228 /** 229 * Whether we want to play the screen off animation when the phone starts going to sleep, based 230 * on the current state of the device. 231 */ 232 fun shouldPlayUnlockedScreenOffAnimation(): Boolean { 233 // If we explicitly already decided not to play the screen off animation, then never change 234 // our mind. 235 if (decidedToAnimateGoingToSleep == false) { 236 return false 237 } 238 239 // If animations are disabled system-wide, don't play this one either. 240 if (Settings.Global.getString( 241 context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE) == "0") { 242 return false 243 } 244 245 // We only play the unlocked screen off animation if we are... unlocked. 246 if (statusBarStateControllerImpl.state != StatusBarState.SHADE) { 247 return false 248 } 249 250 // We currently draw both the light reveal scrim, and the AOD UI, in the shade. If it's 251 // already expanded and showing notifications/QS, the animation looks really messy. For now, 252 // disable it if the notification panel is not fully collapsed. 253 if (!this::statusBar.isInitialized || 254 !statusBar.notificationPanelViewController.isFullyCollapsed) { 255 return false 256 } 257 258 // If we're not allowed to rotate the keyguard, it can only be displayed in zero-degree 259 // portrait. If we're in another orientation, disable the screen off animation so we don't 260 // animate in the keyguard AOD UI sideways or upside down. 261 if (!keyguardStateController.isKeyguardScreenRotationAllowed && 262 context.display.rotation != Surface.ROTATION_0) { 263 return false 264 } 265 266 // Otherwise, good to go. 267 return true 268 } 269 270 fun addCallback(callback: Callback) { 271 callbacks.add(callback) 272 } 273 274 fun removeCallback(callback: Callback) { 275 callbacks.remove(callback) 276 } 277 278 fun sendUnlockedScreenOffProgressUpdate(linear: Float, eased: Float) { 279 callbacks.forEach { 280 it.onUnlockedScreenOffProgressUpdate(linear, eased) 281 } 282 } 283 284 /** 285 * Whether we're doing the light reveal animation or we're done with that and animating in the 286 * AOD UI. 287 */ 288 fun isScreenOffAnimationPlaying(): Boolean { 289 return lightRevealAnimationPlaying || aodUiAnimationPlaying 290 } 291 292 fun shouldAnimateInKeyguard(): Boolean { 293 return shouldAnimateInKeyguard 294 } 295 296 /** 297 * Whether the light reveal animation is playing. The second part of the screen off animation, 298 * where AOD animates in, might still be playing if this returns false. 299 */ 300 fun isScreenOffLightRevealAnimationPlaying(): Boolean { 301 return lightRevealAnimationPlaying 302 } 303 304 interface Callback { 305 fun onUnlockedScreenOffProgressUpdate(linear: Float, eased: Float) 306 } 307 } 308