• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.animation.Interpolators
27 import com.android.app.tracing.coroutines.flow.traceAs
28 import com.android.app.tracing.coroutines.withContextTraced as withContext
29 import com.android.systemui.Flags.transitionRaceCondition
30 import com.android.systemui.dagger.SysUISingleton
31 import com.android.systemui.dagger.qualifiers.Main
32 import com.android.systemui.keyguard.shared.model.KeyguardState
33 import com.android.systemui.keyguard.shared.model.TransitionInfo
34 import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
35 import com.android.systemui.keyguard.shared.model.TransitionState
36 import com.android.systemui.keyguard.shared.model.TransitionStep
37 import com.android.systemui.keyguard.shared.transition.KeyguardTransitionAnimationCallback
38 import java.util.UUID
39 import javax.inject.Inject
40 import kotlinx.coroutines.CoroutineDispatcher
41 import kotlinx.coroutines.channels.BufferOverflow
42 import kotlinx.coroutines.delay
43 import kotlinx.coroutines.flow.Flow
44 import kotlinx.coroutines.flow.MutableSharedFlow
45 import kotlinx.coroutines.flow.MutableStateFlow
46 import kotlinx.coroutines.flow.StateFlow
47 import kotlinx.coroutines.flow.asStateFlow
48 import kotlinx.coroutines.flow.distinctUntilChanged
49 import kotlinx.coroutines.flow.filter
50 import kotlinx.coroutines.sync.Mutex
51 
52 /**
53  * The source of truth for all keyguard transitions.
54  *
55  * While the keyguard component is visible, it can undergo a number of transitions between different
56  * UI screens, such as AOD (Always-on Display), Bouncer, and others mentioned in [KeyguardState].
57  * These UI elements should listen to events emitted by [transitions], to ensure a centrally
58  * coordinated experience.
59  *
60  * To create or modify logic that controls when and how transitions get created, look at
61  * [TransitionInteractor]. These interactors will call [startTransition] and [updateTransition] on
62  * this repository.
63  *
64  * To print all transitions to logcat to help with debugging, run this command:
65  * ```
66  * adb shell cmd statusbar echo -b KeyguardLog:VERBOSE
67  * ```
68  *
69  * This will print all keyguard transitions to logcat with the KeyguardTransitionAuditLogger tag.
70  */
71 interface KeyguardTransitionRepository {
72     /**
73      * All events regarding transitions, as they start, run, and complete. [TransitionStep#value] is
74      * a float between [0, 1] representing progress towards completion. If this is a user driven
75      * transition, that value may not be a monotonic progression, as the user may swipe in any
76      * direction.
77      */
78     val transitions: Flow<TransitionStep>
79 
80     /** The [TransitionInfo] of the most recent call to [startTransition]. */
81     val currentTransitionInfoInternal: StateFlow<TransitionInfo>
82     /** The [TransitionInfo] of the most recent call to [startTransition]. */
83     val currentTransitionInfo: TransitionInfo
84 
85     /**
86      * Interactors that require information about changes between [KeyguardState]s will call this to
87      * register themselves for flowable [TransitionStep]s when that transition occurs.
88      */
89     fun transition(from: KeyguardState, to: KeyguardState): Flow<TransitionStep> {
90         return transitions.filter { step -> step.from == from && step.to == to }
91     }
92 
93     /**
94      * Begin a transition from one state to another. Transitions are interruptible, and will issue a
95      * [TransitionStep] with state = [TransitionState.CANCELED] before beginning the next one.
96      */
97     suspend fun startTransition(info: TransitionInfo): UUID?
98 
99     /**
100      * Emits STARTED and FINISHED transition steps to the given state. This is used during boot to
101      * seed the repository with the appropriate initial state.
102      */
103     suspend fun emitInitialStepsFromOff(to: KeyguardState, testSetup: Boolean = false)
104 
105     /**
106      * Allows manual control of a transition. When calling [startTransition], the consumer must pass
107      * in a null animator. In return, it will get a unique [UUID] that will be validated to allow
108      * further updates.
109      *
110      * When the transition is over, TransitionState.FINISHED must be passed into the [state]
111      * parameter.
112      */
113     suspend fun updateTransition(
114         transitionId: UUID,
115         @FloatRange(from = 0.0, to = 1.0) value: Float,
116         state: TransitionState,
117     )
118 
119     /**
120      * Forces the current transition to emit FINISHED, foregoing any additional RUNNING steps that
121      * otherwise would have been emitted.
122      *
123      * When the screen is off, upcoming performance changes cause all Animators to cease emitting
124      * frames, which means the Animator passed to [startTransition] will never finish if it was
125      * running when the screen turned off. Also, there's simply no reason to emit RUNNING steps when
126      * the screen isn't even on. As long as we emit FINISHED, everything should end up in the
127      * correct state.
128      */
129     suspend fun forceFinishCurrentTransition()
130 }
131 
132 @SysUISingleton
133 class KeyguardTransitionRepositoryImpl
134 @Inject
135 constructor(
136     @Main private val mainDispatcher: CoroutineDispatcher,
137     private val transitionCallback: KeyguardTransitionAnimationCallback,
138 ) : KeyguardTransitionRepository {
139     /**
140      * Each transition between [KeyguardState]s will have an associated Flow. In order to collect
141      * these events, clients should call [transition].
142      */
143     @SuppressLint("SharedFlowCreation")
144     private val _transitions =
145         MutableSharedFlow<TransitionStep>(
146                 replay = 2,
147                 extraBufferCapacity = 20,
148                 onBufferOverflow = BufferOverflow.DROP_OLDEST,
149             )
150             .traceAs("KTR-transitions")
151     override val transitions = _transitions.distinctUntilChanged()
152     private var lastStep: TransitionStep = TransitionStep()
153     private var lastAnimator: ValueAnimator? = null
154     private var animatorListener: AnimatorListenerAdapter? = null
155 
156     private val withContextMutex = Mutex()
157     private val _currentTransitionInfo: MutableStateFlow<TransitionInfo> =
158         MutableStateFlow(
159             TransitionInfo(
160                 ownerName = "",
161                 from = KeyguardState.OFF,
162                 to = KeyguardState.OFF,
163                 animator = null,
164             )
165         )
166     override var currentTransitionInfoInternal = _currentTransitionInfo.asStateFlow()
167 
168     @Volatile
169     override var currentTransitionInfo: TransitionInfo =
170         TransitionInfo(
171             ownerName = "",
172             from = KeyguardState.OFF,
173             to = KeyguardState.OFF,
174             animator = null,
175         )
176         private set
177 
178     /*
179      * When manual control of the transition is requested, a unique [UUID] is used as the handle
180      * to permit calls to [updateTransition]
181      */
182     private var updateTransitionId: UUID? = null
183 
184     // Only used in a test environment
185     var forceDelayForRaceConditionTest = false
186 
187     init {
188         // Start with a FINISHED transition in OFF. KeyguardBootInteractor will transition from OFF
189         // to either GONE or LOCKSCREEN once we're booted up and can determine which state we should
190         // start in.
191         emitTransition(
192             TransitionStep(KeyguardState.OFF, KeyguardState.OFF, 1f, TransitionState.FINISHED)
193         )
194     }
195 
startTransitionnull196     override suspend fun startTransition(info: TransitionInfo): UUID? {
197         if (transitionRaceCondition()) {
198             currentTransitionInfo = info
199         } else {
200             _currentTransitionInfo.value = info
201         }
202         Log.d(TAG, "(Internal) Setting current transition info: $info")
203 
204         // There is no fairness guarantee with 'withContext', which means that transitions could
205         // be processed out of order. Use a Mutex to guarantee ordering. [updateTransition]
206         // requires the same lock
207         withContextMutex.lock()
208         // Only used in a test environment
209         if (forceDelayForRaceConditionTest) {
210             delay(50L)
211         }
212 
213         // Animators must be started on the main thread.
214         return withContext("$TAG#startTransition", mainDispatcher) {
215             withContextMutex.unlock()
216             if (lastStep.from == info.from && lastStep.to == info.to) {
217                 Log.i(TAG, "Duplicate call to start the transition, rejecting: $info")
218                 return@withContext null
219             }
220             val isAnimatorRunning = lastAnimator?.isRunning() ?: false
221             val isManualTransitionRunning =
222                 updateTransitionId != null && lastStep.transitionState != TransitionState.FINISHED
223             val startingValue =
224                 if (isAnimatorRunning || isManualTransitionRunning) {
225                     Log.i(TAG, "Transition still active: $lastStep, canceling")
226                     when (info.modeOnCanceled) {
227                         TransitionModeOnCanceled.LAST_VALUE -> lastStep.value
228                         TransitionModeOnCanceled.RESET -> 0f
229                         TransitionModeOnCanceled.REVERSE -> 1f - lastStep.value
230                     }
231                 } else {
232                     0f
233                 }
234 
235             lastAnimator?.cancel()
236             lastAnimator = info.animator
237 
238             // Cancel any existing manual transitions
239             updateTransitionId?.let { uuid ->
240                 updateTransitionInternal(uuid, lastStep.value, TransitionState.CANCELED)
241             }
242 
243             info.animator?.let { animator ->
244                 // An animator was provided, so use it to run the transition
245                 animator.setFloatValues(startingValue, 1f)
246                 animator.duration = ((1f - startingValue) * animator.duration).toLong()
247                 val updateListener = AnimatorUpdateListener { animation ->
248                     emitTransition(
249                         TransitionStep(
250                             info,
251                             (animation.animatedValue as Float),
252                             TransitionState.RUNNING,
253                         )
254                     )
255                 }
256 
257                 animatorListener =
258                     object : AnimatorListenerAdapter() {
259                         override fun onAnimationStart(animation: Animator) {
260                             transitionCallback.onAnimationStarted(info.from, info.to)
261                             emitTransition(
262                                 TransitionStep(info, startingValue, TransitionState.STARTED)
263                             )
264                         }
265 
266                         override fun onAnimationCancel(animation: Animator) {
267                             transitionCallback.onAnimationCanceled(info.from, info.to)
268                             endAnimation(lastStep.value, TransitionState.CANCELED)
269                         }
270 
271                         override fun onAnimationEnd(animation: Animator) {
272                             transitionCallback.onAnimationEnded(info.from, info.to)
273                             endAnimation(1f, TransitionState.FINISHED)
274                         }
275 
276                         private fun endAnimation(value: Float, state: TransitionState) {
277                             emitTransition(TransitionStep(info, value, state))
278                             animator.removeListener(this)
279                             animator.removeUpdateListener(updateListener)
280                             lastAnimator = null
281                             animatorListener = null
282                         }
283                     }
284                 animator.addListener(animatorListener)
285                 animator.addUpdateListener(updateListener)
286                 animator.start()
287                 return@withContext null
288             }
289                 ?: run {
290                     emitTransition(
291                         nextStep = TransitionStep(info, startingValue, TransitionState.STARTED),
292                         isManual = true,
293                     )
294 
295                     // No animator, so it's manual. Provide a mechanism to callback
296                     updateTransitionId = UUID.randomUUID()
297                     return@withContext updateTransitionId
298                 }
299         }
300     }
301 
updateTransitionnull302     override suspend fun updateTransition(
303         transitionId: UUID,
304         @FloatRange(from = 0.0, to = 1.0) value: Float,
305         state: TransitionState,
306     ) {
307         // There is no fairness guarantee with 'withContext', which means that transitions could
308         // be processed out of order. Use a Mutex to guarantee ordering. [startTransition]
309         // requires the same lock
310         withContextMutex.lock()
311         withContext("$TAG#updateTransition", mainDispatcher) {
312             withContextMutex.unlock()
313 
314             updateTransitionInternal(transitionId, value, state)
315         }
316     }
317 
forceFinishCurrentTransitionnull318     override suspend fun forceFinishCurrentTransition() {
319         if (lastAnimator?.isRunning != true) {
320             return
321         }
322 
323         withContextMutex.lock()
324 
325         return withContext("$TAG#forceFinishCurrentTransition", mainDispatcher) {
326             withContextMutex.unlock()
327 
328             Log.d(TAG, "forceFinishCurrentTransition() - emitting FINISHED early.")
329 
330             lastAnimator?.apply {
331                 // Cancel the animator, but remove listeners first so we don't emit CANCELED.
332                 removeAllListeners()
333                 cancel()
334 
335                 // Emit a final 1f RUNNING step to ensure that any transitions not listening for a
336                 // FINISHED step end up in the right end state.
337                 emitTransition(TransitionStep(currentTransitionInfo, 1f, TransitionState.RUNNING))
338 
339                 // Ask the listener to emit FINISHED and clean up its state.
340                 animatorListener?.onAnimationEnd(this)
341             }
342         }
343     }
344 
updateTransitionInternalnull345     private suspend fun updateTransitionInternal(
346         transitionId: UUID,
347         @FloatRange(from = 0.0, to = 1.0) value: Float,
348         state: TransitionState,
349     ) {
350         if (updateTransitionId != transitionId) {
351             Log.e(TAG, "Attempting to update with old/invalid transitionId: $transitionId")
352             return
353         }
354 
355         if (state == TransitionState.FINISHED || state == TransitionState.CANCELED) {
356             updateTransitionId = null
357         }
358 
359         val nextStep = lastStep.copy(value = value, transitionState = state)
360         emitTransition(nextStep, isManual = true)
361     }
362 
emitTransitionnull363     private fun emitTransition(nextStep: TransitionStep, isManual: Boolean = false) {
364         logAndTrace(nextStep, isManual)
365         _transitions.tryEmit(nextStep)
366         lastStep = nextStep
367     }
368 
emitInitialStepsFromOffnull369     override suspend fun emitInitialStepsFromOff(to: KeyguardState, testSetup: Boolean) {
370         val ownerName = "KeyguardTransitionRepository(boot)"
371         // Tests runs on testDispatcher, which is not the main thread, causing the animator thread
372         // check to fail
373         if (testSetup) {
374             if (transitionRaceCondition()) {
375                 currentTransitionInfo =
376                     TransitionInfo(
377                         ownerName = ownerName,
378                         from = KeyguardState.OFF,
379                         to = to,
380                         animator = null,
381                     )
382             } else {
383                 _currentTransitionInfo.value =
384                     TransitionInfo(
385                         ownerName = ownerName,
386                         from = KeyguardState.OFF,
387                         to = to,
388                         animator = null,
389                     )
390             }
391             emitTransition(
392                 TransitionStep(
393                     KeyguardState.OFF,
394                     to,
395                     0f,
396                     TransitionState.STARTED,
397                     ownerName = ownerName,
398                 )
399             )
400 
401             emitTransition(
402                 TransitionStep(
403                     KeyguardState.OFF,
404                     to,
405                     1f,
406                     TransitionState.FINISHED,
407                     ownerName = ownerName,
408                 )
409             )
410         } else {
411             startTransition(
412                 TransitionInfo(
413                     ownerName = ownerName,
414                     from = KeyguardState.OFF,
415                     to = to,
416                     animator =
417                         ValueAnimator().apply {
418                             interpolator = Interpolators.LINEAR
419                             duration = 933L
420                         },
421                 )
422             )
423         }
424     }
425 
logAndTracenull426     private fun logAndTrace(step: TransitionStep, isManual: Boolean) {
427         if (step.transitionState == TransitionState.RUNNING) {
428             return
429         }
430         val manualStr = if (isManual) " (manual)" else ""
431         val traceName = "Transition: ${step.from} -> ${step.to}$manualStr"
432 
433         val traceCookie = traceName.hashCode()
434         when (step.transitionState) {
435             TransitionState.STARTED -> Trace.beginAsyncSection(traceName, traceCookie)
436             TransitionState.FINISHED -> Trace.endAsyncSection(traceName, traceCookie)
437             TransitionState.CANCELED -> Trace.endAsyncSection(traceName, traceCookie)
438             else -> {}
439         }
440 
441         Log.i(TAG, "${step.transitionState.name} transition: $step$manualStr")
442     }
443 
444     companion object {
445         private const val TAG = "KeyguardTransitionRepository"
446     }
447 }
448