1 /* <lambda>null2 * 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 */ 17 18 package com.android.systemui.keyguard.data.repository 19 20 import android.annotation.FloatRange 21 import com.android.systemui.Flags.transitionRaceCondition 22 import com.android.systemui.dagger.SysUISingleton 23 import com.android.systemui.keyguard.shared.model.KeyguardState 24 import com.android.systemui.keyguard.shared.model.TransitionInfo 25 import com.android.systemui.keyguard.shared.model.TransitionState 26 import com.android.systemui.keyguard.shared.model.TransitionStep 27 import dagger.Binds 28 import dagger.Module 29 import java.util.UUID 30 import javax.inject.Inject 31 import junit.framework.Assert.fail 32 import kotlinx.coroutines.CoroutineScope 33 import kotlinx.coroutines.Job 34 import kotlinx.coroutines.channels.BufferOverflow 35 import kotlinx.coroutines.flow.MutableSharedFlow 36 import kotlinx.coroutines.flow.MutableStateFlow 37 import kotlinx.coroutines.flow.SharedFlow 38 import kotlinx.coroutines.flow.asStateFlow 39 import kotlinx.coroutines.launch 40 import kotlinx.coroutines.test.TestCoroutineScheduler 41 import kotlinx.coroutines.test.TestScope 42 import kotlinx.coroutines.test.runCurrent 43 44 /** 45 * Fake implementation of [KeyguardTransitionRepository]. 46 * 47 * By default, will be seeded with a transition from OFF -> LOCKSCREEN, which is the most common 48 * case. If the lockscreen is disabled, or we're in setup wizard, the repository will initialize 49 * with OFF -> GONE. Construct with initInLockscreen = false if your test requires this behavior. 50 */ 51 @SysUISingleton 52 class FakeKeyguardTransitionRepository( 53 private val initInLockscreen: Boolean = true, 54 55 /** 56 * Initial value for [FakeKeyguardTransitionRepository.sendTransitionStepsOnStartTransition]. 57 * This needs to be configurable in the constructor since some transitions are triggered on 58 * init, before a test has the chance to set sendTransitionStepsOnStartTransition to false. 59 */ 60 private val initiallySendTransitionStepsOnStartTransition: Boolean = true, 61 private val testScope: TestScope, 62 ) : KeyguardTransitionRepository { 63 64 /** 65 * If true, calls to [startTransition] will automatically emit STARTED, RUNNING, and FINISHED 66 * transition steps from/to the given states. 67 * 68 * [startTransition] is what the From*TransitionInteractors call, so this more closely emulates 69 * the behavior of the real KeyguardTransitionRepository, and reduces the work needed to 70 * manually set up the repository state in each test. For example, setting dreaming=true will 71 * automatically cause FromDreamingTransitionInteractor to call startTransition(DREAMING), and 72 * then we'll send STARTED/RUNNING/FINISHED DREAMING TransitionSteps. 73 * 74 * If your test needs to make assertions at specific points between STARTED/FINISHED, or if it's 75 * difficult to set up all of the conditions to make the transition interactors actually call 76 * startTransition, set this value to false. 77 */ 78 var sendTransitionStepsOnStartTransition = initiallySendTransitionStepsOnStartTransition 79 80 private val _transitions = 81 MutableSharedFlow<TransitionStep>(replay = 3, onBufferOverflow = BufferOverflow.DROP_OLDEST) 82 override val transitions: SharedFlow<TransitionStep> = _transitions 83 84 @Inject 85 constructor( 86 testScope: TestScope 87 ) : this( 88 initInLockscreen = true, 89 initiallySendTransitionStepsOnStartTransition = true, 90 testScope, 91 ) 92 93 private val _currentTransitionInfo: MutableStateFlow<TransitionInfo> = 94 MutableStateFlow( 95 TransitionInfo( 96 ownerName = "", 97 from = KeyguardState.OFF, 98 to = KeyguardState.LOCKSCREEN, 99 animator = null, 100 ) 101 ) 102 override var currentTransitionInfoInternal = _currentTransitionInfo.asStateFlow() 103 override var currentTransitionInfo = 104 TransitionInfo( 105 ownerName = "", 106 from = KeyguardState.OFF, 107 to = KeyguardState.LOCKSCREEN, 108 animator = null, 109 ) 110 111 init { 112 // Seed with a FINISHED transition in OFF, same as the real repository. 113 _transitions.tryEmit( 114 TransitionStep(KeyguardState.OFF, KeyguardState.OFF, 1f, TransitionState.FINISHED) 115 ) 116 117 if (initInLockscreen) { 118 tryEmitInitialStepsFromOff(KeyguardState.LOCKSCREEN) 119 } else { 120 tryEmitInitialStepsFromOff(KeyguardState.OFF) 121 } 122 } 123 124 /** 125 * Sends TransitionSteps between [from] and [to], calling [runCurrent] after each step. 126 * 127 * By default, sends steps through FINISHED (STARTED, RUNNING @0.5f, RUNNING @1f, FINISHED) but 128 * can be halted part way using [throughTransitionState]. 129 */ 130 suspend fun sendTransitionSteps( 131 from: KeyguardState, 132 to: KeyguardState, 133 testScope: TestScope, 134 throughTransitionState: TransitionState = TransitionState.FINISHED, 135 ) { 136 sendTransitionSteps(from, to, testScope.testScheduler, throughTransitionState) 137 } 138 139 /** 140 * Sends a STARTED step between [from] and [to], followed by two RUNNING steps at value 141 * [throughValue] / 2 and [throughValue], calling [runCurrent] after each step. 142 */ 143 suspend fun sendTransitionStepsThroughRunning( 144 from: KeyguardState, 145 to: KeyguardState, 146 testScope: TestScope, 147 throughValue: Float = 1f, 148 ) { 149 sendTransitionSteps( 150 from, 151 to, 152 testScope.testScheduler, 153 TransitionState.RUNNING, 154 throughValue, 155 ) 156 } 157 158 /** 159 * Sends the provided [step] and makes sure that all previous [TransitionState]'s are sent when 160 * [fillInSteps] is true. e.g. when a step FINISHED is provided, a step with STARTED and RUNNING 161 * is also sent. 162 */ 163 suspend fun sendTransitionSteps( 164 step: TransitionStep, 165 testScope: TestScope, 166 fillInSteps: Boolean = true, 167 ) { 168 if (fillInSteps && step.transitionState != TransitionState.STARTED) { 169 sendTransitionStep( 170 step = 171 TransitionStep( 172 transitionState = TransitionState.STARTED, 173 from = step.from, 174 to = step.to, 175 value = 0f, 176 ) 177 ) 178 testScope.testScheduler.runCurrent() 179 180 if (step.transitionState != TransitionState.RUNNING) { 181 sendTransitionStep( 182 step = 183 TransitionStep( 184 transitionState = TransitionState.RUNNING, 185 from = step.from, 186 to = step.to, 187 value = 0.6f, 188 ) 189 ) 190 testScope.testScheduler.runCurrent() 191 } 192 } 193 sendTransitionStep(step = step) 194 testScope.testScheduler.runCurrent() 195 } 196 197 /** 198 * Sends TransitionSteps between [from] and [to], calling [runCurrent] after each step. 199 * 200 * By default, sends steps through FINISHED (STARTED, RUNNING @0.5f, RUNNING @1f, FINISHED) but 201 * can be halted part way using [throughTransitionState]. 202 */ 203 suspend fun sendTransitionSteps( 204 from: KeyguardState, 205 to: KeyguardState, 206 testScheduler: TestCoroutineScheduler, 207 throughTransitionState: TransitionState = TransitionState.FINISHED, 208 throughTransitionValue: Float = 1f, 209 ) { 210 val lastStep = _transitions.replayCache.lastOrNull() 211 if (lastStep != null && lastStep.transitionState != TransitionState.FINISHED) { 212 sendTransitionStep( 213 step = 214 TransitionStep( 215 transitionState = TransitionState.CANCELED, 216 from = lastStep.from, 217 to = lastStep.to, 218 value = 0f, 219 ) 220 ) 221 testScheduler.runCurrent() 222 } 223 224 sendTransitionStep( 225 step = 226 TransitionStep( 227 transitionState = TransitionState.STARTED, 228 from = from, 229 to = to, 230 value = 0f, 231 ) 232 ) 233 testScheduler.runCurrent() 234 235 if ( 236 throughTransitionState == TransitionState.RUNNING || 237 throughTransitionState == TransitionState.FINISHED 238 ) { 239 // Send two steps to better simulate RUNNING transitions. 240 sendTransitionStep( 241 step = 242 TransitionStep( 243 transitionState = TransitionState.RUNNING, 244 from = from, 245 to = to, 246 value = throughTransitionValue / 2f, 247 ) 248 ) 249 testScheduler.runCurrent() 250 251 sendTransitionStep( 252 step = 253 TransitionStep( 254 transitionState = TransitionState.RUNNING, 255 from = from, 256 to = to, 257 value = throughTransitionValue, 258 ) 259 ) 260 testScheduler.runCurrent() 261 } 262 263 if (throughTransitionState == TransitionState.FINISHED) { 264 sendTransitionStep( 265 step = 266 TransitionStep( 267 transitionState = TransitionState.FINISHED, 268 from = from, 269 to = to, 270 value = 1f, 271 ) 272 ) 273 testScheduler.runCurrent() 274 } 275 } 276 277 suspend fun sendTransitionStep(step: TransitionStep, validateStep: Boolean = true) { 278 this.sendTransitionStep( 279 step = step, 280 validateStep = validateStep, 281 ownerName = step.ownerName, 282 ) 283 } 284 285 /** 286 * Directly emits the provided TransitionStep, which can be useful in tests for testing behavior 287 * during specific phases of a transition (such as asserting values while a transition has 288 * STARTED but not FINISHED). 289 * 290 * WARNING: You can get the transition repository into undefined states using this method - for 291 * example, you could send a FINISHED step to LOCKSCREEN having never sent a STARTED step. This 292 * can get flows that combine startedStep/finishedStep into a bad state. 293 * 294 * If you are just trying to get the transition repository FINISHED in a certain state, use 295 * [sendTransitionSteps] - this will send STARTED, RUNNING, and FINISHED steps for you which 296 * ensures that [KeyguardTransitionInteractor] flows will be in the correct state. 297 * 298 * If you're testing something involving transitions themselves and are sure you want to send 299 * only a FINISHED step, override [validateStep]. 300 */ 301 suspend fun sendTransitionStep( 302 from: KeyguardState = KeyguardState.OFF, 303 to: KeyguardState = KeyguardState.OFF, 304 value: Float = 0f, 305 transitionState: TransitionState = TransitionState.FINISHED, 306 ownerName: String = "", 307 step: TransitionStep = 308 TransitionStep( 309 from = from, 310 to = to, 311 value = value, 312 transitionState = transitionState, 313 ownerName = ownerName, 314 ), 315 validateStep: Boolean = true, 316 ) { 317 if (step.transitionState == TransitionState.STARTED) { 318 if (transitionRaceCondition()) { 319 currentTransitionInfo = 320 TransitionInfo(from = step.from, to = step.to, animator = null, ownerName = "") 321 } else { 322 _currentTransitionInfo.value = 323 TransitionInfo(from = step.from, to = step.to, animator = null, ownerName = "") 324 } 325 } 326 327 _transitions.replayCache.last().let { lastStep -> 328 if ( 329 validateStep && 330 step.transitionState == TransitionState.FINISHED && 331 !(lastStep.transitionState == TransitionState.STARTED || 332 lastStep.transitionState == TransitionState.RUNNING) 333 ) { 334 fail( 335 "Attempted to send a FINISHED TransitionStep without a prior " + 336 "STARTED/RUNNING step. This leaves the FakeKeyguardTransitionRepository " + 337 "in an undefined state and should not be done. Pass " + 338 "allowInvalidStep=true to sendTransitionStep if you are trying to test " + 339 "this specific and" + 340 "incorrect state." 341 ) 342 } 343 } 344 _transitions.emit(step) 345 } 346 347 /** Version of [sendTransitionStep] that's usable from Java tests. */ 348 fun sendTransitionStepJava( 349 coroutineScope: CoroutineScope, 350 step: TransitionStep, 351 validateStep: Boolean = true, 352 ): Job { 353 return coroutineScope.launch { 354 sendTransitionStep(step = step, validateStep = validateStep) 355 } 356 } 357 358 suspend fun sendTransitionSteps( 359 steps: List<TransitionStep>, 360 testScope: TestScope, 361 validateSteps: Boolean = true, 362 ) { 363 steps.forEach { 364 sendTransitionStep(step = it, validateStep = validateSteps) 365 testScope.testScheduler.runCurrent() 366 } 367 } 368 369 override suspend fun startTransition(info: TransitionInfo): UUID? { 370 if (transitionRaceCondition()) { 371 currentTransitionInfo = info 372 } else { 373 _currentTransitionInfo.value = info 374 } 375 376 if (sendTransitionStepsOnStartTransition) { 377 sendTransitionSteps(from = info.from, to = info.to, testScope = testScope) 378 } 379 380 return if (info.animator == null) UUID.randomUUID() else null 381 } 382 383 override suspend fun emitInitialStepsFromOff(to: KeyguardState, testSetup: Boolean) { 384 tryEmitInitialStepsFromOff(to) 385 } 386 387 private fun tryEmitInitialStepsFromOff(to: KeyguardState) { 388 _transitions.tryEmit( 389 TransitionStep( 390 KeyguardState.OFF, 391 to, 392 0f, 393 TransitionState.STARTED, 394 ownerName = "KeyguardTransitionRepository(boot)", 395 ) 396 ) 397 398 _transitions.tryEmit( 399 TransitionStep( 400 KeyguardState.OFF, 401 to, 402 1f, 403 TransitionState.FINISHED, 404 ownerName = "KeyguardTransitionRepository(boot)", 405 ) 406 ) 407 } 408 409 override suspend fun updateTransition( 410 transitionId: UUID, 411 @FloatRange(from = 0.0, to = 1.0) value: Float, 412 state: TransitionState, 413 ) = Unit 414 415 override suspend fun forceFinishCurrentTransition() { 416 _transitions.tryEmit( 417 TransitionStep( 418 _currentTransitionInfo.value.from, 419 _currentTransitionInfo.value.to, 420 1f, 421 TransitionState.FINISHED, 422 ownerName = _currentTransitionInfo.value.ownerName, 423 ) 424 ) 425 } 426 427 suspend fun transitionTo(from: KeyguardState, to: KeyguardState) { 428 sendTransitionStep(TransitionStep(from, to, 0f, TransitionState.STARTED)) 429 sendTransitionStep(TransitionStep(from, to, 0.5f, TransitionState.RUNNING)) 430 sendTransitionStep(TransitionStep(from, to, 1f, TransitionState.FINISHED)) 431 } 432 } 433 434 @Module 435 interface FakeKeyguardTransitionRepositoryModule { bindFakenull436 @Binds fun bindFake(fake: FakeKeyguardTransitionRepository): KeyguardTransitionRepository 437 } 438