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