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