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 package com.android.systemui.keyguard.data.repository 17 18 import android.animation.Animator 19 import android.animation.AnimatorListenerAdapter 20 import android.animation.ValueAnimator 21 import android.animation.ValueAnimator.AnimatorUpdateListener 22 import android.annotation.FloatRange 23 import android.annotation.SuppressLint 24 import android.os.Trace 25 import android.util.Log 26 import com.android.app.animation.Interpolators 27 import com.android.app.tracing.coroutines.flow.traceAs 28 import com.android.app.tracing.coroutines.withContextTraced as withContext 29 import com.android.systemui.Flags.transitionRaceCondition 30 import com.android.systemui.dagger.SysUISingleton 31 import com.android.systemui.dagger.qualifiers.Main 32 import com.android.systemui.keyguard.shared.model.KeyguardState 33 import com.android.systemui.keyguard.shared.model.TransitionInfo 34 import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled 35 import com.android.systemui.keyguard.shared.model.TransitionState 36 import com.android.systemui.keyguard.shared.model.TransitionStep 37 import com.android.systemui.keyguard.shared.transition.KeyguardTransitionAnimationCallback 38 import java.util.UUID 39 import javax.inject.Inject 40 import kotlinx.coroutines.CoroutineDispatcher 41 import kotlinx.coroutines.channels.BufferOverflow 42 import kotlinx.coroutines.delay 43 import kotlinx.coroutines.flow.Flow 44 import kotlinx.coroutines.flow.MutableSharedFlow 45 import kotlinx.coroutines.flow.MutableStateFlow 46 import kotlinx.coroutines.flow.StateFlow 47 import kotlinx.coroutines.flow.asStateFlow 48 import kotlinx.coroutines.flow.distinctUntilChanged 49 import kotlinx.coroutines.flow.filter 50 import kotlinx.coroutines.sync.Mutex 51 52 /** 53 * The source of truth for all keyguard transitions. 54 * 55 * While the keyguard component is visible, it can undergo a number of transitions between different 56 * UI screens, such as AOD (Always-on Display), Bouncer, and others mentioned in [KeyguardState]. 57 * These UI elements should listen to events emitted by [transitions], to ensure a centrally 58 * coordinated experience. 59 * 60 * To create or modify logic that controls when and how transitions get created, look at 61 * [TransitionInteractor]. These interactors will call [startTransition] and [updateTransition] on 62 * this repository. 63 * 64 * To print all transitions to logcat to help with debugging, run this command: 65 * ``` 66 * adb shell cmd statusbar echo -b KeyguardLog:VERBOSE 67 * ``` 68 * 69 * This will print all keyguard transitions to logcat with the KeyguardTransitionAuditLogger tag. 70 */ 71 interface KeyguardTransitionRepository { 72 /** 73 * All events regarding transitions, as they start, run, and complete. [TransitionStep#value] is 74 * a float between [0, 1] representing progress towards completion. If this is a user driven 75 * transition, that value may not be a monotonic progression, as the user may swipe in any 76 * direction. 77 */ 78 val transitions: Flow<TransitionStep> 79 80 /** The [TransitionInfo] of the most recent call to [startTransition]. */ 81 val currentTransitionInfoInternal: StateFlow<TransitionInfo> 82 /** The [TransitionInfo] of the most recent call to [startTransition]. */ 83 val currentTransitionInfo: TransitionInfo 84 85 /** 86 * Interactors that require information about changes between [KeyguardState]s will call this to 87 * register themselves for flowable [TransitionStep]s when that transition occurs. 88 */ 89 fun transition(from: KeyguardState, to: KeyguardState): Flow<TransitionStep> { 90 return transitions.filter { step -> step.from == from && step.to == to } 91 } 92 93 /** 94 * Begin a transition from one state to another. Transitions are interruptible, and will issue a 95 * [TransitionStep] with state = [TransitionState.CANCELED] before beginning the next one. 96 */ 97 suspend fun startTransition(info: TransitionInfo): UUID? 98 99 /** 100 * Emits STARTED and FINISHED transition steps to the given state. This is used during boot to 101 * seed the repository with the appropriate initial state. 102 */ 103 suspend fun emitInitialStepsFromOff(to: KeyguardState, testSetup: Boolean = false) 104 105 /** 106 * Allows manual control of a transition. When calling [startTransition], the consumer must pass 107 * in a null animator. In return, it will get a unique [UUID] that will be validated to allow 108 * further updates. 109 * 110 * When the transition is over, TransitionState.FINISHED must be passed into the [state] 111 * parameter. 112 */ 113 suspend fun updateTransition( 114 transitionId: UUID, 115 @FloatRange(from = 0.0, to = 1.0) value: Float, 116 state: TransitionState, 117 ) 118 119 /** 120 * Forces the current transition to emit FINISHED, foregoing any additional RUNNING steps that 121 * otherwise would have been emitted. 122 * 123 * When the screen is off, upcoming performance changes cause all Animators to cease emitting 124 * frames, which means the Animator passed to [startTransition] will never finish if it was 125 * running when the screen turned off. Also, there's simply no reason to emit RUNNING steps when 126 * the screen isn't even on. As long as we emit FINISHED, everything should end up in the 127 * correct state. 128 */ 129 suspend fun forceFinishCurrentTransition() 130 } 131 132 @SysUISingleton 133 class KeyguardTransitionRepositoryImpl 134 @Inject 135 constructor( 136 @Main private val mainDispatcher: CoroutineDispatcher, 137 private val transitionCallback: KeyguardTransitionAnimationCallback, 138 ) : KeyguardTransitionRepository { 139 /** 140 * Each transition between [KeyguardState]s will have an associated Flow. In order to collect 141 * these events, clients should call [transition]. 142 */ 143 @SuppressLint("SharedFlowCreation") 144 private val _transitions = 145 MutableSharedFlow<TransitionStep>( 146 replay = 2, 147 extraBufferCapacity = 20, 148 onBufferOverflow = BufferOverflow.DROP_OLDEST, 149 ) 150 .traceAs("KTR-transitions") 151 override val transitions = _transitions.distinctUntilChanged() 152 private var lastStep: TransitionStep = TransitionStep() 153 private var lastAnimator: ValueAnimator? = null 154 private var animatorListener: AnimatorListenerAdapter? = null 155 156 private val withContextMutex = Mutex() 157 private val _currentTransitionInfo: MutableStateFlow<TransitionInfo> = 158 MutableStateFlow( 159 TransitionInfo( 160 ownerName = "", 161 from = KeyguardState.OFF, 162 to = KeyguardState.OFF, 163 animator = null, 164 ) 165 ) 166 override var currentTransitionInfoInternal = _currentTransitionInfo.asStateFlow() 167 168 @Volatile 169 override var currentTransitionInfo: TransitionInfo = 170 TransitionInfo( 171 ownerName = "", 172 from = KeyguardState.OFF, 173 to = KeyguardState.OFF, 174 animator = null, 175 ) 176 private set 177 178 /* 179 * When manual control of the transition is requested, a unique [UUID] is used as the handle 180 * to permit calls to [updateTransition] 181 */ 182 private var updateTransitionId: UUID? = null 183 184 // Only used in a test environment 185 var forceDelayForRaceConditionTest = false 186 187 init { 188 // Start with a FINISHED transition in OFF. KeyguardBootInteractor will transition from OFF 189 // to either GONE or LOCKSCREEN once we're booted up and can determine which state we should 190 // start in. 191 emitTransition( 192 TransitionStep(KeyguardState.OFF, KeyguardState.OFF, 1f, TransitionState.FINISHED) 193 ) 194 } 195 startTransitionnull196 override suspend fun startTransition(info: TransitionInfo): UUID? { 197 if (transitionRaceCondition()) { 198 currentTransitionInfo = info 199 } else { 200 _currentTransitionInfo.value = info 201 } 202 Log.d(TAG, "(Internal) Setting current transition info: $info") 203 204 // There is no fairness guarantee with 'withContext', which means that transitions could 205 // be processed out of order. Use a Mutex to guarantee ordering. [updateTransition] 206 // requires the same lock 207 withContextMutex.lock() 208 // Only used in a test environment 209 if (forceDelayForRaceConditionTest) { 210 delay(50L) 211 } 212 213 // Animators must be started on the main thread. 214 return withContext("$TAG#startTransition", mainDispatcher) { 215 withContextMutex.unlock() 216 if (lastStep.from == info.from && lastStep.to == info.to) { 217 Log.i(TAG, "Duplicate call to start the transition, rejecting: $info") 218 return@withContext null 219 } 220 val isAnimatorRunning = lastAnimator?.isRunning() ?: false 221 val isManualTransitionRunning = 222 updateTransitionId != null && lastStep.transitionState != TransitionState.FINISHED 223 val startingValue = 224 if (isAnimatorRunning || isManualTransitionRunning) { 225 Log.i(TAG, "Transition still active: $lastStep, canceling") 226 when (info.modeOnCanceled) { 227 TransitionModeOnCanceled.LAST_VALUE -> lastStep.value 228 TransitionModeOnCanceled.RESET -> 0f 229 TransitionModeOnCanceled.REVERSE -> 1f - lastStep.value 230 } 231 } else { 232 0f 233 } 234 235 lastAnimator?.cancel() 236 lastAnimator = info.animator 237 238 // Cancel any existing manual transitions 239 updateTransitionId?.let { uuid -> 240 updateTransitionInternal(uuid, lastStep.value, TransitionState.CANCELED) 241 } 242 243 info.animator?.let { animator -> 244 // An animator was provided, so use it to run the transition 245 animator.setFloatValues(startingValue, 1f) 246 animator.duration = ((1f - startingValue) * animator.duration).toLong() 247 val updateListener = AnimatorUpdateListener { animation -> 248 emitTransition( 249 TransitionStep( 250 info, 251 (animation.animatedValue as Float), 252 TransitionState.RUNNING, 253 ) 254 ) 255 } 256 257 animatorListener = 258 object : AnimatorListenerAdapter() { 259 override fun onAnimationStart(animation: Animator) { 260 transitionCallback.onAnimationStarted(info.from, info.to) 261 emitTransition( 262 TransitionStep(info, startingValue, TransitionState.STARTED) 263 ) 264 } 265 266 override fun onAnimationCancel(animation: Animator) { 267 transitionCallback.onAnimationCanceled(info.from, info.to) 268 endAnimation(lastStep.value, TransitionState.CANCELED) 269 } 270 271 override fun onAnimationEnd(animation: Animator) { 272 transitionCallback.onAnimationEnded(info.from, info.to) 273 endAnimation(1f, TransitionState.FINISHED) 274 } 275 276 private fun endAnimation(value: Float, state: TransitionState) { 277 emitTransition(TransitionStep(info, value, state)) 278 animator.removeListener(this) 279 animator.removeUpdateListener(updateListener) 280 lastAnimator = null 281 animatorListener = null 282 } 283 } 284 animator.addListener(animatorListener) 285 animator.addUpdateListener(updateListener) 286 animator.start() 287 return@withContext null 288 } 289 ?: run { 290 emitTransition( 291 nextStep = TransitionStep(info, startingValue, TransitionState.STARTED), 292 isManual = true, 293 ) 294 295 // No animator, so it's manual. Provide a mechanism to callback 296 updateTransitionId = UUID.randomUUID() 297 return@withContext updateTransitionId 298 } 299 } 300 } 301 updateTransitionnull302 override suspend fun updateTransition( 303 transitionId: UUID, 304 @FloatRange(from = 0.0, to = 1.0) value: Float, 305 state: TransitionState, 306 ) { 307 // There is no fairness guarantee with 'withContext', which means that transitions could 308 // be processed out of order. Use a Mutex to guarantee ordering. [startTransition] 309 // requires the same lock 310 withContextMutex.lock() 311 withContext("$TAG#updateTransition", mainDispatcher) { 312 withContextMutex.unlock() 313 314 updateTransitionInternal(transitionId, value, state) 315 } 316 } 317 forceFinishCurrentTransitionnull318 override suspend fun forceFinishCurrentTransition() { 319 if (lastAnimator?.isRunning != true) { 320 return 321 } 322 323 withContextMutex.lock() 324 325 return withContext("$TAG#forceFinishCurrentTransition", mainDispatcher) { 326 withContextMutex.unlock() 327 328 Log.d(TAG, "forceFinishCurrentTransition() - emitting FINISHED early.") 329 330 lastAnimator?.apply { 331 // Cancel the animator, but remove listeners first so we don't emit CANCELED. 332 removeAllListeners() 333 cancel() 334 335 // Emit a final 1f RUNNING step to ensure that any transitions not listening for a 336 // FINISHED step end up in the right end state. 337 emitTransition(TransitionStep(currentTransitionInfo, 1f, TransitionState.RUNNING)) 338 339 // Ask the listener to emit FINISHED and clean up its state. 340 animatorListener?.onAnimationEnd(this) 341 } 342 } 343 } 344 updateTransitionInternalnull345 private suspend fun updateTransitionInternal( 346 transitionId: UUID, 347 @FloatRange(from = 0.0, to = 1.0) value: Float, 348 state: TransitionState, 349 ) { 350 if (updateTransitionId != transitionId) { 351 Log.e(TAG, "Attempting to update with old/invalid transitionId: $transitionId") 352 return 353 } 354 355 if (state == TransitionState.FINISHED || state == TransitionState.CANCELED) { 356 updateTransitionId = null 357 } 358 359 val nextStep = lastStep.copy(value = value, transitionState = state) 360 emitTransition(nextStep, isManual = true) 361 } 362 emitTransitionnull363 private fun emitTransition(nextStep: TransitionStep, isManual: Boolean = false) { 364 logAndTrace(nextStep, isManual) 365 _transitions.tryEmit(nextStep) 366 lastStep = nextStep 367 } 368 emitInitialStepsFromOffnull369 override suspend fun emitInitialStepsFromOff(to: KeyguardState, testSetup: Boolean) { 370 val ownerName = "KeyguardTransitionRepository(boot)" 371 // Tests runs on testDispatcher, which is not the main thread, causing the animator thread 372 // check to fail 373 if (testSetup) { 374 if (transitionRaceCondition()) { 375 currentTransitionInfo = 376 TransitionInfo( 377 ownerName = ownerName, 378 from = KeyguardState.OFF, 379 to = to, 380 animator = null, 381 ) 382 } else { 383 _currentTransitionInfo.value = 384 TransitionInfo( 385 ownerName = ownerName, 386 from = KeyguardState.OFF, 387 to = to, 388 animator = null, 389 ) 390 } 391 emitTransition( 392 TransitionStep( 393 KeyguardState.OFF, 394 to, 395 0f, 396 TransitionState.STARTED, 397 ownerName = ownerName, 398 ) 399 ) 400 401 emitTransition( 402 TransitionStep( 403 KeyguardState.OFF, 404 to, 405 1f, 406 TransitionState.FINISHED, 407 ownerName = ownerName, 408 ) 409 ) 410 } else { 411 startTransition( 412 TransitionInfo( 413 ownerName = ownerName, 414 from = KeyguardState.OFF, 415 to = to, 416 animator = 417 ValueAnimator().apply { 418 interpolator = Interpolators.LINEAR 419 duration = 933L 420 }, 421 ) 422 ) 423 } 424 } 425 logAndTracenull426 private fun logAndTrace(step: TransitionStep, isManual: Boolean) { 427 if (step.transitionState == TransitionState.RUNNING) { 428 return 429 } 430 val manualStr = if (isManual) " (manual)" else "" 431 val traceName = "Transition: ${step.from} -> ${step.to}$manualStr" 432 433 val traceCookie = traceName.hashCode() 434 when (step.transitionState) { 435 TransitionState.STARTED -> Trace.beginAsyncSection(traceName, traceCookie) 436 TransitionState.FINISHED -> Trace.endAsyncSection(traceName, traceCookie) 437 TransitionState.CANCELED -> Trace.endAsyncSection(traceName, traceCookie) 438 else -> {} 439 } 440 441 Log.i(TAG, "${step.transitionState.name} transition: $step$manualStr") 442 } 443 444 companion object { 445 private const val TAG = "KeyguardTransitionRepository" 446 } 447 } 448