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.dagger.SysUISingleton 22 import com.android.systemui.keyguard.shared.model.KeyguardState 23 import com.android.systemui.keyguard.shared.model.TransitionInfo 24 import com.android.systemui.keyguard.shared.model.TransitionState 25 import com.android.systemui.keyguard.shared.model.TransitionStep 26 import dagger.Binds 27 import dagger.Module 28 import java.util.UUID 29 import javax.inject.Inject 30 import junit.framework.Assert.fail 31 import kotlinx.coroutines.CoroutineScope 32 import kotlinx.coroutines.Job 33 import kotlinx.coroutines.channels.BufferOverflow 34 import kotlinx.coroutines.flow.MutableSharedFlow 35 import kotlinx.coroutines.flow.MutableStateFlow 36 import kotlinx.coroutines.flow.SharedFlow 37 import kotlinx.coroutines.flow.asStateFlow 38 import kotlinx.coroutines.launch 39 import kotlinx.coroutines.test.TestCoroutineScheduler 40 import kotlinx.coroutines.test.TestScope 41 import kotlinx.coroutines.test.runCurrent 42 43 /** 44 * Fake implementation of [KeyguardTransitionRepository]. 45 * 46 * By default, will be seeded with a transition from OFF -> LOCKSCREEN, which is the most common 47 * case. If the lockscreen is disabled, or we're in setup wizard, the repository will initialize 48 * with OFF -> GONE. Construct with initInLockscreen = false if your test requires this behavior. 49 */ 50 @SysUISingleton 51 class FakeKeyguardTransitionRepository( 52 private val initInLockscreen: Boolean = true, 53 ) : KeyguardTransitionRepository { 54 private val _transitions = 55 MutableSharedFlow<TransitionStep>(replay = 3, onBufferOverflow = BufferOverflow.DROP_OLDEST) 56 override val transitions: SharedFlow<TransitionStep> = _transitions 57 58 @Inject constructor() : this(initInLockscreen = true) 59 60 private val _currentTransitionInfo: MutableStateFlow<TransitionInfo> = 61 MutableStateFlow( 62 TransitionInfo( 63 ownerName = "", 64 from = KeyguardState.OFF, 65 to = KeyguardState.LOCKSCREEN, 66 animator = null 67 ) 68 ) 69 override var currentTransitionInfoInternal = _currentTransitionInfo.asStateFlow() 70 71 init { 72 // Seed with a FINISHED transition in OFF, same as the real repository. 73 _transitions.tryEmit( 74 TransitionStep( 75 KeyguardState.OFF, 76 KeyguardState.OFF, 77 1f, 78 TransitionState.FINISHED, 79 ) 80 ) 81 82 if (initInLockscreen) { 83 tryEmitInitialStepsFromOff(KeyguardState.LOCKSCREEN) 84 } else { 85 tryEmitInitialStepsFromOff(KeyguardState.OFF) 86 } 87 } 88 89 /** 90 * Sends TransitionSteps between [from] and [to], calling [runCurrent] after each step. 91 * 92 * By default, sends steps through FINISHED (STARTED, RUNNING, FINISHED) but can be halted part 93 * way using [throughTransitionState]. 94 */ 95 suspend fun sendTransitionSteps( 96 from: KeyguardState, 97 to: KeyguardState, 98 testScope: TestScope, 99 throughTransitionState: TransitionState = TransitionState.FINISHED, 100 ) { 101 sendTransitionSteps(from, to, testScope.testScheduler, throughTransitionState) 102 } 103 104 /** 105 * Sends TransitionSteps between [from] and [to], calling [runCurrent] after each step. 106 * 107 * By default, sends steps through FINISHED (STARTED, RUNNING, FINISHED) but can be halted part 108 * way using [throughTransitionState]. 109 */ 110 suspend fun sendTransitionSteps( 111 from: KeyguardState, 112 to: KeyguardState, 113 testScheduler: TestCoroutineScheduler, 114 throughTransitionState: TransitionState = TransitionState.FINISHED, 115 ) { 116 sendTransitionStep( 117 step = 118 TransitionStep( 119 transitionState = TransitionState.STARTED, 120 from = from, 121 to = to, 122 value = 0f, 123 ) 124 ) 125 testScheduler.runCurrent() 126 127 if ( 128 throughTransitionState == TransitionState.RUNNING || 129 throughTransitionState == TransitionState.FINISHED 130 ) { 131 sendTransitionStep( 132 step = 133 TransitionStep( 134 transitionState = TransitionState.RUNNING, 135 from = from, 136 to = to, 137 value = 0.5f 138 ) 139 ) 140 testScheduler.runCurrent() 141 142 sendTransitionStep( 143 step = 144 TransitionStep( 145 transitionState = TransitionState.RUNNING, 146 from = from, 147 to = to, 148 value = 1f 149 ) 150 ) 151 testScheduler.runCurrent() 152 } 153 154 if (throughTransitionState == TransitionState.FINISHED) { 155 sendTransitionStep( 156 step = 157 TransitionStep( 158 transitionState = TransitionState.FINISHED, 159 from = from, 160 to = to, 161 value = 1f, 162 ) 163 ) 164 testScheduler.runCurrent() 165 } 166 } 167 168 suspend fun sendTransitionStep(step: TransitionStep, validateStep: Boolean = true) { 169 this.sendTransitionStep( 170 step = step, 171 validateStep = validateStep, 172 ownerName = step.ownerName 173 ) 174 } 175 176 /** 177 * Directly emits the provided TransitionStep, which can be useful in tests for testing behavior 178 * during specific phases of a transition (such as asserting values while a transition has 179 * STARTED but not FINISHED). 180 * 181 * WARNING: You can get the transition repository into undefined states using this method - for 182 * example, you could send a FINISHED step to LOCKSCREEN having never sent a STARTED step. This 183 * can get flows that combine startedStep/finishedStep into a bad state. 184 * 185 * If you are just trying to get the transition repository FINISHED in a certain state, use 186 * [sendTransitionSteps] - this will send STARTED, RUNNING, and FINISHED steps for you which 187 * ensures that [KeyguardTransitionInteractor] flows will be in the correct state. 188 * 189 * If you're testing something involving transitions themselves and are sure you want to send 190 * only a FINISHED step, override [validateStep]. 191 */ 192 suspend fun sendTransitionStep( 193 from: KeyguardState = KeyguardState.OFF, 194 to: KeyguardState = KeyguardState.OFF, 195 value: Float = 0f, 196 transitionState: TransitionState = TransitionState.FINISHED, 197 ownerName: String = "", 198 step: TransitionStep = 199 TransitionStep( 200 from = from, 201 to = to, 202 value = value, 203 transitionState = transitionState, 204 ownerName = ownerName 205 ), 206 validateStep: Boolean = true 207 ) { 208 if (step.transitionState == TransitionState.STARTED) { 209 _currentTransitionInfo.value = 210 TransitionInfo(from = step.from, to = step.to, animator = null, ownerName = "") 211 } 212 213 _transitions.replayCache.last().let { lastStep -> 214 if ( 215 validateStep && 216 step.transitionState == TransitionState.FINISHED && 217 !(lastStep.transitionState == TransitionState.STARTED || 218 lastStep.transitionState == TransitionState.RUNNING) 219 ) { 220 fail( 221 "Attempted to send a FINISHED TransitionStep without a prior " + 222 "STARTED/RUNNING step. This leaves the FakeKeyguardTransitionRepository " + 223 "in an undefined state and should not be done. Pass " + 224 "allowInvalidStep=true to sendTransitionStep if you are trying to test " + 225 "this specific and" + 226 "incorrect state." 227 ) 228 } 229 } 230 _transitions.emit(step) 231 } 232 233 /** Version of [sendTransitionStep] that's usable from Java tests. */ 234 fun sendTransitionStepJava( 235 coroutineScope: CoroutineScope, 236 step: TransitionStep, 237 validateStep: Boolean = true 238 ): Job { 239 return coroutineScope.launch { 240 sendTransitionStep(step = step, validateStep = validateStep) 241 } 242 } 243 244 suspend fun sendTransitionSteps( 245 steps: List<TransitionStep>, 246 testScope: TestScope, 247 validateSteps: Boolean = true 248 ) { 249 steps.forEach { 250 sendTransitionStep(step = it, validateStep = validateSteps) 251 testScope.testScheduler.runCurrent() 252 } 253 } 254 255 override suspend fun startTransition(info: TransitionInfo): UUID? { 256 _currentTransitionInfo.value = info 257 return if (info.animator == null) UUID.randomUUID() else null 258 } 259 260 override suspend fun emitInitialStepsFromOff(to: KeyguardState) { 261 tryEmitInitialStepsFromOff(to) 262 } 263 264 private fun tryEmitInitialStepsFromOff(to: KeyguardState) { 265 _transitions.tryEmit( 266 TransitionStep( 267 KeyguardState.OFF, 268 to, 269 0f, 270 TransitionState.STARTED, 271 ownerName = "KeyguardTransitionRepository(boot)", 272 ) 273 ) 274 275 _transitions.tryEmit( 276 TransitionStep( 277 KeyguardState.OFF, 278 to, 279 1f, 280 TransitionState.FINISHED, 281 ownerName = "KeyguardTransitionRepository(boot)", 282 ), 283 ) 284 } 285 286 override fun updateTransition( 287 transitionId: UUID, 288 @FloatRange(from = 0.0, to = 1.0) value: Float, 289 state: TransitionState 290 ) = Unit 291 } 292 293 @Module 294 interface FakeKeyguardTransitionRepositoryModule { bindFakenull295 @Binds fun bindFake(fake: FakeKeyguardTransitionRepository): KeyguardTransitionRepository 296 } 297