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