1 /*
2  * 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.os.Trace
24 import android.util.Log
25 import com.android.systemui.dagger.SysUISingleton
26 import com.android.systemui.keyguard.shared.model.KeyguardState
27 import com.android.systemui.keyguard.shared.model.TransitionInfo
28 import com.android.systemui.keyguard.shared.model.TransitionState
29 import com.android.systemui.keyguard.shared.model.TransitionStep
30 import java.util.UUID
31 import javax.inject.Inject
32 import kotlinx.coroutines.channels.BufferOverflow
33 import kotlinx.coroutines.flow.Flow
34 import kotlinx.coroutines.flow.MutableSharedFlow
35 import kotlinx.coroutines.flow.asSharedFlow
36 import kotlinx.coroutines.flow.distinctUntilChanged
37 import kotlinx.coroutines.flow.filter
38 
39 /**
40  * The source of truth for all keyguard transitions.
41  *
42  * While the keyguard component is visible, it can undergo a number of transitions between different
43  * UI screens, such as AOD (Always-on Display), Bouncer, and others mentioned in [KeyguardState].
44  * These UI elements should listen to events emitted by [transitions], to ensure a centrally
45  * coordinated experience.
46  *
47  * To create or modify logic that controls when and how transitions get created, look at
48  * [TransitionInteractor]. These interactors will call [startTransition] and [updateTransition] on
49  * this repository.
50  *
51  * To print all transitions to logcat to help with debugging, run this command:
52  * adb shell settings put global systemui/buffer/KeyguardLog VERBOSE
53  *
54  * This will print all keyguard transitions to logcat with the KeyguardTransitionAuditLogger tag.
55  */
56 interface KeyguardTransitionRepository {
57     /**
58      * All events regarding transitions, as they start, run, and complete. [TransitionStep#value] is
59      * a float between [0, 1] representing progress towards completion. If this is a user driven
60      * transition, that value may not be a monotonic progression, as the user may swipe in any
61      * direction.
62      */
63     val transitions: Flow<TransitionStep>
64 
65     /**
66      * Interactors that require information about changes between [KeyguardState]s will call this to
67      * register themselves for flowable [TransitionStep]s when that transition occurs.
68      */
69     fun transition(from: KeyguardState, to: KeyguardState): Flow<TransitionStep> {
70         return transitions.filter { step -> step.from == from && step.to == to }
71     }
72 
73     /**
74      * Begin a transition from one state to another. Transitions are interruptible, and will issue a
75      * [TransitionStep] with state = [TransitionState.CANCELED] before beginning the next one.
76      *
77      * When canceled, there are two options: to continue from the current position of the prior
78      * transition, or to reset the position. When [resetIfCanceled] == true, it will do the latter.
79      */
80     fun startTransition(info: TransitionInfo, resetIfCanceled: Boolean = false): UUID?
81 
82     /**
83      * Allows manual control of a transition. When calling [startTransition], the consumer must pass
84      * in a null animator. In return, it will get a unique [UUID] that will be validated to allow
85      * further updates.
86      *
87      * When the transition is over, TransitionState.FINISHED must be passed into the [state]
88      * parameter.
89      */
90     fun updateTransition(
91         transitionId: UUID,
92         @FloatRange(from = 0.0, to = 1.0) value: Float,
93         state: TransitionState
94     )
95 }
96 
97 @SysUISingleton
98 class KeyguardTransitionRepositoryImpl @Inject constructor() : KeyguardTransitionRepository {
99     /*
100      * Each transition between [KeyguardState]s will have an associated Flow.
101      * In order to collect these events, clients should call [transition].
102      */
103     private val _transitions =
104         MutableSharedFlow<TransitionStep>(
105             replay = 2,
106             extraBufferCapacity = 10,
107             onBufferOverflow = BufferOverflow.DROP_OLDEST,
108         )
109     override val transitions = _transitions.asSharedFlow().distinctUntilChanged()
110     private var lastStep: TransitionStep = TransitionStep()
111     private var lastAnimator: ValueAnimator? = null
112 
113     /*
114      * When manual control of the transition is requested, a unique [UUID] is used as the handle
115      * to permit calls to [updateTransition]
116      */
117     private var updateTransitionId: UUID? = null
118 
119     init {
120         // Seed with transitions signaling a boot into lockscreen state
121         emitTransition(
122             TransitionStep(
123                 KeyguardState.OFF,
124                 KeyguardState.LOCKSCREEN,
125                 0f,
126                 TransitionState.STARTED,
127                 KeyguardTransitionRepositoryImpl::class.simpleName!!,
128             )
129         )
130         emitTransition(
131             TransitionStep(
132                 KeyguardState.OFF,
133                 KeyguardState.LOCKSCREEN,
134                 1f,
135                 TransitionState.FINISHED,
136                 KeyguardTransitionRepositoryImpl::class.simpleName!!,
137             )
138         )
139     }
140 
141     override fun startTransition(
142         info: TransitionInfo,
143         resetIfCanceled: Boolean,
144     ): UUID? {
145         if (lastStep.from == info.from && lastStep.to == info.to) {
146             Log.i(TAG, "Duplicate call to start the transition, rejecting: $info")
147             return null
148         }
149         val startingValue =
150             if (lastStep.transitionState != TransitionState.FINISHED) {
151                 Log.i(TAG, "Transition still active: $lastStep, canceling")
152                 if (resetIfCanceled) {
153                     0f
154                 } else {
155                     lastStep.value
156                 }
157             } else {
158                 0f
159             }
160 
161         lastAnimator?.cancel()
162         lastAnimator = info.animator
163 
164         info.animator?.let { animator ->
165             // An animator was provided, so use it to run the transition
166             animator.setFloatValues(startingValue, 1f)
167             animator.duration = ((1f - startingValue) * animator.duration).toLong()
168             val updateListener = AnimatorUpdateListener { animation ->
169                 emitTransition(
170                     TransitionStep(
171                         info,
172                         (animation.animatedValue as Float),
173                         TransitionState.RUNNING
174                     )
175                 )
176             }
177             val adapter =
178                 object : AnimatorListenerAdapter() {
179                     override fun onAnimationStart(animation: Animator) {
180                         emitTransition(TransitionStep(info, startingValue, TransitionState.STARTED))
181                     }
182                     override fun onAnimationCancel(animation: Animator) {
183                         endAnimation(lastStep.value, TransitionState.CANCELED)
184                     }
185                     override fun onAnimationEnd(animation: Animator) {
186                         endAnimation(1f, TransitionState.FINISHED)
187                     }
188 
189                     private fun endAnimation(value: Float, state: TransitionState) {
190                         emitTransition(TransitionStep(info, value, state))
191                         animator.removeListener(this)
192                         animator.removeUpdateListener(updateListener)
193                         lastAnimator = null
194                     }
195                 }
196             animator.addListener(adapter)
197             animator.addUpdateListener(updateListener)
198             animator.start()
199             return@startTransition null
200         }
201             ?: run {
202                 emitTransition(TransitionStep(info, startingValue, TransitionState.STARTED))
203 
204                 // No animator, so it's manual. Provide a mechanism to callback
205                 updateTransitionId = UUID.randomUUID()
206                 return@startTransition updateTransitionId
207             }
208     }
209 
210     override fun updateTransition(
211         transitionId: UUID,
212         @FloatRange(from = 0.0, to = 1.0) value: Float,
213         state: TransitionState
214     ) {
215         if (updateTransitionId != transitionId) {
216             Log.wtf(TAG, "Attempting to update with old/invalid transitionId: $transitionId")
217             return
218         }
219 
220         if (state == TransitionState.FINISHED || state == TransitionState.CANCELED) {
221             updateTransitionId = null
222         }
223 
224         val nextStep = lastStep.copy(value = value, transitionState = state)
225         emitTransition(nextStep, isManual = true)
226     }
227 
228     private fun emitTransition(nextStep: TransitionStep, isManual: Boolean = false) {
229         trace(nextStep, isManual)
230         val emitted = _transitions.tryEmit(nextStep)
231         if (!emitted) {
232             Log.w(TAG, "Failed to emit next value without suspending")
233         }
234         lastStep = nextStep
235     }
236 
237     private fun trace(step: TransitionStep, isManual: Boolean) {
238         if (step.transitionState == TransitionState.RUNNING) {
239             return
240         }
241         val traceName =
242             "Transition: ${step.from} -> ${step.to} " +
243                 if (isManual) {
244                     "(manual)"
245                 } else {
246                     ""
247                 }
248         val traceCookie = traceName.hashCode()
249         if (step.transitionState == TransitionState.STARTED) {
250             Trace.beginAsyncSection(traceName, traceCookie)
251         } else if (
252             step.transitionState == TransitionState.FINISHED ||
253                 step.transitionState == TransitionState.CANCELED
254         ) {
255             Trace.endAsyncSection(traceName, traceCookie)
256         }
257     }
258 
259     companion object {
260         private const val TAG = "KeyguardTransitionRepository"
261     }
262 }
263