1 /*
<lambda>null2  * Copyright (C) 2024 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.keyguard.domain.interactor.scenetransition
18 
19 import com.android.compose.animation.scene.ObservableTransitionState
20 import com.android.compose.animation.scene.SceneKey
21 import com.android.systemui.CoreStartable
22 import com.android.systemui.dagger.SysUISingleton
23 import com.android.systemui.dagger.qualifiers.Application
24 import com.android.systemui.keyguard.data.repository.LockscreenSceneTransitionRepository
25 import com.android.systemui.keyguard.data.repository.LockscreenSceneTransitionRepository.Companion.DEFAULT_STATE
26 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
27 import com.android.systemui.keyguard.shared.model.KeyguardState
28 import com.android.systemui.keyguard.shared.model.KeyguardState.UNDEFINED
29 import com.android.systemui.keyguard.shared.model.TransitionInfo
30 import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
31 import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
32 import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
33 import com.android.systemui.scene.domain.interactor.SceneInteractor
34 import com.android.systemui.scene.shared.model.Scenes
35 import com.android.systemui.util.kotlin.pairwise
36 import java.util.UUID
37 import javax.inject.Inject
38 import kotlinx.coroutines.CoroutineScope
39 import kotlinx.coroutines.Job
40 import kotlinx.coroutines.launch
41 
42 /**
43  * This class listens to scene framework scene transitions and manages keyguard transition framework
44  * (KTF) states accordingly.
45  *
46  * There are a few rules:
47  * - When scene framework is on a scene outside of Lockscreen, then KTF is in state UNDEFINED
48  * - When scene framework is on Lockscreen, KTF is allowed to change its scenes freely
49  * - When scene framework is transitioning away from Lockscreen, then KTF transitions to UNDEFINED
50  *   and shares its progress.
51  * - When scene framework is transitioning to Lockscreen, then KTF starts a transition to LOCKSCREEN
52  *   but it is allowed to interrupt this transition and transition to other internal KTF states
53  *
54  * There are a few notable differences between SceneTransitionLayout (STL) and KTF that require
55  * special treatment when synchronizing both state machines.
56  * - STL does not emit cancelations as KTF does
57  * - Both STL and KTF require state continuity, though the rules from where starting the next
58  *   transition is allowed is different on each side:
59  *     - STL has a concept of "currentScene" which can be chosen to be either A or B in a A -> B
60  *       transition. The currentScene determines which transition can be started next. In KTF the
61  *       currentScene is always the `to` state. Which means transitions can only be started from B.
62  *       This also holds true when A -> B was canceled: the next transition needs to start from B.
63  *     - KTF can not settle back in its from scene, instead it needs to cancel and start a reversed
64  *       transition.
65  */
66 @SysUISingleton
67 class LockscreenSceneTransitionInteractor
68 @Inject
69 constructor(
70     val transitionInteractor: KeyguardTransitionInteractor,
71     @Application private val applicationScope: CoroutineScope,
72     private val sceneInteractor: SceneInteractor,
73     private val repository: LockscreenSceneTransitionRepository,
74 ) : CoreStartable, SceneInteractor.OnSceneAboutToChangeListener {
75 
76     private var currentTransitionId: UUID? = null
77     private var progressJob: Job? = null
78 
79     override fun start() {
80         sceneInteractor.registerSceneStateProcessor(this)
81         listenForSceneTransitionProgress()
82     }
83 
84     override fun onSceneAboutToChange(toScene: SceneKey, sceneState: Any?) {
85         if (toScene != Scenes.Lockscreen || sceneState == null) return
86         if (sceneState !is KeyguardState) {
87             throw IllegalArgumentException("Lockscreen sceneState needs to be a KeyguardState.")
88         }
89         repository.nextLockscreenTargetState.value = sceneState
90     }
91 
92     private fun listenForSceneTransitionProgress() {
93         applicationScope.launch {
94             sceneInteractor.transitionState
95                 .pairwise(ObservableTransitionState.Idle(Scenes.Lockscreen))
96                 .collect { (prevTransition, transition) ->
97                     when (transition) {
98                         is ObservableTransitionState.Idle -> handleIdle(prevTransition, transition)
99                         is ObservableTransitionState.Transition -> handleTransition(transition)
100                     }
101                 }
102         }
103     }
104 
105     private suspend fun handleIdle(
106         prevTransition: ObservableTransitionState,
107         idle: ObservableTransitionState.Idle
108     ) {
109         if (currentTransitionId == null) return
110         if (prevTransition !is ObservableTransitionState.Transition) return
111 
112         if (idle.currentScene == prevTransition.toScene) {
113             finishCurrentTransition()
114         } else {
115             val targetState =
116                 if (idle.currentScene == Scenes.Lockscreen) {
117                     transitionInteractor.getStartedFromState()
118                 } else {
119                     UNDEFINED
120                 }
121             finishReversedTransitionTo(targetState)
122         }
123     }
124 
125     private fun finishCurrentTransition() {
126         transitionInteractor.updateTransition(currentTransitionId!!, 1f, FINISHED)
127         resetTransitionData()
128     }
129 
130     private suspend fun finishReversedTransitionTo(state: KeyguardState) {
131         val newTransition =
132             TransitionInfo(
133                 ownerName = this::class.java.simpleName,
134                 from = transitionInteractor.currentTransitionInfoInternal.value.to,
135                 to = state,
136                 animator = null,
137                 modeOnCanceled = TransitionModeOnCanceled.REVERSE
138             )
139         currentTransitionId = transitionInteractor.startTransition(newTransition)
140         transitionInteractor.updateTransition(currentTransitionId!!, 1f, FINISHED)
141         resetTransitionData()
142     }
143 
144     private fun resetTransitionData() {
145         progressJob?.cancel()
146         progressJob = null
147         currentTransitionId = null
148     }
149 
150     private suspend fun handleTransition(transition: ObservableTransitionState.Transition) {
151         if (transition.fromScene == Scenes.Lockscreen) {
152             if (currentTransitionId != null) {
153                 val currentToState = transitionInteractor.currentTransitionInfoInternal.value.to
154                 if (currentToState == UNDEFINED) {
155                     transitionKtfTo(transitionInteractor.getStartedFromState())
156                 }
157             }
158             startTransitionFromLockscreen()
159             collectProgress(transition)
160         } else if (transition.toScene == Scenes.Lockscreen) {
161             if (currentTransitionId != null) {
162                 transitionKtfTo(UNDEFINED)
163             }
164             startTransitionToLockscreen()
165             collectProgress(transition)
166         } else {
167             transitionKtfTo(UNDEFINED)
168         }
169     }
170 
171     private suspend fun transitionKtfTo(state: KeyguardState) {
172         // TODO(b/330311871): This is based on a sharedFlow and thus might not be up-to-date and
173         //  cause a race condition. (There is no known scenario that is currently affected.)
174         val currentTransition = transitionInteractor.transitionState.value
175         if (currentTransition.isFinishedIn(state)) {
176             // This is already the state we want to be in
177             resetTransitionData()
178         } else if (currentTransition.isTransitioning(to = state)) {
179             finishCurrentTransition()
180         } else {
181             finishReversedTransitionTo(state)
182         }
183     }
184 
185     private fun collectProgress(transition: ObservableTransitionState.Transition) {
186         progressJob?.cancel()
187         progressJob = applicationScope.launch { transition.progress.collect { updateProgress(it) } }
188     }
189 
190     private suspend fun startTransitionToLockscreen() {
191         val newTransition =
192             TransitionInfo(
193                 ownerName = this::class.java.simpleName,
194                 from = UNDEFINED,
195                 to = repository.nextLockscreenTargetState.value,
196                 animator = null,
197                 modeOnCanceled = TransitionModeOnCanceled.RESET
198             )
199         repository.nextLockscreenTargetState.value = DEFAULT_STATE
200         startTransition(newTransition)
201     }
202 
203     private suspend fun startTransitionFromLockscreen() {
204         val currentState = transitionInteractor.currentTransitionInfoInternal.value.to
205         val newTransition =
206             TransitionInfo(
207                 ownerName = this::class.java.simpleName,
208                 from = currentState,
209                 to = UNDEFINED,
210                 animator = null,
211                 modeOnCanceled = TransitionModeOnCanceled.RESET
212             )
213         startTransition(newTransition)
214     }
215 
216     private suspend fun startTransition(transitionInfo: TransitionInfo) {
217         if (currentTransitionId != null) {
218             resetTransitionData()
219         }
220         currentTransitionId = transitionInteractor.startTransition(transitionInfo)
221     }
222 
223     private fun updateProgress(progress: Float) {
224         if (currentTransitionId == null) return
225         transitionInteractor.updateTransition(
226             currentTransitionId!!,
227             progress.coerceIn(0f, 1f),
228             RUNNING
229         )
230     }
231 }
232