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