1 /*
<lambda>null2  * Copyright (C) 2022 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 package com.android.systemui.keyguard.data.repository
17 
18 import android.animation.Animator
19 import android.animation.AnimatorListenerAdapter
20 import android.animation.ValueAnimator
21 import android.animation.ValueAnimator.AnimatorUpdateListener
22 import android.annotation.FloatRange
23 import android.annotation.SuppressLint
24 import android.os.Trace
25 import android.util.Log
26 import com.android.app.tracing.coroutines.withContext
27 import com.android.systemui.dagger.SysUISingleton
28 import com.android.systemui.dagger.qualifiers.Main
29 import com.android.systemui.keyguard.shared.model.KeyguardState
30 import com.android.systemui.keyguard.shared.model.TransitionInfo
31 import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
32 import com.android.systemui.keyguard.shared.model.TransitionState
33 import com.android.systemui.keyguard.shared.model.TransitionStep
34 import java.util.UUID
35 import javax.inject.Inject
36 import kotlinx.coroutines.CoroutineDispatcher
37 import kotlinx.coroutines.channels.BufferOverflow
38 import kotlinx.coroutines.delay
39 import kotlinx.coroutines.flow.Flow
40 import kotlinx.coroutines.flow.MutableSharedFlow
41 import kotlinx.coroutines.flow.MutableStateFlow
42 import kotlinx.coroutines.flow.StateFlow
43 import kotlinx.coroutines.flow.asSharedFlow
44 import kotlinx.coroutines.flow.asStateFlow
45 import kotlinx.coroutines.flow.distinctUntilChanged
46 import kotlinx.coroutines.flow.filter
47 import kotlinx.coroutines.sync.Mutex
48 
49 /**
50  * The source of truth for all keyguard transitions.
51  *
52  * While the keyguard component is visible, it can undergo a number of transitions between different
53  * UI screens, such as AOD (Always-on Display), Bouncer, and others mentioned in [KeyguardState].
54  * These UI elements should listen to events emitted by [transitions], to ensure a centrally
55  * coordinated experience.
56  *
57  * To create or modify logic that controls when and how transitions get created, look at
58  * [TransitionInteractor]. These interactors will call [startTransition] and [updateTransition] on
59  * this repository.
60  *
61  * To print all transitions to logcat to help with debugging, run this command:
62  * ```
63  * adb shell cmd statusbar echo -b KeyguardLog:VERBOSE
64  * ```
65  *
66  * This will print all keyguard transitions to logcat with the KeyguardTransitionAuditLogger tag.
67  */
68 interface KeyguardTransitionRepository {
69     /**
70      * All events regarding transitions, as they start, run, and complete. [TransitionStep#value] is
71      * a float between [0, 1] representing progress towards completion. If this is a user driven
72      * transition, that value may not be a monotonic progression, as the user may swipe in any
73      * direction.
74      */
75     val transitions: Flow<TransitionStep>
76 
77     /** The [TransitionInfo] of the most recent call to [startTransition]. */
78     val currentTransitionInfoInternal: StateFlow<TransitionInfo>
79 
80     /**
81      * Interactors that require information about changes between [KeyguardState]s will call this to
82      * register themselves for flowable [TransitionStep]s when that transition occurs.
83      */
84     fun transition(from: KeyguardState, to: KeyguardState): Flow<TransitionStep> {
85         return transitions.filter { step -> step.from == from && step.to == to }
86     }
87 
88     /**
89      * Begin a transition from one state to another. Transitions are interruptible, and will issue a
90      * [TransitionStep] with state = [TransitionState.CANCELED] before beginning the next one.
91      */
92     suspend fun startTransition(info: TransitionInfo): UUID?
93 
94     /**
95      * Emits STARTED and FINISHED transition steps to the given state. This is used during boot to
96      * seed the repository with the appropriate initial state.
97      */
98     suspend fun emitInitialStepsFromOff(to: KeyguardState)
99 
100     /**
101      * Allows manual control of a transition. When calling [startTransition], the consumer must pass
102      * in a null animator. In return, it will get a unique [UUID] that will be validated to allow
103      * further updates.
104      *
105      * When the transition is over, TransitionState.FINISHED must be passed into the [state]
106      * parameter.
107      */
108     fun updateTransition(
109         transitionId: UUID,
110         @FloatRange(from = 0.0, to = 1.0) value: Float,
111         state: TransitionState
112     )
113 }
114 
115 @SysUISingleton
116 class KeyguardTransitionRepositoryImpl
117 @Inject
118 constructor(
119     @Main val mainDispatcher: CoroutineDispatcher,
120 ) : KeyguardTransitionRepository {
121     /**
122      * Each transition between [KeyguardState]s will have an associated Flow. In order to collect
123      * these events, clients should call [transition].
124      */
125     @SuppressLint("SharedFlowCreation")
126     private val _transitions =
127         MutableSharedFlow<TransitionStep>(
128             replay = 2,
129             extraBufferCapacity = 20,
130             onBufferOverflow = BufferOverflow.DROP_OLDEST,
131         )
132     override val transitions = _transitions.asSharedFlow().distinctUntilChanged()
133     private var lastStep: TransitionStep = TransitionStep()
134     private var lastAnimator: ValueAnimator? = null
135 
136     private val _currentTransitionMutex = Mutex()
137     private val _currentTransitionInfo: MutableStateFlow<TransitionInfo> =
138         MutableStateFlow(
139             TransitionInfo(
140                 ownerName = "",
141                 from = KeyguardState.OFF,
142                 to = KeyguardState.OFF,
143                 animator = null
144             )
145         )
146     override var currentTransitionInfoInternal = _currentTransitionInfo.asStateFlow()
147 
148     /*
149      * When manual control of the transition is requested, a unique [UUID] is used as the handle
150      * to permit calls to [updateTransition]
151      */
152     private var updateTransitionId: UUID? = null
153 
154     // Only used in a test environment
155     var forceDelayForRaceConditionTest = false
156 
157     init {
158         // Start with a FINISHED transition in OFF. KeyguardBootInteractor will transition from OFF
159         // to either GONE or LOCKSCREEN once we're booted up and can determine which state we should
160         // start in.
161         emitTransition(
162             TransitionStep(
163                 KeyguardState.OFF,
164                 KeyguardState.OFF,
165                 1f,
166                 TransitionState.FINISHED,
167             )
168         )
169     }
170 
startTransitionnull171     override suspend fun startTransition(info: TransitionInfo): UUID? {
172         _currentTransitionInfo.value = info
173         Log.d(TAG, "(Internal) Setting current transition info: $info")
174 
175         // There is no fairness guarantee with 'withContext', which means that transitions could
176         // be processed out of order. Use a Mutex to guarantee ordering.
177         _currentTransitionMutex.lock()
178 
179         // Only used in a test environment
180         if (forceDelayForRaceConditionTest) {
181             delay(50L)
182         }
183 
184         // Animators must be started on the main thread.
185         return withContext("$TAG#startTransition", mainDispatcher) {
186             _currentTransitionMutex.unlock()
187 
188             if (lastStep.from == info.from && lastStep.to == info.to) {
189                 Log.i(TAG, "Duplicate call to start the transition, rejecting: $info")
190                 return@withContext null
191             }
192             val startingValue =
193                 if (lastStep.transitionState != TransitionState.FINISHED) {
194                     Log.i(TAG, "Transition still active: $lastStep, canceling")
195                     when (info.modeOnCanceled) {
196                         TransitionModeOnCanceled.LAST_VALUE -> lastStep.value
197                         TransitionModeOnCanceled.RESET -> 0f
198                         TransitionModeOnCanceled.REVERSE -> 1f - lastStep.value
199                     }
200                 } else {
201                     0f
202                 }
203 
204             lastAnimator?.cancel()
205             lastAnimator = info.animator
206 
207             // Cancel any existing manual transitions
208             updateTransitionId?.let { uuid ->
209                 updateTransition(uuid, lastStep.value, TransitionState.CANCELED)
210             }
211 
212             info.animator?.let { animator ->
213                 // An animator was provided, so use it to run the transition
214                 animator.setFloatValues(startingValue, 1f)
215                 animator.duration = ((1f - startingValue) * animator.duration).toLong()
216                 val updateListener = AnimatorUpdateListener { animation ->
217                     emitTransition(
218                         TransitionStep(
219                             info,
220                             (animation.animatedValue as Float),
221                             TransitionState.RUNNING
222                         )
223                     )
224                 }
225 
226                 val adapter =
227                     object : AnimatorListenerAdapter() {
228                         override fun onAnimationStart(animation: Animator) {
229                             emitTransition(
230                                 TransitionStep(info, startingValue, TransitionState.STARTED)
231                             )
232                         }
233 
234                         override fun onAnimationCancel(animation: Animator) {
235                             endAnimation(lastStep.value, TransitionState.CANCELED)
236                         }
237 
238                         override fun onAnimationEnd(animation: Animator) {
239                             endAnimation(1f, TransitionState.FINISHED)
240                         }
241 
242                         private fun endAnimation(value: Float, state: TransitionState) {
243                             emitTransition(TransitionStep(info, value, state))
244                             animator.removeListener(this)
245                             animator.removeUpdateListener(updateListener)
246                             lastAnimator = null
247                         }
248                     }
249                 animator.addListener(adapter)
250                 animator.addUpdateListener(updateListener)
251                 animator.start()
252                 return@withContext null
253             }
254                 ?: run {
255                     emitTransition(TransitionStep(info, startingValue, TransitionState.STARTED))
256 
257                     // No animator, so it's manual. Provide a mechanism to callback
258                     updateTransitionId = UUID.randomUUID()
259                     return@withContext updateTransitionId
260                 }
261         }
262     }
263 
updateTransitionnull264     override fun updateTransition(
265         transitionId: UUID,
266         @FloatRange(from = 0.0, to = 1.0) value: Float,
267         state: TransitionState
268     ) {
269         if (updateTransitionId != transitionId) {
270             Log.wtf(TAG, "Attempting to update with old/invalid transitionId: $transitionId")
271             return
272         }
273 
274         if (state == TransitionState.FINISHED || state == TransitionState.CANCELED) {
275             updateTransitionId = null
276         }
277 
278         val nextStep = lastStep.copy(value = value, transitionState = state)
279         emitTransition(nextStep, isManual = true)
280     }
281 
emitTransitionnull282     private fun emitTransition(nextStep: TransitionStep, isManual: Boolean = false) {
283         logAndTrace(nextStep, isManual)
284         _transitions.tryEmit(nextStep)
285         lastStep = nextStep
286     }
287 
emitInitialStepsFromOffnull288     override suspend fun emitInitialStepsFromOff(to: KeyguardState) {
289         _currentTransitionInfo.value =
290             TransitionInfo(
291                 ownerName = "KeyguardTransitionRepository(boot)",
292                 from = KeyguardState.OFF,
293                 to = to,
294                 animator = null
295             )
296 
297         emitTransition(
298             TransitionStep(
299                 KeyguardState.OFF,
300                 to,
301                 0f,
302                 TransitionState.STARTED,
303                 ownerName = "KeyguardTransitionRepository(boot)",
304             )
305         )
306 
307         emitTransition(
308             TransitionStep(
309                 KeyguardState.OFF,
310                 to,
311                 1f,
312                 TransitionState.FINISHED,
313                 ownerName = "KeyguardTransitionRepository(boot)",
314             ),
315         )
316     }
317 
logAndTracenull318     private fun logAndTrace(step: TransitionStep, isManual: Boolean) {
319         if (step.transitionState == TransitionState.RUNNING) {
320             return
321         }
322         val manualStr = if (isManual) " (manual)" else ""
323         val traceName = "Transition: ${step.from} -> ${step.to}$manualStr"
324 
325         val traceCookie = traceName.hashCode()
326         when (step.transitionState) {
327             TransitionState.STARTED -> Trace.beginAsyncSection(traceName, traceCookie)
328             TransitionState.FINISHED -> Trace.endAsyncSection(traceName, traceCookie)
329             TransitionState.CANCELED -> Trace.endAsyncSection(traceName, traceCookie)
330             else -> {}
331         }
332 
333         Log.i(TAG, "${step.transitionState.name} transition: $step$manualStr")
334     }
335 
336     companion object {
337         private const val TAG = "KeyguardTransitionRepository"
338     }
339 }
340