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