diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 55f8bea4d0569..910955a45f7b7 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -32,8 +32,9 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlags 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.lifecycle.repeatWhenAttached -import com.android.systemui.plugins.Clock +import com.android.systemui.plugins.ClockController import com.android.systemui.shared.regionsampling.RegionSamplingInstance import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback @@ -56,6 +57,7 @@ import kotlinx.coroutines.launch */ open class ClockEventController @Inject constructor( private val keyguardInteractor: KeyguardInteractor, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val broadcastDispatcher: BroadcastDispatcher, private val batteryController: BatteryController, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, @@ -216,6 +218,7 @@ open class ClockEventController @Inject constructor( repeatOnLifecycle(Lifecycle.State.STARTED) { listenForDozing(this) listenForDozeAmount(this) + listenForDozeAmountTransition(this) } } } @@ -246,7 +249,7 @@ open class ClockEventController @Inject constructor( } @VisibleForTesting - internal suspend fun listenForDozeAmount(scope: CoroutineScope): Job { + internal fun listenForDozeAmount(scope: CoroutineScope): Job { return scope.launch { keyguardInteractor.dozeAmount.collect { dozeAmount = it @@ -256,7 +259,18 @@ open class ClockEventController @Inject constructor( } @VisibleForTesting - internal suspend fun listenForDozing(scope: CoroutineScope): Job { + internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job { + return scope.launch { + keyguardTransitionInteractor.aodToLockscreenTransition.collect { + // Would eventually run this: + // dozeAmount = it.value + // clock?.animations?.doze(dozeAmount) + } + } + } + + @VisibleForTesting + internal fun listenForDozing(scope: CoroutineScope): Job { return scope.launch { combine ( keyguardInteractor.dozeAmount, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index 56f1ac46a8758..56a1f1ae936e5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -43,6 +43,7 @@ import com.android.systemui.keyguard.DismissCallbackRegistry; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule; +import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule; import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceModule; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.statusbar.NotificationShadeDepthController; @@ -72,6 +73,7 @@ import dagger.Provides; FalsingModule.class, KeyguardQuickAffordanceModule.class, KeyguardRepositoryModule.class, + StartKeyguardTransitionModule.class, }) public class KeyguardModule { /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 45b668e609eae..b186ae0ceec4d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -21,6 +21,7 @@ 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.shared.model.StatusBarState import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.policy.KeyguardStateController import javax.inject.Inject @@ -85,6 +86,9 @@ interface KeyguardRepository { */ val dozeAmount: Flow + /** Observable for the [StatusBarState] */ + val statusBarState: Flow + /** * Returns `true` if the keyguard is showing; `false` otherwise. * @@ -185,6 +189,24 @@ constructor( return keyguardStateController.isShowing } + override val statusBarState: Flow = conflatedCallbackFlow { + val callback = + object : StatusBarStateController.StateListener { + override fun onStateChanged(state: Int) { + trySendWithFailureLogging(statusBarStateIntToObject(state), TAG, "state") + } + } + + statusBarStateController.addCallback(callback) + trySendWithFailureLogging( + statusBarStateIntToObject(statusBarStateController.getState()), + TAG, + "initial state" + ) + + awaitClose { statusBarStateController.removeCallback(callback) } + } + override fun setAnimateDozingTransitions(animate: Boolean) { _animateBottomAreaDozingTransitions.value = animate } @@ -197,6 +219,15 @@ constructor( _clockPosition.value = Position(x, y) } + private fun statusBarStateIntToObject(value: Int): StatusBarState { + return when (value) { + 0 -> StatusBarState.SHADE + 1 -> StatusBarState.KEYGUARD + 2 -> StatusBarState.SHADE_LOCKED + else -> throw IllegalArgumentException("Invalid StatusBarState value: $value") + } + } + companion object { private const val TAG = "KeyguardRepositoryImpl" } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt new file mode 100644 index 0000000000000..e8532ecfdc375 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -0,0 +1,169 @@ +/* + * 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.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.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +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]. + */ + 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 + + /** + * 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 { + return transitions.filter { step -> step.from == from && step.to == to } + } + + /** + * 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. + */ + 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 + } + } + + /** + * 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 + ) { + 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 + } + + currentTransitionInfo?.let { info -> + if (state == TransitionState.FINISHED) { + updateTransitionId = null + Log.i(TAG, "Ending transition: $info") + } + + emitTransition(info, value, state) + } + } + + private fun emitTransition( + info: TransitionInfo, + value: Float, + transitionState: TransitionState + ) { + if (transitionState == TransitionState.FINISHED) { + currentTransitionInfo = null + } + _transitions.value = TransitionStep(info.from, info.to, value, transitionState) + } + + companion object { + private const val TAG = "KeyguardTransitionRepository" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt new file mode 100644 index 0000000000000..400376663f1af --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt @@ -0,0 +1,77 @@ +/* + * 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.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 javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +@SysUISingleton +class AodLockscreenTransitionInteractor +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val keyguardRepository: KeyguardRepository, + private val keyguardTransitionRepository: KeyguardTransitionRepository, +) : TransitionInteractor("AOD<->LOCKSCREEN") { + + override fun start() { + scope.launch { + keyguardRepository.isDozing.collect { isDozing -> + if (isDozing) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.LOCKSCREEN, + KeyguardState.AOD, + getAnimator(), + ) + ) + } else { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.AOD, + KeyguardState.LOCKSCREEN, + getAnimator(), + ) + ) + } + } + } + } + + private fun getAnimator(): ValueAnimator { + return ValueAnimator().apply { + setInterpolator(Interpolators.LINEAR) + setDuration(TRANSITION_DURATION_MS) + } + } + + companion object { + private const val TRANSITION_DURATION_MS = 500L + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 192919e32cf64..fc2269c6b01ca 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -38,7 +38,7 @@ constructor( val dozeAmount: Flow = repository.dozeAmount /** Whether the system is in doze mode. */ val isDozing: Flow = repository.isDozing - /** Whether the keyguard is showing ot not. */ + /** Whether the keyguard is showing to not. */ val isKeyguardShowing: Flow = repository.isKeyguardShowing fun isKeyguardShowing(): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt new file mode 100644 index 0000000000000..b166681433a8b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt @@ -0,0 +1,49 @@ +/* + * 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.util.Log +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import java.util.Set +import javax.inject.Inject + +@SysUISingleton +class KeyguardTransitionCoreStartable +@Inject +constructor( + private val interactors: Set, +) : CoreStartable { + + override fun start() { + // By listing the interactors in a when, the compiler will help enforce all classes + // extending the sealed class [TransitionInteractor] will be initialized. + interactors.forEach { + // `when` needs to be an expression in order for the compiler to enforce it being + // exhaustive + val ret = + when (it) { + is LockscreenBouncerTransitionInteractor -> Log.d(TAG, "Started $it") + is AodLockscreenTransitionInteractor -> Log.d(TAG, "Started $it") + } + it.start() + } + } + + companion object { + private const val TAG = "KeyguardTransitionCoreStartable" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt new file mode 100644 index 0000000000000..59bb227869175 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -0,0 +1,37 @@ +/* + * 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.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.TransitionStep +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +/** Encapsulates business-logic related to the keyguard transitions. */ +@SysUISingleton +class KeyguardTransitionInteractor +@Inject +constructor( + repository: KeyguardTransitionRepository, +) { + /** AOD->LOCKSCREEN transition information. */ + val aodToLockscreenTransition: Flow = repository.transition(AOD, LOCKSCREEN) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt new file mode 100644 index 0000000000000..3c2a12e3836a4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt @@ -0,0 +1,98 @@ +/* + * 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.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +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.StatusBarState.SHADE_LOCKED +import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.shade.data.repository.ShadeRepository +import com.android.systemui.util.kotlin.sample +import java.util.UUID +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch + +@SysUISingleton +class LockscreenBouncerTransitionInteractor +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val keyguardRepository: KeyguardRepository, + private val shadeRepository: ShadeRepository, + private val keyguardTransitionRepository: KeyguardTransitionRepository, +) : 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 + + 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, + ) + ) + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt new file mode 100644 index 0000000000000..74c542c0043f6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt @@ -0,0 +1,42 @@ +/* + * 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.systemui.CoreStartable +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import dagger.multibindings.IntoSet + +@Module +abstract class StartKeyguardTransitionModule { + + @Binds + @IntoMap + @ClassKey(KeyguardTransitionCoreStartable::class) + abstract fun bind(impl: KeyguardTransitionCoreStartable): CoreStartable + + @Binds + @IntoSet + abstract fun lockscreenBouncer( + impl: LockscreenBouncerTransitionInteractor + ): TransitionInteractor + + @Binds + @IntoSet + abstract fun aodLockscreen(impl: AodLockscreenTransitionInteractor): TransitionInteractor +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt new file mode 100644 index 0000000000000..a2a46d9e3a71c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt @@ -0,0 +1,32 @@ +/* + * 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 +/** + * Each TransitionInteractor is responsible for determining under which conditions to notify + * [KeyguardTransitionRepository] to signal a transition. When (and if) the transition occurs is + * determined by [KeyguardTransitionRepository]. + * + * [name] field should be a unique identifiable string representing this state, used primarily for + * logging + * + * MUST list implementing classes in dagger module [StartKeyguardTransitionModule] and also in the + * 'when' clause of [KeyguardTransitionCoreStartable] + */ +sealed class TransitionInteractor(val name: String) { + + abstract fun start() +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt new file mode 100644 index 0000000000000..f66d5d3650c8d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt @@ -0,0 +1,34 @@ +/* + * 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 + +/** List of all possible states to transition to/from */ +enum class KeyguardState { + /** For initialization only */ + NONE, + /* Always-on Display. The device is in a low-power mode with a minimal UI visible */ + AOD, + /* + * The security screen prompt UI, containing PIN, Password, Pattern, and all FPS + * (Fingerprint Sensor) variations, for the user to verify their credentials + */ + BOUNCER, + /* + * Device is actively displaying keyguard UI and is not in low-power mode. Device may be + * unlocked if SWIPE security method is used, or if face lockscreen bypass is false. + */ + LOCKSCREEN, +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt new file mode 100644 index 0000000000000..bb953477583d9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/StatusBarState.kt @@ -0,0 +1,23 @@ +/* + * 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 + +/** See [com.android.systemui.statusbar.StatusBarState] for definitions */ +enum class StatusBarState { + SHADE, + KEYGUARD, + SHADE_LOCKED, +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt new file mode 100644 index 0000000000000..bfccf3fe076cb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionInfo.kt @@ -0,0 +1,35 @@ +/* + * 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 + +import android.animation.ValueAnimator + +/** Tracks who is controlling the current transition, and how to run it. */ +data class TransitionInfo( + val ownerName: String, + val from: KeyguardState, + val to: KeyguardState, + val animator: ValueAnimator?, // 'null' animator signal manual control +) { + override fun toString(): String = + "TransitionInfo(ownerName=$ownerName, from=$from, to=$to, " + + (if (animator != null) { + "animated" + } else { + "manual" + }) + + ")" +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt new file mode 100644 index 0000000000000..d8691c17f53db --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt @@ -0,0 +1,24 @@ +/* + * 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 + +/** Possible states for a running transition between [State] */ +enum class TransitionState { + NONE, + STARTED, + RUNNING, + FINISHED +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt new file mode 100644 index 0000000000000..688ec912aac8c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt @@ -0,0 +1,24 @@ +/* + * 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 + +/** This information will flow from the [KeyguardTransitionRepository] to control the UI layer */ +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, +) diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt new file mode 100644 index 0000000000000..09019a69df47d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt @@ -0,0 +1,62 @@ +/* + * 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.shade.data.repository + +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.shade.ShadeExpansionChangeEvent +import com.android.systemui.shade.ShadeExpansionListener +import com.android.systemui.shade.ShadeExpansionStateManager +import com.android.systemui.shade.domain.model.ShadeModel +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged + +/** Business logic for shade interactions */ +@SysUISingleton +class ShadeRepository @Inject constructor(shadeExpansionStateManager: ShadeExpansionStateManager) { + + val shadeModel: Flow = + conflatedCallbackFlow { + val callback = + object : ShadeExpansionListener { + override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) { + // Don't propagate ShadeExpansionChangeEvent.dragDownPxAmount field. + // It is too noisy and produces extra events that consumers won't care + // about + val info = + ShadeModel( + expansionAmount = event.fraction, + isExpanded = event.expanded, + isUserDragging = event.tracking + ) + trySendWithFailureLogging(info, TAG, "updated shade expansion info") + } + } + + shadeExpansionStateManager.addExpansionListener(callback) + trySendWithFailureLogging(ShadeModel(), TAG, "initial shade expansion info") + + awaitClose { shadeExpansionStateManager.removeExpansionListener(callback) } + } + .distinctUntilChanged() + + companion object { + private const val TAG = "ShadeRepository" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt new file mode 100644 index 0000000000000..ce0f4283ff831 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/model/ShadeModel.kt @@ -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.shade.domain.model + +import android.annotation.FloatRange + +/** Information about shade (NotificationPanel) expansion */ +data class ShadeModel( + /** 0 when collapsed, 1 when fully expanded. */ + @FloatRange(from = 0.0, to = 1.0) val expansionAmount: Float = 0f, + /** Whether the panel should be considered expanded */ + val isExpanded: Boolean = false, + /** Whether the user is actively dragging the panel. */ + val isUserDragging: Boolean = false, +) diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index af439f0e009b4..03efd06d0e5e5 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -24,13 +24,14 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.flags.FeatureFlags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.plugins.ClockAnimations import com.android.systemui.plugins.ClockController import com.android.systemui.plugins.ClockEvents import com.android.systemui.plugins.ClockFaceController import com.android.systemui.plugins.ClockFaceEvents -import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.mockito.any @@ -79,6 +80,7 @@ class ClockEventControllerTest : SysuiTestCase() { @Mock private lateinit var smallClockEvents: ClockFaceEvents @Mock private lateinit var largeClockEvents: ClockFaceEvents @Mock private lateinit var parentView: View + @Mock private lateinit var transitionRepository: KeyguardTransitionRepository private lateinit var repository: FakeKeyguardRepository private lateinit var underTest: ClockEventController @@ -98,6 +100,7 @@ class ClockEventControllerTest : SysuiTestCase() { underTest = ClockEventController( KeyguardInteractor(repository = repository), + KeyguardTransitionInteractor(repository = transitionRepository), broadcastDispatcher, batteryController, keyguardUpdateMonitor, @@ -132,7 +135,6 @@ class ClockEventControllerTest : SysuiTestCase() { @Test fun themeChanged_verifyClockPaletteUpdated() = runBlocking(IMMEDIATE) { - clockEventController.clock = clock verify(smallClockEvents).onRegionDarknessChanged(anyBoolean()) verify(largeClockEvents).onRegionDarknessChanged(anyBoolean()) @@ -145,7 +147,6 @@ class ClockEventControllerTest : SysuiTestCase() { @Test fun fontChanged_verifyFontSizeUpdated() = runBlocking(IMMEDIATE) { - clockEventController.clock = clock verify(smallClockEvents).onRegionDarknessChanged(anyBoolean()) verify(largeClockEvents).onRegionDarknessChanged(anyBoolean()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt new file mode 100644 index 0000000000000..1b34100b1cef1 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt @@ -0,0 +1,259 @@ +/* + * 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.AnimationHandler.AnimationFrameCallbackProvider +import android.animation.ValueAnimator +import android.util.Log +import android.util.Log.TerribleFailure +import android.util.Log.TerribleFailureHandler +import android.view.Choreographer.FrameCallback +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.Interpolators +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.BOUNCER +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +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 com.google.common.truth.Truth.assertThat +import java.math.BigDecimal +import java.math.RoundingMode +import java.util.UUID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield +import org.junit.After +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class KeyguardTransitionRepositoryTest : SysuiTestCase() { + + private lateinit var underTest: KeyguardTransitionRepository + private lateinit var oldWtfHandler: TerribleFailureHandler + private lateinit var wtfHandler: WtfHandler + + @Before + fun setUp() { + underTest = KeyguardTransitionRepository() + wtfHandler = WtfHandler() + oldWtfHandler = Log.setWtfHandler(wtfHandler) + } + + @After + fun tearDown() { + oldWtfHandler?.let { Log.setWtfHandler(it) } + } + + @Test + fun `startTransition runs animator to completion`() = + runBlocking(IMMEDIATE) { + val (animator, provider) = setupAnimator(this) + + val steps = mutableListOf() + val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this) + + underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, animator)) + + val startTime = System.currentTimeMillis() + while (animator.isRunning()) { + yield() + if (System.currentTimeMillis() - startTime > MAX_TEST_DURATION) { + fail("Failed test due to excessive runtime of: $MAX_TEST_DURATION") + } + } + + assertSteps(steps, listWithStep(BigDecimal(.1))) + + job.cancel() + provider.stop() + } + + @Test + fun `startTransition called during another transition fails`() { + underTest.startTransition(TransitionInfo(OWNER_NAME, AOD, LOCKSCREEN, null)) + underTest.startTransition(TransitionInfo(OWNER_NAME, LOCKSCREEN, BOUNCER, null)) + + assertThat(wtfHandler.failed).isTrue() + } + + @Test + fun `Null animator enables manual control with updateTransition`() = + runBlocking(IMMEDIATE) { + val steps = mutableListOf() + val job = underTest.transition(AOD, LOCKSCREEN).onEach { steps.add(it) }.launchIn(this) + + val uuid = + underTest.startTransition( + TransitionInfo( + ownerName = OWNER_NAME, + from = AOD, + to = LOCKSCREEN, + animator = null, + ) + ) + + checkNotNull(uuid).let { + underTest.updateTransition(it, 0.5f, TransitionState.RUNNING) + underTest.updateTransition(it, 1f, TransitionState.FINISHED) + } + + assertThat(steps.size).isEqualTo(3) + assertThat(steps[0]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED)) + assertThat(steps[1]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0.5f, TransitionState.RUNNING)) + assertThat(steps[2]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED)) + job.cancel() + } + + @Test + fun `Attempt to manually update transition with invalid UUID throws exception`() { + underTest.updateTransition(UUID.randomUUID(), 0f, TransitionState.RUNNING) + assertThat(wtfHandler.failed).isTrue() + } + + @Test + fun `Attempt to manually update transition after FINISHED state throws exception`() { + val uuid = + underTest.startTransition( + TransitionInfo( + ownerName = OWNER_NAME, + from = AOD, + to = LOCKSCREEN, + animator = null, + ) + ) + + checkNotNull(uuid).let { + underTest.updateTransition(it, 1f, TransitionState.FINISHED) + underTest.updateTransition(it, 0.5f, TransitionState.RUNNING) + } + assertThat(wtfHandler.failed).isTrue() + } + + private fun listWithStep(step: BigDecimal): List { + val steps = mutableListOf() + + var i = BigDecimal.ZERO + while (i.compareTo(BigDecimal.ONE) <= 0) { + steps.add(i) + i = (i + step).setScale(2, RoundingMode.HALF_UP) + } + + return steps + } + + private fun assertSteps(steps: List, fractions: List) { + // + 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]) + .isEqualTo( + TransitionStep(AOD, LOCKSCREEN, fraction.toFloat(), TransitionState.RUNNING) + ) + } + assertThat(steps[steps.size - 1]) + .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED)) + + assertThat(wtfHandler.failed).isFalse() + } + + private fun setupAnimator( + scope: CoroutineScope + ): Pair { + val animator = + ValueAnimator().apply { + setInterpolator(Interpolators.LINEAR) + setDuration(ANIMATION_DURATION) + } + + val provider = TestFrameCallbackProvider(animator, scope) + provider.start() + + return Pair(animator, provider) + } + + /** Gives direct control over ValueAnimator. See [AnimationHandler] */ + private class TestFrameCallbackProvider( + private val animator: ValueAnimator, + private val scope: CoroutineScope, + ) : AnimationFrameCallbackProvider { + + private var frameCount = 1L + private var frames = MutableStateFlow(Pair(0L, null)) + private var job: Job? = null + + fun start() { + animator.getAnimationHandler().setProvider(this) + + job = + scope.launch { + frames.collect { + // Delay is required for AnimationHandler to properly register a callback + delay(1) + val (frameNumber, callback) = it + callback?.doFrame(frameNumber) + } + } + } + + fun stop() { + job?.cancel() + animator.getAnimationHandler().setProvider(null) + } + + override fun postFrameCallback(cb: FrameCallback) { + frames.value = Pair(++frameCount, cb) + } + override fun postCommitCallback(runnable: Runnable) {} + override fun getFrameTime() = frameCount + override fun getFrameDelay() = 1L + override fun setFrameDelay(delay: Long) {} + } + + private class WtfHandler : TerribleFailureHandler { + var failed = false + override fun onTerribleFailure(tag: String, what: TerribleFailure, system: Boolean) { + failed = true + } + } + + companion object { + private const val MAX_TEST_DURATION = 100L + private const val ANIMATION_DURATION = 10L + private const val OWNER_NAME = "Test" + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 725b1f41372c9..0c126805fb787 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -18,6 +18,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 kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -44,6 +45,9 @@ class FakeKeyguardRepository : KeyguardRepository { private val _dozeAmount = MutableStateFlow(0f) override val dozeAmount: Flow = _dozeAmount + private val _statusBarState = MutableStateFlow(StatusBarState.SHADE) + override val statusBarState: Flow = _statusBarState + override fun isKeyguardShowing(): Boolean { return _isKeyguardShowing.value }