1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.unfold 18 19 import android.annotation.BinderThread 20 import android.content.Context 21 import android.hardware.devicestate.DeviceStateManager 22 import android.os.PowerManager 23 import android.provider.Settings 24 import androidx.annotation.VisibleForTesting 25 import androidx.core.view.OneShotPreDrawListener 26 import androidx.lifecycle.Lifecycle 27 import androidx.lifecycle.repeatOnLifecycle 28 import com.android.internal.util.LatencyTracker 29 import com.android.systemui.dagger.qualifiers.Main 30 import com.android.systemui.keyguard.WakefulnessLifecycle 31 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor 32 import com.android.systemui.lifecycle.repeatWhenAttached 33 import com.android.systemui.shade.ShadeFoldAnimator 34 import com.android.systemui.shade.ShadeViewController 35 import com.android.systemui.statusbar.LightRevealScrim 36 import com.android.systemui.statusbar.phone.CentralSurfaces 37 import com.android.systemui.statusbar.phone.ScreenOffAnimation 38 import com.android.systemui.statusbar.policy.CallbackController 39 import com.android.systemui.unfold.FoldAodAnimationController.FoldAodAnimationStatus 40 import com.android.systemui.util.concurrency.DelayableExecutor 41 import com.android.systemui.util.settings.GlobalSettings 42 import dagger.Lazy 43 import kotlinx.coroutines.CoroutineScope 44 import kotlinx.coroutines.Job 45 import kotlinx.coroutines.launch 46 import java.util.function.Consumer 47 import javax.inject.Inject 48 49 /** 50 * Controls folding to AOD animation: when AOD is enabled and foldable device is folded we play a 51 * special AOD animation on the outer screen 52 */ 53 @SysUIUnfoldScope 54 class FoldAodAnimationController 55 @Inject 56 constructor( 57 @Main private val mainExecutor: DelayableExecutor, 58 private val context: Context, 59 private val deviceStateManager: DeviceStateManager, 60 private val wakefulnessLifecycle: WakefulnessLifecycle, 61 private val globalSettings: GlobalSettings, 62 private val latencyTracker: LatencyTracker, 63 private val keyguardInteractor: Lazy<KeyguardInteractor>, 64 ) : CallbackController<FoldAodAnimationStatus>, ScreenOffAnimation, WakefulnessLifecycle.Observer { 65 66 private lateinit var shadeViewController: ShadeViewController 67 68 private var isFolded = false 69 private var isFoldHandled = true 70 71 private var alwaysOnEnabled: Boolean = false 72 private var isDozing: Boolean = false 73 private var isScrimOpaque: Boolean = false 74 private var pendingScrimReadyCallback: Runnable? = null 75 76 private var shouldPlayAnimation = false 77 private var isAnimationPlaying = false 78 private var cancelAnimation: Runnable? = null 79 80 private val statusListeners = arrayListOf<FoldAodAnimationStatus>() 81 private val foldToAodLatencyTracker = FoldToAodLatencyTracker() 82 83 private val startAnimationRunnable = Runnable { 84 getShadeFoldAnimator().startFoldToAodAnimation( 85 /* startAction= */ { foldToAodLatencyTracker.onAnimationStarted() }, 86 /* endAction= */ { setAnimationState(playing = false) }, 87 /* cancelAction= */ { setAnimationState(playing = false) }, 88 ) 89 } 90 91 override fun initialize( 92 centralSurfaces: CentralSurfaces, 93 shadeViewController: ShadeViewController, 94 lightRevealScrim: LightRevealScrim, 95 ) { 96 this.shadeViewController = shadeViewController 97 98 deviceStateManager.registerCallback(mainExecutor, FoldListener()) 99 wakefulnessLifecycle.addObserver(this) 100 101 // TODO(b/254878364): remove this call to NPVC.getView() 102 getShadeFoldAnimator().view?.repeatWhenAttached { 103 repeatOnLifecycle(Lifecycle.State.STARTED) { listenForDozing(this) } 104 } 105 } 106 107 /** Returns true if we should run fold to AOD animation */ 108 override fun shouldPlayAnimation(): Boolean = shouldPlayAnimation 109 110 private fun shouldStartAnimation(): Boolean = 111 alwaysOnEnabled && 112 wakefulnessLifecycle.lastSleepReason == PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD && 113 globalSettings.getString(Settings.Global.ANIMATOR_DURATION_SCALE) != "0" 114 115 override fun startAnimation(): Boolean = 116 if (shouldStartAnimation()) { 117 setAnimationState(playing = true) 118 getShadeFoldAnimator().prepareFoldToAodAnimation() 119 true 120 } else { 121 setAnimationState(playing = false) 122 false 123 } 124 125 override fun onStartedWakingUp() { 126 if (isAnimationPlaying) { 127 foldToAodLatencyTracker.cancel() 128 cancelAnimation?.run() 129 getShadeFoldAnimator().cancelFoldToAodAnimation() 130 } 131 132 setAnimationState(playing = false) 133 } 134 135 private fun getShadeFoldAnimator(): ShadeFoldAnimator = 136 shadeViewController.shadeFoldAnimator 137 138 private fun setAnimationState(playing: Boolean) { 139 shouldPlayAnimation = playing 140 isAnimationPlaying = playing 141 statusListeners.forEach(FoldAodAnimationStatus::onFoldToAodAnimationChanged) 142 } 143 144 /** 145 * Called when screen starts turning on, the contents of the screen might not be visible yet. 146 * This method reports back that the animation is ready in [onReady] callback. 147 * 148 * @param onReady callback when the animation is ready 149 * @see [com.android.systemui.keyguard.KeyguardViewMediator] 150 */ 151 @BinderThread 152 fun onScreenTurningOn(onReady: Runnable) = mainExecutor.execute { 153 if (shouldPlayAnimation) { 154 // The device was not dozing and going to sleep after folding, play the animation 155 156 if (isScrimOpaque) { 157 onReady.run() 158 } else { 159 pendingScrimReadyCallback = onReady 160 } 161 } else if (isFolded && !isFoldHandled && alwaysOnEnabled && isDozing) { 162 setAnimationState(playing = true) 163 getShadeFoldAnimator().prepareFoldToAodAnimation() 164 165 // We don't need to wait for the scrim as it is already displayed 166 // but we should wait for the initial animation preparations to be drawn 167 // (setting initial alpha/translation) 168 // TODO(b/254878364): remove this call to NPVC.getView() 169 getShadeFoldAnimator().view?.let { 170 OneShotPreDrawListener.add(it, onReady) 171 } 172 } else { 173 // No animation, call ready callback immediately 174 onReady.run() 175 } 176 177 if (isFolded) { 178 // Any time the screen turns on, this state needs to be reset if the device has been 179 // folded. Reaching this line implies AOD has been shown in one way or another, 180 // if enabled 181 isFoldHandled = true 182 } 183 } 184 185 /** Called when keyguard scrim opaque changed */ 186 override fun onScrimOpaqueChanged(isOpaque: Boolean) { 187 isScrimOpaque = isOpaque 188 189 if (isOpaque) { 190 pendingScrimReadyCallback?.run() 191 pendingScrimReadyCallback = null 192 } 193 } 194 195 @BinderThread 196 fun onScreenTurnedOn() = mainExecutor.execute { 197 if (shouldPlayAnimation) { 198 cancelAnimation?.run() 199 200 // Post starting the animation to the next frame to avoid junk due to inset changes 201 cancelAnimation = mainExecutor.executeDelayed( 202 startAnimationRunnable, 203 /* delayMillis= */ 0 204 ) 205 shouldPlayAnimation = false 206 } 207 } 208 209 override fun isAnimationPlaying(): Boolean = isAnimationPlaying 210 211 override fun isKeyguardHideDelayed(): Boolean = isAnimationPlaying() 212 213 override fun shouldShowAodIconsWhenShade(): Boolean = shouldPlayAnimation() 214 215 override fun shouldAnimateAodIcons(): Boolean = !shouldPlayAnimation() 216 217 override fun shouldAnimateDozingChange(): Boolean = !shouldPlayAnimation() 218 219 override fun shouldAnimateClockChange(): Boolean = !isAnimationPlaying() 220 221 override fun shouldDelayDisplayDozeTransition(): Boolean = shouldPlayAnimation() 222 223 /** Called when AOD status is changed */ 224 override fun onAlwaysOnChanged(alwaysOn: Boolean) { 225 alwaysOnEnabled = alwaysOn 226 } 227 228 override fun addCallback(listener: FoldAodAnimationStatus) { 229 statusListeners += listener 230 } 231 232 override fun removeCallback(listener: FoldAodAnimationStatus) { 233 statusListeners.remove(listener) 234 } 235 236 @VisibleForTesting 237 internal suspend fun listenForDozing(scope: CoroutineScope): Job { 238 return scope.launch { keyguardInteractor.get().isDozing.collect { isDozing = it } } 239 } 240 241 interface FoldAodAnimationStatus { 242 fun onFoldToAodAnimationChanged() 243 } 244 245 private inner class FoldListener : 246 DeviceStateManager.FoldStateListener( 247 context, 248 Consumer { isFolded -> 249 if (!isFolded) { 250 // We are unfolded now, reset the fold handle status 251 isFoldHandled = false 252 } 253 this.isFolded = isFolded 254 if (isFolded) { 255 foldToAodLatencyTracker.onFolded() 256 } 257 } 258 ) 259 260 /** 261 * Tracks the latency of fold to AOD using [LatencyTracker]. 262 * 263 * Events that trigger start and end are: 264 * 265 * - Start: Once [DeviceStateManager] sends the folded signal [FoldToAodLatencyTracker.onFolded] 266 * is called and latency tracking starts. 267 * - End: Once the fold -> AOD animation starts, [FoldToAodLatencyTracker.onAnimationStarted] is 268 * called, and latency tracking stops. 269 */ 270 private inner class FoldToAodLatencyTracker { 271 272 /** Triggers the latency logging, if needed. */ 273 fun onFolded() { 274 if (shouldStartAnimation()) { 275 latencyTracker.onActionStart(LatencyTracker.ACTION_FOLD_TO_AOD) 276 } 277 } 278 /** 279 * Called once the Fold -> AOD animation is started. 280 * 281 * For latency tracking, this determines the end of the fold to aod action. 282 */ 283 fun onAnimationStarted() { 284 latencyTracker.onActionEnd(LatencyTracker.ACTION_FOLD_TO_AOD) 285 } 286 287 fun cancel() { 288 latencyTracker.onActionCancel(LatencyTracker.ACTION_FOLD_TO_AOD) 289 } 290 } 291 } 292