Transitions - Add GONE state

When keyguard is dismissed, transition to a new GONE state. This helps
address an issue with the ClockEventController, where the clock was
not properly animating back to its AOD state.

Adding audit capability via KeyguardTransitionAuditLogger

Migrated to MutableSharedFlow. MutableStateFlow was conflating too
many events, some of them fairly important.

Test: atest ClockEventControllerTest KeyguardRepositoryImplTest
KeyguardTransitionRepositoryTest KeyguardTransitionInteractorTest
Bug: 195430376

Change-Id: Ic4bd48cea6e783cfdc116fa2776639da0641c100
This commit is contained in:
Matt Pietal
2022-10-26 20:56:23 +00:00
parent f16bc97e18
commit 02dc8128fa
25 changed files with 794 additions and 175 deletions

View File

@@ -237,6 +237,9 @@ class DefaultClockController(
) {
var isActive: Boolean = fraction < 0.5f
fun update(newFraction: Float): Pair<Boolean, Boolean> {
if (newFraction == fraction) {
return Pair(isActive, false)
}
val wasActive = isActive
val hasJumped =
(fraction == 0f && newFraction == 1f) || (fraction == 1f && newFraction == 0f)

View File

@@ -34,6 +34,7 @@ import com.android.systemui.flags.Flags.DOZING_MIGRATION_1
import com.android.systemui.flags.Flags.REGION_SAMPLING
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.log.dagger.KeyguardClockLog
import com.android.systemui.plugins.ClockController
@@ -52,6 +53,7 @@ import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
/**
@@ -186,8 +188,10 @@ open class ClockEventController @Inject constructor(
private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() {
override fun onKeyguardVisibilityChanged(visible: Boolean) {
isKeyguardVisible = visible
if (!isKeyguardVisible) {
clock?.animations?.doze(if (isDozing) 1f else 0f)
if (!featureFlags.isEnabled(DOZING_MIGRATION_1)) {
if (!isKeyguardVisible) {
clock?.animations?.doze(if (isDozing) 1f else 0f)
}
}
}
@@ -224,6 +228,7 @@ open class ClockEventController @Inject constructor(
listenForDozing(this)
if (featureFlags.isEnabled(DOZING_MIGRATION_1)) {
listenForDozeAmountTransition(this)
listenForGoneToAodTransition(this)
} else {
listenForDozeAmount(this)
}
@@ -276,6 +281,22 @@ open class ClockEventController @Inject constructor(
}
}
/**
* When keyguard is displayed again after being gone, the clock must be reset to full
* dozing.
*/
@VisibleForTesting
internal fun listenForGoneToAodTransition(scope: CoroutineScope): Job {
return scope.launch {
keyguardTransitionInteractor.goneToAodTransition.filter {
it.transitionState == TransitionState.STARTED
}.collect {
dozeAmount = 1f
clock?.animations?.doze(dozeAmount)
}
}
}
@VisibleForTesting
internal fun listenForDozing(scope: CoroutineScope): Job {
return scope.launch {

View File

@@ -21,6 +21,7 @@ import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.plugins.log.LogLevel
import com.android.systemui.plugins.log.LogLevel.DEBUG
import com.android.systemui.plugins.log.LogLevel.ERROR
import com.android.systemui.plugins.log.LogLevel.INFO
import com.android.systemui.plugins.log.LogLevel.VERBOSE
import com.android.systemui.plugins.log.LogLevel.WARNING
import com.android.systemui.plugins.log.MessageInitializer
@@ -50,6 +51,14 @@ class KeyguardLogger @Inject constructor(@KeyguardLog private val buffer: LogBuf
buffer.log(TAG, DEBUG, messageInitializer, messagePrinter)
}
fun v(msg: String, arg: Any) {
buffer.log(TAG, VERBOSE, { str1 = arg.toString() }, { "$msg: $str1" })
}
fun i(msg: String, arg: Any) {
buffer.log(TAG, INFO, { str1 = arg.toString() }, { "$msg: $str1" })
}
// TODO: remove after b/237743330 is fixed
fun logStatusBarCalculatedAlpha(alpha: Float) {
debugLog({ double1 = alpha.toDouble() }, { "Calculated new alpha: $double1" })

View File

@@ -122,7 +122,8 @@ object Flags {
* Migration from the legacy isDozing/dozeAmount paths to the new KeyguardTransitionRepository
* will occur in stages. This is one stage of many to come.
*/
@JvmField val DOZING_MIGRATION_1 = UnreleasedFlag(213, teamfood = true)
// TODO(b/255607168): Tracking Bug
@JvmField val DOZING_MIGRATION_1 = UnreleasedFlag(213)
// 300 - power menu
// TODO(b/254512600): Tracking Bug

View File

@@ -21,7 +21,10 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall
import com.android.systemui.common.shared.model.Position
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.doze.DozeHost
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.keyguard.WakefulnessLifecycle.Wakefulness
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.WakefulnessModel
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.policy.KeyguardStateController
import javax.inject.Inject
@@ -89,6 +92,9 @@ interface KeyguardRepository {
/** Observable for the [StatusBarState] */
val statusBarState: Flow<StatusBarState>
/** Observable for device wake/sleep state */
val wakefulnessState: Flow<WakefulnessModel>
/**
* Returns `true` if the keyguard is showing; `false` otherwise.
*
@@ -118,6 +124,7 @@ constructor(
statusBarStateController: StatusBarStateController,
private val keyguardStateController: KeyguardStateController,
dozeHost: DozeHost,
wakefulnessLifecycle: WakefulnessLifecycle,
) : KeyguardRepository {
private val _animateBottomAreaDozingTransitions = MutableStateFlow(false)
override val animateBottomAreaDozingTransitions =
@@ -207,6 +214,40 @@ constructor(
awaitClose { statusBarStateController.removeCallback(callback) }
}
override val wakefulnessState: Flow<WakefulnessModel> = conflatedCallbackFlow {
val callback =
object : WakefulnessLifecycle.Observer {
override fun onStartedWakingUp() {
trySendWithFailureLogging(
WakefulnessModel.STARTING_TO_WAKE,
TAG,
"Wakefulness: starting to wake"
)
}
override fun onFinishedWakingUp() {
trySendWithFailureLogging(WakefulnessModel.AWAKE, TAG, "Wakefulness: awake")
}
override fun onStartedGoingToSleep() {
trySendWithFailureLogging(
WakefulnessModel.STARTING_TO_SLEEP,
TAG,
"Wakefulness: starting to sleep"
)
}
override fun onFinishedGoingToSleep() {
trySendWithFailureLogging(WakefulnessModel.ASLEEP, TAG, "Wakefulness: asleep")
}
}
wakefulnessLifecycle.addObserver(callback)
trySendWithFailureLogging(
wakefulnessIntToObject(wakefulnessLifecycle.getWakefulness()),
TAG,
"initial wakefulness state"
)
awaitClose { wakefulnessLifecycle.removeObserver(callback) }
}
override fun setAnimateDozingTransitions(animate: Boolean) {
_animateBottomAreaDozingTransitions.value = animate
}
@@ -228,6 +269,16 @@ constructor(
}
}
private fun wakefulnessIntToObject(@Wakefulness value: Int): WakefulnessModel {
return when (value) {
0 -> WakefulnessModel.ASLEEP
1 -> WakefulnessModel.STARTING_TO_WAKE
2 -> WakefulnessModel.AWAKE
3 -> WakefulnessModel.STARTING_TO_SLEEP
else -> throw IllegalArgumentException("Invalid Wakefulness value: $value")
}
}
companion object {
private const val TAG = "KeyguardRepositoryImpl"
}

View File

@@ -22,4 +22,9 @@ import dagger.Module
@Module
interface KeyguardRepositoryModule {
@Binds fun keyguardRepository(impl: KeyguardRepositoryImpl): KeyguardRepository
@Binds
fun keyguardTransitionRepository(
impl: KeyguardTransitionRepositoryImpl
): KeyguardTransitionRepository
}

View File

@@ -29,27 +29,33 @@ 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.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
@SysUISingleton
class KeyguardTransitionRepository @Inject constructor() {
/*
* Each transition between [KeyguardState]s will have an associated Flow.
* In order to collect these events, clients should call [transition].
/**
* 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.
*/
private val _transitions = MutableStateFlow(TransitionStep())
val transitions = _transitions.asStateFlow()
/* Information about the active transition. */
private var currentTransitionInfo: TransitionInfo? = 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
val transitions: Flow<TransitionStep>
/**
* Interactors that require information about changes between [KeyguardState]s will call this to
@@ -60,65 +66,10 @@ class KeyguardTransitionRepository @Inject constructor() {
}
/**
* Begin a transition from one state to another. The [info.from] must match
* [currentTransitionInfo.to], or the request will be denied. This is enforced to avoid
* unplanned transitions.
* Begin a transition from one state to another. Will not start if another transition is in
* progress.
*/
fun startTransition(info: TransitionInfo): UUID? {
if (currentTransitionInfo != null) {
// Open questions:
// * Queue of transitions? buffer of 1?
// * Are transitions cancellable if a new one is triggered?
// * What validation does this need to do?
Log.wtf(TAG, "Transition still active: $currentTransitionInfo")
return null
}
currentTransitionInfo?.animator?.cancel()
currentTransitionInfo = info
info.animator?.let { animator ->
// An animator was provided, so use it to run the transition
animator.setFloatValues(0f, 1f)
val updateListener =
object : AnimatorUpdateListener {
override fun onAnimationUpdate(animation: ValueAnimator) {
emitTransition(
info,
(animation.getAnimatedValue() as Float),
TransitionState.RUNNING
)
}
}
val adapter =
object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator) {
Log.i(TAG, "Starting transition: $info")
emitTransition(info, 0f, TransitionState.STARTED)
}
override fun onAnimationCancel(animation: Animator) {
Log.i(TAG, "Cancelling transition: $info")
}
override fun onAnimationEnd(animation: Animator) {
Log.i(TAG, "Ending transition: $info")
emitTransition(info, 1f, TransitionState.FINISHED)
animator.removeListener(this)
animator.removeUpdateListener(updateListener)
}
}
animator.addListener(adapter)
animator.addUpdateListener(updateListener)
animator.start()
return@startTransition null
}
?: run {
Log.i(TAG, "Starting transition (manual): $info")
emitTransition(info, 0f, TransitionState.STARTED)
// No animator, so it's manual. Provide a mechanism to callback
updateTransitionId = UUID.randomUUID()
return@startTransition updateTransitionId
}
}
fun startTransition(info: TransitionInfo): UUID?
/**
* Allows manual control of a transition. When calling [startTransition], the consumer must pass
@@ -132,58 +83,127 @@ class KeyguardTransitionRepository @Inject constructor() {
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>(
extraBufferCapacity = 10,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
override val transitions = _transitions.asSharedFlow().distinctUntilChanged()
private var lastStep: TransitionStep = TransitionStep()
/*
* 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
override fun startTransition(info: TransitionInfo): UUID? {
if (lastStep.transitionState != TransitionState.FINISHED) {
// Open questions:
// * Queue of transitions? buffer of 1?
// * Are transitions cancellable if a new one is triggered?
// * What validation does this need to do?
Log.wtf(TAG, "Transition still active: $lastStep")
return null
}
info.animator?.let { animator ->
// An animator was provided, so use it to run the transition
animator.setFloatValues(0f, 1f)
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, 0f, TransitionState.STARTED))
}
override fun onAnimationCancel(animation: Animator) {
Log.i(TAG, "Cancelling transition: $info")
}
override fun onAnimationEnd(animation: Animator) {
emitTransition(TransitionStep(info, 1f, TransitionState.FINISHED))
animator.removeListener(this)
animator.removeUpdateListener(updateListener)
}
}
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 (currentTransitionInfo == null) {
Log.wtf(TAG, "Attempting to update with null 'currentTransitionInfo'")
return
if (state == TransitionState.FINISHED) {
updateTransitionId = null
}
currentTransitionInfo?.let { info ->
if (state == TransitionState.FINISHED) {
updateTransitionId = null
Log.i(TAG, "Ending transition: $info")
}
emitTransition(info, value, state)
}
val nextStep = lastStep.copy(value = value, transitionState = state)
emitTransition(nextStep, isManual = true)
}
private fun emitTransition(
info: TransitionInfo,
value: Float,
transitionState: TransitionState
) {
trace(info, transitionState)
if (transitionState == TransitionState.FINISHED) {
currentTransitionInfo = null
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")
}
_transitions.value = TransitionStep(info.from, info.to, value, transitionState)
lastStep = nextStep
}
private fun trace(info: TransitionInfo, transitionState: TransitionState) {
private fun trace(step: TransitionStep, isManual: Boolean) {
if (
transitionState != TransitionState.STARTED &&
transitionState != TransitionState.FINISHED
step.transitionState != TransitionState.STARTED &&
step.transitionState != TransitionState.FINISHED
) {
return
}
val traceName =
"Transition: ${info.from} -> ${info.to} " +
if (info.animator == null) {
"Transition: ${step.from} -> ${step.to} " +
if (isManual) {
"(manual)"
} else {
""
}
val traceCookie = traceName.hashCode()
if (transitionState == TransitionState.STARTED) {
if (step.transitionState == TransitionState.STARTED) {
Trace.beginAsyncSection(traceName, traceCookie)
} else if (transitionState == TransitionState.FINISHED) {
} else if (step.transitionState == TransitionState.FINISHED) {
Trace.endAsyncSection(traceName, traceCookie)
}
}

View File

@@ -24,6 +24,7 @@ import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
@@ -36,31 +37,35 @@ constructor(
@Application private val scope: CoroutineScope,
private val keyguardRepository: KeyguardRepository,
private val keyguardTransitionRepository: KeyguardTransitionRepository,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
) : TransitionInteractor("AOD<->LOCKSCREEN") {
override fun start() {
scope.launch {
keyguardRepository.isDozing.collect { isDozing ->
if (isDozing) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
KeyguardState.LOCKSCREEN,
KeyguardState.AOD,
getAnimator(),
keyguardRepository.isDozing
.sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
.collect { pair ->
val (isDozing, keyguardState) = pair
if (isDozing && keyguardState == KeyguardState.LOCKSCREEN) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
KeyguardState.LOCKSCREEN,
KeyguardState.AOD,
getAnimator(),
)
)
)
} else {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
KeyguardState.AOD,
KeyguardState.LOCKSCREEN,
getAnimator(),
} else if (!isDozing && keyguardState == KeyguardState.AOD) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
KeyguardState.AOD,
KeyguardState.LOCKSCREEN,
getAnimator(),
)
)
)
}
}
}
}
}

View File

@@ -0,0 +1,76 @@
/*
* 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.domain.interactor
import android.animation.ValueAnimator
import com.android.systemui.animation.Interpolators
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.keyguard.shared.model.WakefulnessModel
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
@SysUISingleton
class GoneAodTransitionInteractor
@Inject
constructor(
@Application private val scope: CoroutineScope,
private val keyguardInteractor: KeyguardInteractor,
private val keyguardTransitionRepository: KeyguardTransitionRepository,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
) : TransitionInteractor("GONE->AOD") {
override fun start() {
scope.launch {
keyguardInteractor.wakefulnessState
.sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
.collect { pair ->
val (wakefulnessState, keyguardState) = pair
if (
keyguardState == KeyguardState.GONE &&
wakefulnessState == WakefulnessModel.STARTING_TO_SLEEP
) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
KeyguardState.GONE,
KeyguardState.AOD,
getAnimator(),
)
)
}
}
}
}
private fun getAnimator(): ValueAnimator {
return ValueAnimator().apply {
setInterpolator(Interpolators.LINEAR)
setDuration(TRANSITION_DURATION_MS)
}
}
companion object {
private const val TRANSITION_DURATION_MS = 500L
}
}

View File

@@ -19,6 +19,7 @@ package com.android.systemui.keyguard.domain.interactor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.keyguard.shared.model.WakefulnessModel
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
@@ -40,6 +41,8 @@ constructor(
val isDozing: Flow<Boolean> = repository.isDozing
/** Whether the keyguard is showing to not. */
val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing
/** The device wake/sleep state */
val wakefulnessState: Flow<WakefulnessModel> = repository.wakefulnessState
fun isKeyguardShowing(): Boolean {
return repository.isKeyguardShowing()

View File

@@ -0,0 +1,51 @@
/*
* 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.domain.interactor
import com.android.keyguard.logging.KeyguardLogger
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
/** Collect flows of interest for auditing keyguard transitions. */
@SysUISingleton
class KeyguardTransitionAuditLogger
@Inject
constructor(
@Application private val scope: CoroutineScope,
private val interactor: KeyguardTransitionInteractor,
private val keyguardInteractor: KeyguardInteractor,
private val logger: KeyguardLogger,
) {
fun start() {
scope.launch {
keyguardInteractor.wakefulnessState.collect { logger.v("WakefulnessState", it) }
}
scope.launch {
interactor.finishedKeyguardState.collect { logger.i("Finished transition to", it) }
}
scope.launch {
interactor.startedKeyguardState.collect { logger.i("Started transition to", it) }
}
}
}

View File

@@ -26,6 +26,7 @@ class KeyguardTransitionCoreStartable
@Inject
constructor(
private val interactors: Set<TransitionInteractor>,
private val auditLogger: KeyguardTransitionAuditLogger,
) : CoreStartable {
override fun start() {
@@ -38,9 +39,12 @@ constructor(
when (it) {
is LockscreenBouncerTransitionInteractor -> Log.d(TAG, "Started $it")
is AodLockscreenTransitionInteractor -> Log.d(TAG, "Started $it")
is GoneAodTransitionInteractor -> Log.d(TAG, "Started $it")
is LockscreenGoneTransitionInteractor -> Log.d(TAG, "Started $it")
}
it.start()
}
auditLogger.start()
}
companion object {

View File

@@ -19,11 +19,15 @@ package com.android.systemui.keyguard.domain.interactor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
@@ -40,6 +44,9 @@ constructor(
/** LOCKSCREEN->AOD transition information. */
val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD)
/** GONE->AOD information. */
val goneToAodTransition: Flow<TransitionStep> = repository.transition(GONE, AOD)
/**
* AOD<->LOCKSCREEN transition information, mapped to dozeAmount range of AOD (1f) <->
* Lockscreen (0f).
@@ -49,4 +56,16 @@ constructor(
aodToLockscreenTransition.map { step -> step.copy(value = 1f - step.value) },
lockscreenToAodTransition,
)
/* The last completed [KeyguardState] transition */
val finishedKeyguardState: Flow<KeyguardState> =
repository.transitions
.filter { step -> step.transitionState == TransitionState.FINISHED }
.map { step -> step.to }
/* The last started [KeyguardState] transition */
val startedKeyguardState: Flow<KeyguardState> =
repository.transitions
.filter { step -> step.transitionState == TransitionState.STARTED }
.map { step -> step.to }
}

View File

@@ -29,6 +29,7 @@ import com.android.systemui.util.kotlin.sample
import java.util.UUID
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
@@ -40,59 +41,63 @@ constructor(
private val keyguardRepository: KeyguardRepository,
private val shadeRepository: ShadeRepository,
private val keyguardTransitionRepository: KeyguardTransitionRepository,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor
) : TransitionInteractor("LOCKSCREEN<->BOUNCER") {
private var transitionId: UUID? = null
override fun start() {
scope.launch {
shadeRepository.shadeModel.sample(
combine(
keyguardTransitionRepository.transitions,
keyguardRepository.statusBarState,
) { transitions, statusBarState ->
Pair(transitions, statusBarState)
}
) { shadeModel, pair ->
val (transitions, statusBarState) = pair
shadeRepository.shadeModel
.sample(
combine(
keyguardTransitionInteractor.finishedKeyguardState,
keyguardRepository.statusBarState,
) { keyguardState, statusBarState ->
Pair(keyguardState, statusBarState)
},
{ shadeModel, pair -> Triple(shadeModel, pair.first, pair.second) }
)
.collect { triple ->
val (shadeModel, keyguardState, statusBarState) = triple
val id = transitionId
if (id != null) {
// An existing `id` means a transition is started, and calls to
// `updateTransition` will control it until FINISHED
keyguardTransitionRepository.updateTransition(
id,
shadeModel.expansionAmount,
if (shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f) {
transitionId = null
TransitionState.FINISHED
} else {
TransitionState.RUNNING
}
)
} else {
// TODO (b/251849525): Remove statusbarstate check when that state is integrated
// into KeyguardTransitionRepository
val isOnLockscreen =
transitions.transitionState == TransitionState.FINISHED &&
transitions.to == KeyguardState.LOCKSCREEN
if (
isOnLockscreen &&
shadeModel.isUserDragging &&
statusBarState != SHADE_LOCKED
) {
transitionId =
keyguardTransitionRepository.startTransition(
TransitionInfo(
ownerName = name,
from = KeyguardState.LOCKSCREEN,
to = KeyguardState.BOUNCER,
animator = null,
val id = transitionId
if (id != null) {
// An existing `id` means a transition is started, and calls to
// `updateTransition` will control it until FINISHED
keyguardTransitionRepository.updateTransition(
id,
shadeModel.expansionAmount,
if (
shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f
) {
transitionId = null
TransitionState.FINISHED
} else {
TransitionState.RUNNING
}
)
} else {
// TODO (b/251849525): Remove statusbarstate check when that state is
// integrated
// into KeyguardTransitionRepository
if (
keyguardState == KeyguardState.LOCKSCREEN &&
shadeModel.isUserDragging &&
statusBarState != SHADE_LOCKED
) {
transitionId =
keyguardTransitionRepository.startTransition(
TransitionInfo(
ownerName = name,
from = KeyguardState.LOCKSCREEN,
to = KeyguardState.BOUNCER,
animator = null,
)
)
)
}
}
}
}
}
}
}

View File

@@ -0,0 +1,67 @@
/*
* 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.domain.interactor
import android.animation.ValueAnimator
import com.android.systemui.animation.Interpolators
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
@SysUISingleton
class LockscreenGoneTransitionInteractor
@Inject
constructor(
@Application private val scope: CoroutineScope,
private val keyguardInteractor: KeyguardInteractor,
private val keyguardTransitionRepository: KeyguardTransitionRepository,
) : TransitionInteractor("LOCKSCREEN->GONE") {
override fun start() {
scope.launch {
keyguardInteractor.isKeyguardShowing.collect { isShowing ->
if (!isShowing) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
KeyguardState.LOCKSCREEN,
KeyguardState.GONE,
getAnimator(),
)
)
}
}
}
}
private fun getAnimator(): ValueAnimator {
return ValueAnimator().apply {
setInterpolator(Interpolators.LINEAR)
setDuration(TRANSITION_DURATION_MS)
}
}
companion object {
private const val TRANSITION_DURATION_MS = 10L
}
}

View File

@@ -39,4 +39,10 @@ abstract class StartKeyguardTransitionModule {
@Binds
@IntoSet
abstract fun aodLockscreen(impl: AodLockscreenTransitionInteractor): TransitionInteractor
@Binds @IntoSet abstract fun goneAod(impl: GoneAodTransitionInteractor): TransitionInteractor
@Binds
@IntoSet
abstract fun lockscreenGone(impl: LockscreenGoneTransitionInteractor): TransitionInteractor
}

View File

@@ -17,7 +17,10 @@ package com.android.systemui.keyguard.shared.model
/** List of all possible states to transition to/from */
enum class KeyguardState {
/** For initialization only */
/**
* For initialization as well as when the security method is set to NONE, indicating that
* the keyguard should never be shown.
*/
NONE,
/* Always-on Display. The device is in a low-power mode with a minimal UI visible */
AOD,
@@ -31,4 +34,11 @@ enum class KeyguardState {
* unlocked if SWIPE security method is used, or if face lockscreen bypass is false.
*/
LOCKSCREEN,
/*
* Keyguard is no longer visible. In most cases the user has just authenticated and keyguard
* is being removed, but there are other cases where the user is swiping away keyguard, such as
* with SWIPE security method or face unlock without bypass.
*/
GONE,
}

View File

@@ -17,7 +17,6 @@ package com.android.systemui.keyguard.shared.model
/** Possible states for a running transition between [State] */
enum class TransitionState {
NONE,
STARTED,
RUNNING,
FINISHED

View File

@@ -20,5 +20,11 @@ data class TransitionStep(
val from: KeyguardState = KeyguardState.NONE,
val to: KeyguardState = KeyguardState.NONE,
val value: Float = 0f, // constrained [0.0, 1.0]
val transitionState: TransitionState = TransitionState.NONE,
)
val transitionState: TransitionState = TransitionState.FINISHED,
) {
constructor(
info: TransitionInfo,
value: Float,
transitionState: TransitionState,
) : this(info.from, info.to, value, transitionState)
}

View File

@@ -0,0 +1,28 @@
/*
* 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.shared.model
/** Model device wakefulness states. */
enum class WakefulnessModel {
/** The device is asleep and not interactive. */
ASLEEP,
/** Received a signal that the device is beginning to wake up. */
STARTING_TO_WAKE,
/** Device is now fully awake and interactive. */
AWAKE,
/** Signal that the device is now going to sleep. */
STARTING_TO_SLEEP,
}

View File

@@ -20,6 +20,8 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.Position
import com.android.systemui.doze.DozeHost
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.keyguard.shared.model.WakefulnessModel
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.mockito.argumentCaptor
@@ -43,6 +45,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() {
@Mock private lateinit var statusBarStateController: StatusBarStateController
@Mock private lateinit var dozeHost: DozeHost
@Mock private lateinit var keyguardStateController: KeyguardStateController
@Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
private lateinit var underTest: KeyguardRepositoryImpl
@@ -55,6 +58,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() {
statusBarStateController,
keyguardStateController,
dozeHost,
wakefulnessLifecycle,
)
}
@@ -184,4 +188,33 @@ class KeyguardRepositoryImplTest : SysuiTestCase() {
job.cancel()
verify(statusBarStateController).removeCallback(captor.value)
}
@Test
fun wakefullness() = runBlockingTest {
val values = mutableListOf<WakefulnessModel>()
val job = underTest.wakefulnessState.onEach(values::add).launchIn(this)
val captor = argumentCaptor<WakefulnessLifecycle.Observer>()
verify(wakefulnessLifecycle).addObserver(captor.capture())
captor.value.onStartedWakingUp()
captor.value.onFinishedWakingUp()
captor.value.onStartedGoingToSleep()
captor.value.onFinishedGoingToSleep()
assertThat(values)
.isEqualTo(
listOf(
// Initial value will be ASLEEP
WakefulnessModel.ASLEEP,
WakefulnessModel.STARTING_TO_WAKE,
WakefulnessModel.AWAKE,
WakefulnessModel.STARTING_TO_SLEEP,
WakefulnessModel.ASLEEP,
)
)
job.cancel()
verify(wakefulnessLifecycle).removeObserver(captor.value)
}
}

View File

@@ -63,7 +63,7 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() {
@Before
fun setUp() {
underTest = KeyguardTransitionRepository()
underTest = KeyguardTransitionRepositoryImpl()
wtfHandler = WtfHandler()
oldWtfHandler = Log.setWtfHandler(wtfHandler)
}
@@ -174,9 +174,6 @@ class KeyguardTransitionRepositoryTest : SysuiTestCase() {
}
private fun assertSteps(steps: List<TransitionStep>, fractions: List<BigDecimal>) {
// + 2 accounts for start and finish of automated transition
assertThat(steps.size).isEqualTo(fractions.size + 2)
assertThat(steps[0]).isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED))
fractions.forEachIndexed { index, fraction ->
assertThat(steps[index + 1])

View File

@@ -0,0 +1,149 @@
/*
* 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.domain.interactor
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@SmallTest
@RunWith(JUnit4::class)
class KeyguardTransitionInteractorTest : SysuiTestCase() {
private lateinit var underTest: KeyguardTransitionInteractor
private lateinit var repository: FakeKeyguardTransitionRepository
@Before
fun setUp() {
repository = FakeKeyguardTransitionRepository()
underTest = KeyguardTransitionInteractor(repository)
}
@Test
fun `transition collectors receives only appropriate events`() =
runBlocking(IMMEDIATE) {
var goneToAodSteps = mutableListOf<TransitionStep>()
val job1 =
underTest.goneToAodTransition.onEach { goneToAodSteps.add(it) }.launchIn(this)
var aodToLockscreenSteps = mutableListOf<TransitionStep>()
val job2 =
underTest.aodToLockscreenTransition
.onEach { aodToLockscreenSteps.add(it) }
.launchIn(this)
val steps = mutableListOf<TransitionStep>()
steps.add(TransitionStep(AOD, GONE, 0f, STARTED))
steps.add(TransitionStep(AOD, GONE, 1f, FINISHED))
steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
steps.add(TransitionStep(GONE, AOD, 0f, STARTED))
steps.add(TransitionStep(GONE, AOD, 0.1f, RUNNING))
steps.add(TransitionStep(GONE, AOD, 0.2f, RUNNING))
steps.forEach { repository.sendTransitionStep(it) }
assertThat(aodToLockscreenSteps).isEqualTo(steps.subList(2, 5))
assertThat(goneToAodSteps).isEqualTo(steps.subList(5, 8))
job1.cancel()
job2.cancel()
}
@Test
fun dozeAmountTransitionTest() =
runBlocking(IMMEDIATE) {
var dozeAmountSteps = mutableListOf<TransitionStep>()
val job =
underTest.dozeAmountTransition.onEach { dozeAmountSteps.add(it) }.launchIn(this)
val steps = mutableListOf<TransitionStep>()
steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0.8f, RUNNING))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING))
steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED))
steps.forEach { repository.sendTransitionStep(it) }
assertThat(dozeAmountSteps.subList(0, 3))
.isEqualTo(
listOf(
steps[0].copy(value = 1f - steps[0].value),
steps[1].copy(value = 1f - steps[1].value),
steps[2].copy(value = 1f - steps[2].value),
)
)
assertThat(dozeAmountSteps.subList(3, 7)).isEqualTo(steps.subList(3, 7))
job.cancel()
}
@Test
fun keyguardStateTests() =
runBlocking(IMMEDIATE) {
var finishedSteps = mutableListOf<KeyguardState>()
val job1 =
underTest.finishedKeyguardState.onEach { finishedSteps.add(it) }.launchIn(this)
var startedSteps = mutableListOf<KeyguardState>()
val job2 = underTest.startedKeyguardState.onEach { startedSteps.add(it) }.launchIn(this)
val steps = mutableListOf<TransitionStep>()
steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING))
steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED))
steps.add(TransitionStep(AOD, GONE, 1f, STARTED))
steps.forEach { repository.sendTransitionStep(it) }
assertThat(finishedSteps).isEqualTo(listOf(LOCKSCREEN, AOD))
assertThat(startedSteps).isEqualTo(listOf(LOCKSCREEN, AOD, GONE))
job1.cancel()
job2.cancel()
}
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate
}
}

View File

@@ -19,6 +19,7 @@ package com.android.systemui.keyguard.data.repository
import com.android.systemui.common.shared.model.Position
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.WakefulnessModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -48,6 +49,9 @@ class FakeKeyguardRepository : KeyguardRepository {
private val _statusBarState = MutableStateFlow(StatusBarState.SHADE)
override val statusBarState: Flow<StatusBarState> = _statusBarState
private val _wakefulnessState = MutableStateFlow(WakefulnessModel.ASLEEP)
override val wakefulnessState: Flow<WakefulnessModel> = _wakefulnessState
override fun isKeyguardShowing(): Boolean {
return _isKeyguardShowing.value
}

View File

@@ -0,0 +1,47 @@
/*
* 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.annotation.FloatRange
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 kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
/** Fake implementation of [KeyguardTransitionRepository] */
class FakeKeyguardTransitionRepository : KeyguardTransitionRepository {
private val _transitions = MutableSharedFlow<TransitionStep>()
override val transitions: SharedFlow<TransitionStep> = _transitions
suspend fun sendTransitionStep(step: TransitionStep) {
_transitions.emit(step)
}
override fun startTransition(info: TransitionInfo): UUID? {
return null
}
override fun updateTransition(
transitionId: UUID,
@FloatRange(from = 0.0, to = 1.0) value: Float,
state: TransitionState
) = Unit
}