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