1 /* 2 * 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.os.Trace 24 import android.util.Log 25 import com.android.systemui.dagger.SysUISingleton 26 import com.android.systemui.keyguard.shared.model.KeyguardState 27 import com.android.systemui.keyguard.shared.model.TransitionInfo 28 import com.android.systemui.keyguard.shared.model.TransitionState 29 import com.android.systemui.keyguard.shared.model.TransitionStep 30 import java.util.UUID 31 import javax.inject.Inject 32 import kotlinx.coroutines.channels.BufferOverflow 33 import kotlinx.coroutines.flow.Flow 34 import kotlinx.coroutines.flow.MutableSharedFlow 35 import kotlinx.coroutines.flow.asSharedFlow 36 import kotlinx.coroutines.flow.distinctUntilChanged 37 import kotlinx.coroutines.flow.filter 38 39 /** 40 * The source of truth for all keyguard transitions. 41 * 42 * While the keyguard component is visible, it can undergo a number of transitions between different 43 * UI screens, such as AOD (Always-on Display), Bouncer, and others mentioned in [KeyguardState]. 44 * These UI elements should listen to events emitted by [transitions], to ensure a centrally 45 * coordinated experience. 46 * 47 * To create or modify logic that controls when and how transitions get created, look at 48 * [TransitionInteractor]. These interactors will call [startTransition] and [updateTransition] on 49 * this repository. 50 * 51 * To print all transitions to logcat to help with debugging, run this command: 52 * adb shell settings put global systemui/buffer/KeyguardLog VERBOSE 53 * 54 * This will print all keyguard transitions to logcat with the KeyguardTransitionAuditLogger tag. 55 */ 56 interface KeyguardTransitionRepository { 57 /** 58 * All events regarding transitions, as they start, run, and complete. [TransitionStep#value] is 59 * a float between [0, 1] representing progress towards completion. If this is a user driven 60 * transition, that value may not be a monotonic progression, as the user may swipe in any 61 * direction. 62 */ 63 val transitions: Flow<TransitionStep> 64 65 /** 66 * Interactors that require information about changes between [KeyguardState]s will call this to 67 * register themselves for flowable [TransitionStep]s when that transition occurs. 68 */ 69 fun transition(from: KeyguardState, to: KeyguardState): Flow<TransitionStep> { 70 return transitions.filter { step -> step.from == from && step.to == to } 71 } 72 73 /** 74 * Begin a transition from one state to another. Transitions are interruptible, and will issue a 75 * [TransitionStep] with state = [TransitionState.CANCELED] before beginning the next one. 76 * 77 * When canceled, there are two options: to continue from the current position of the prior 78 * transition, or to reset the position. When [resetIfCanceled] == true, it will do the latter. 79 */ 80 fun startTransition(info: TransitionInfo, resetIfCanceled: Boolean = false): UUID? 81 82 /** 83 * Allows manual control of a transition. When calling [startTransition], the consumer must pass 84 * in a null animator. In return, it will get a unique [UUID] that will be validated to allow 85 * further updates. 86 * 87 * When the transition is over, TransitionState.FINISHED must be passed into the [state] 88 * parameter. 89 */ 90 fun updateTransition( 91 transitionId: UUID, 92 @FloatRange(from = 0.0, to = 1.0) value: Float, 93 state: TransitionState 94 ) 95 } 96 97 @SysUISingleton 98 class KeyguardTransitionRepositoryImpl @Inject constructor() : KeyguardTransitionRepository { 99 /* 100 * Each transition between [KeyguardState]s will have an associated Flow. 101 * In order to collect these events, clients should call [transition]. 102 */ 103 private val _transitions = 104 MutableSharedFlow<TransitionStep>( 105 replay = 2, 106 extraBufferCapacity = 10, 107 onBufferOverflow = BufferOverflow.DROP_OLDEST, 108 ) 109 override val transitions = _transitions.asSharedFlow().distinctUntilChanged() 110 private var lastStep: TransitionStep = TransitionStep() 111 private var lastAnimator: ValueAnimator? = null 112 113 /* 114 * When manual control of the transition is requested, a unique [UUID] is used as the handle 115 * to permit calls to [updateTransition] 116 */ 117 private var updateTransitionId: UUID? = null 118 119 init { 120 // Seed with transitions signaling a boot into lockscreen state 121 emitTransition( 122 TransitionStep( 123 KeyguardState.OFF, 124 KeyguardState.LOCKSCREEN, 125 0f, 126 TransitionState.STARTED, 127 KeyguardTransitionRepositoryImpl::class.simpleName!!, 128 ) 129 ) 130 emitTransition( 131 TransitionStep( 132 KeyguardState.OFF, 133 KeyguardState.LOCKSCREEN, 134 1f, 135 TransitionState.FINISHED, 136 KeyguardTransitionRepositoryImpl::class.simpleName!!, 137 ) 138 ) 139 } 140 141 override fun startTransition( 142 info: TransitionInfo, 143 resetIfCanceled: Boolean, 144 ): UUID? { 145 if (lastStep.from == info.from && lastStep.to == info.to) { 146 Log.i(TAG, "Duplicate call to start the transition, rejecting: $info") 147 return null 148 } 149 val startingValue = 150 if (lastStep.transitionState != TransitionState.FINISHED) { 151 Log.i(TAG, "Transition still active: $lastStep, canceling") 152 if (resetIfCanceled) { 153 0f 154 } else { 155 lastStep.value 156 } 157 } else { 158 0f 159 } 160 161 lastAnimator?.cancel() 162 lastAnimator = info.animator 163 164 info.animator?.let { animator -> 165 // An animator was provided, so use it to run the transition 166 animator.setFloatValues(startingValue, 1f) 167 animator.duration = ((1f - startingValue) * animator.duration).toLong() 168 val updateListener = AnimatorUpdateListener { animation -> 169 emitTransition( 170 TransitionStep( 171 info, 172 (animation.animatedValue as Float), 173 TransitionState.RUNNING 174 ) 175 ) 176 } 177 val adapter = 178 object : AnimatorListenerAdapter() { 179 override fun onAnimationStart(animation: Animator) { 180 emitTransition(TransitionStep(info, startingValue, TransitionState.STARTED)) 181 } 182 override fun onAnimationCancel(animation: Animator) { 183 endAnimation(lastStep.value, TransitionState.CANCELED) 184 } 185 override fun onAnimationEnd(animation: Animator) { 186 endAnimation(1f, TransitionState.FINISHED) 187 } 188 189 private fun endAnimation(value: Float, state: TransitionState) { 190 emitTransition(TransitionStep(info, value, state)) 191 animator.removeListener(this) 192 animator.removeUpdateListener(updateListener) 193 lastAnimator = null 194 } 195 } 196 animator.addListener(adapter) 197 animator.addUpdateListener(updateListener) 198 animator.start() 199 return@startTransition null 200 } 201 ?: run { 202 emitTransition(TransitionStep(info, startingValue, TransitionState.STARTED)) 203 204 // No animator, so it's manual. Provide a mechanism to callback 205 updateTransitionId = UUID.randomUUID() 206 return@startTransition updateTransitionId 207 } 208 } 209 210 override fun updateTransition( 211 transitionId: UUID, 212 @FloatRange(from = 0.0, to = 1.0) value: Float, 213 state: TransitionState 214 ) { 215 if (updateTransitionId != transitionId) { 216 Log.wtf(TAG, "Attempting to update with old/invalid transitionId: $transitionId") 217 return 218 } 219 220 if (state == TransitionState.FINISHED || state == TransitionState.CANCELED) { 221 updateTransitionId = null 222 } 223 224 val nextStep = lastStep.copy(value = value, transitionState = state) 225 emitTransition(nextStep, isManual = true) 226 } 227 228 private fun emitTransition(nextStep: TransitionStep, isManual: Boolean = false) { 229 trace(nextStep, isManual) 230 val emitted = _transitions.tryEmit(nextStep) 231 if (!emitted) { 232 Log.w(TAG, "Failed to emit next value without suspending") 233 } 234 lastStep = nextStep 235 } 236 237 private fun trace(step: TransitionStep, isManual: Boolean) { 238 if (step.transitionState == TransitionState.RUNNING) { 239 return 240 } 241 val traceName = 242 "Transition: ${step.from} -> ${step.to} " + 243 if (isManual) { 244 "(manual)" 245 } else { 246 "" 247 } 248 val traceCookie = traceName.hashCode() 249 if (step.transitionState == TransitionState.STARTED) { 250 Trace.beginAsyncSection(traceName, traceCookie) 251 } else if ( 252 step.transitionState == TransitionState.FINISHED || 253 step.transitionState == TransitionState.CANCELED 254 ) { 255 Trace.endAsyncSection(traceName, traceCookie) 256 } 257 } 258 259 companion object { 260 private const val TAG = "KeyguardTransitionRepository" 261 } 262 } 263