| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| package com.android.systemui.keyguard.data.repository |
| |
| import android.animation.Animator |
| import android.animation.AnimatorListenerAdapter |
| import android.animation.ValueAnimator |
| import android.animation.ValueAnimator.AnimatorUpdateListener |
| import android.annotation.FloatRange |
| import android.os.Trace |
| import android.util.Log |
| import com.android.systemui.dagger.SysUISingleton |
| import com.android.systemui.keyguard.shared.model.KeyguardState |
| import com.android.systemui.keyguard.shared.model.TransitionInfo |
| import com.android.systemui.keyguard.shared.model.TransitionState |
| import com.android.systemui.keyguard.shared.model.TransitionStep |
| import java.util.UUID |
| import javax.inject.Inject |
| import kotlinx.coroutines.channels.BufferOverflow |
| import kotlinx.coroutines.flow.Flow |
| import kotlinx.coroutines.flow.MutableSharedFlow |
| import kotlinx.coroutines.flow.asSharedFlow |
| import kotlinx.coroutines.flow.distinctUntilChanged |
| import kotlinx.coroutines.flow.filter |
| |
| /** |
| * The source of truth for all keyguard transitions. |
| * |
| * While the keyguard component is visible, it can undergo a number of transitions between different |
| * UI screens, such as AOD (Always-on Display), Bouncer, and others mentioned in [KeyguardState]. |
| * These UI elements should listen to events emitted by [transitions], to ensure a centrally |
| * coordinated experience. |
| * |
| * To create or modify logic that controls when and how transitions get created, look at |
| * [TransitionInteractor]. These interactors will call [startTransition] and [updateTransition] on |
| * this repository. |
| */ |
| interface KeyguardTransitionRepository { |
| /** |
| * All events regarding transitions, as they start, run, and complete. [TransitionStep#value] is |
| * a float between [0, 1] representing progress towards completion. If this is a user driven |
| * transition, that value may not be a monotonic progression, as the user may swipe in any |
| * direction. |
| */ |
| val transitions: Flow<TransitionStep> |
| |
| /** |
| * Interactors that require information about changes between [KeyguardState]s will call this to |
| * register themselves for flowable [TransitionStep]s when that transition occurs. |
| */ |
| fun transition(from: KeyguardState, to: KeyguardState): Flow<TransitionStep> { |
| return transitions.filter { step -> step.from == from && step.to == to } |
| } |
| |
| /** |
| * Begin a transition from one state to another. Transitions are interruptible, and will issue a |
| * [TransitionStep] with state = [TransitionState.CANCELED] before beginning the next one. |
| * |
| * When canceled, there are two options: to continue from the current position of the prior |
| * transition, or to reset the position. When [resetIfCanceled] == true, it will do the latter. |
| */ |
| fun startTransition(info: TransitionInfo, resetIfCanceled: Boolean = false): UUID? |
| |
| /** |
| * Allows manual control of a transition. When calling [startTransition], the consumer must pass |
| * in a null animator. In return, it will get a unique [UUID] that will be validated to allow |
| * further updates. |
| * |
| * When the transition is over, TransitionState.FINISHED must be passed into the [state] |
| * parameter. |
| */ |
| fun updateTransition( |
| transitionId: UUID, |
| @FloatRange(from = 0.0, to = 1.0) value: Float, |
| state: TransitionState |
| ) |
| } |
| |
| @SysUISingleton |
| class KeyguardTransitionRepositoryImpl @Inject constructor() : KeyguardTransitionRepository { |
| /* |
| * Each transition between [KeyguardState]s will have an associated Flow. |
| * In order to collect these events, clients should call [transition]. |
| */ |
| private val _transitions = |
| MutableSharedFlow<TransitionStep>( |
| replay = 2, |
| extraBufferCapacity = 10, |
| onBufferOverflow = BufferOverflow.DROP_OLDEST, |
| ) |
| override val transitions = _transitions.asSharedFlow().distinctUntilChanged() |
| private var lastStep: TransitionStep = TransitionStep() |
| private var lastAnimator: ValueAnimator? = null |
| |
| /* |
| * When manual control of the transition is requested, a unique [UUID] is used as the handle |
| * to permit calls to [updateTransition] |
| */ |
| private var updateTransitionId: UUID? = null |
| |
| init { |
| // Seed with transitions signaling a boot into lockscreen state |
| emitTransition( |
| TransitionStep( |
| KeyguardState.OFF, |
| KeyguardState.LOCKSCREEN, |
| 0f, |
| TransitionState.STARTED, |
| KeyguardTransitionRepositoryImpl::class.simpleName!!, |
| ) |
| ) |
| emitTransition( |
| TransitionStep( |
| KeyguardState.OFF, |
| KeyguardState.LOCKSCREEN, |
| 1f, |
| TransitionState.FINISHED, |
| KeyguardTransitionRepositoryImpl::class.simpleName!!, |
| ) |
| ) |
| } |
| |
| override fun startTransition( |
| info: TransitionInfo, |
| resetIfCanceled: Boolean, |
| ): UUID? { |
| if (lastStep.from == info.from && lastStep.to == info.to) { |
| Log.i(TAG, "Duplicate call to start the transition, rejecting: $info") |
| return null |
| } |
| val startingValue = |
| if (lastStep.transitionState != TransitionState.FINISHED) { |
| Log.i(TAG, "Transition still active: $lastStep, canceling") |
| if (resetIfCanceled) { |
| 0f |
| } else { |
| lastStep.value |
| } |
| } else { |
| 0f |
| } |
| |
| lastAnimator?.cancel() |
| lastAnimator = info.animator |
| |
| info.animator?.let { animator -> |
| // An animator was provided, so use it to run the transition |
| animator.setFloatValues(startingValue, 1f) |
| animator.duration = ((1f - startingValue) * animator.duration).toLong() |
| val updateListener = |
| object : AnimatorUpdateListener { |
| override fun onAnimationUpdate(animation: ValueAnimator) { |
| emitTransition( |
| TransitionStep( |
| info, |
| (animation.getAnimatedValue() as Float), |
| TransitionState.RUNNING |
| ) |
| ) |
| } |
| } |
| val adapter = |
| object : AnimatorListenerAdapter() { |
| override fun onAnimationStart(animation: Animator) { |
| emitTransition(TransitionStep(info, startingValue, TransitionState.STARTED)) |
| } |
| override fun onAnimationCancel(animation: Animator) { |
| endAnimation(animation, lastStep.value, TransitionState.CANCELED) |
| } |
| override fun onAnimationEnd(animation: Animator) { |
| endAnimation(animation, 1f, TransitionState.FINISHED) |
| } |
| |
| private fun endAnimation( |
| animation: Animator, |
| value: Float, |
| state: TransitionState |
| ) { |
| emitTransition(TransitionStep(info, value, state)) |
| animator.removeListener(this) |
| animator.removeUpdateListener(updateListener) |
| lastAnimator = null |
| } |
| } |
| animator.addListener(adapter) |
| animator.addUpdateListener(updateListener) |
| animator.start() |
| return@startTransition null |
| } |
| ?: run { |
| emitTransition(TransitionStep(info, 0f, TransitionState.STARTED)) |
| |
| // No animator, so it's manual. Provide a mechanism to callback |
| updateTransitionId = UUID.randomUUID() |
| return@startTransition updateTransitionId |
| } |
| } |
| |
| override fun updateTransition( |
| transitionId: UUID, |
| @FloatRange(from = 0.0, to = 1.0) value: Float, |
| state: TransitionState |
| ) { |
| if (updateTransitionId != transitionId) { |
| Log.wtf(TAG, "Attempting to update with old/invalid transitionId: $transitionId") |
| return |
| } |
| |
| if (state == TransitionState.FINISHED || state == TransitionState.CANCELED) { |
| updateTransitionId = null |
| } |
| |
| val nextStep = lastStep.copy(value = value, transitionState = state) |
| emitTransition(nextStep, isManual = true) |
| } |
| |
| private fun emitTransition(nextStep: TransitionStep, isManual: Boolean = false) { |
| trace(nextStep, isManual) |
| val emitted = _transitions.tryEmit(nextStep) |
| if (!emitted) { |
| Log.w(TAG, "Failed to emit next value without suspending") |
| } |
| lastStep = nextStep |
| } |
| |
| private fun trace(step: TransitionStep, isManual: Boolean) { |
| if (step.transitionState == TransitionState.RUNNING) { |
| return |
| } |
| val traceName = |
| "Transition: ${step.from} -> ${step.to} " + |
| if (isManual) { |
| "(manual)" |
| } else { |
| "" |
| } |
| val traceCookie = traceName.hashCode() |
| if (step.transitionState == TransitionState.STARTED) { |
| Trace.beginAsyncSection(traceName, traceCookie) |
| } else if ( |
| step.transitionState == TransitionState.FINISHED || |
| step.transitionState == TransitionState.CANCELED |
| ) { |
| Trace.endAsyncSection(traceName, traceCookie) |
| } |
| } |
| |
| companion object { |
| private const val TAG = "KeyguardTransitionRepository" |
| } |
| } |