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