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