From 1bc1d42def6e3872c2c7e35ea91fd85d0aac78fc Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Mon, 7 Aug 2023 15:03:03 -0700 Subject: [PATCH 1/2] [flexiglass] View-model integration test. Adds a view-model level integration test for Flexiglass. This is meant to be a collection of all known "rules" related to how Flexiglass automatically changes scenes based on device state like authentication and wakefulness. Fix: 294899205 Test: Built and ran, manually made sure that all scenarios described in the test class work as intended. Change-Id: I49577bc72c838b3afbb8d00931b2a388aeaf0a60 --- .../interactor/AuthenticationInteractor.kt | 2 +- .../scene/SceneFrameworkIntegrationTest.kt | 478 ++++++++++++++++++ 2 files changed, 479 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt index e121790f07b0e..4ab884494a067 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt @@ -102,7 +102,7 @@ constructor( .stateIn( scope = applicationScope, started = SharingStarted.Eagerly, - initialValue = true, + initialValue = false, ) /** diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt new file mode 100644 index 0000000000000..a537121758ea7 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -0,0 +1,478 @@ +/* + * Copyright 2023 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.scene + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.model.AuthenticationMethodModel as DataLayerAuthenticationMethodModel +import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainLayerAuthenticationMethodModel +import com.android.systemui.authentication.domain.model.AuthenticationMethodModel +import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.shared.model.WakefulnessState +import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel +import com.android.systemui.model.SysUiState +import com.android.systemui.scene.domain.startable.SceneContainerStartable +import com.android.systemui.scene.shared.model.ObservableTransitionState +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel +import com.android.systemui.settings.FakeDisplayTracker +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +/** + * Integration test cases for the Scene Framework. + * + * **Principles** + * * All test cases here should be done from the perspective of the view-models of the system. + * * Focus on happy paths, let smaller unit tests focus on failure cases. + * * These are _integration_ tests and, as such, are larger and harder to maintain than unit tests. + * Therefore, when adding or modifying test cases, consider whether what you're testing is better + * covered by a more granular unit test. + * * Please reuse the helper methods in this class (for example, [putDeviceToSleep] or + * [emulateUserDrivenTransition]). + * * All tests start with the device locked and with a PIN auth method. The class offers useful + * methods like [setAuthMethod], [unlockDevice], [lockDevice], etc. to help you set up a starting + * state that makes more sense for your test case. + * * All helper methods in this class make assertions that are meant to make sure that they're only + * being used when the state is as required (e.g. cannot unlock an already unlocked device, cannot + * put to sleep a device that's already asleep, etc.). + */ +@SmallTest +@RunWith(JUnit4::class) +class SceneFrameworkIntegrationTest : SysuiTestCase() { + + private val utils = SceneTestUtils(this) + private val testScope = utils.testScope + + private val sceneContainerConfig = utils.fakeSceneContainerConfig() + private val sceneRepository = + utils.fakeSceneContainerRepository( + containerConfig = sceneContainerConfig, + ) + private val sceneInteractor = + utils.sceneInteractor( + repository = sceneRepository, + ) + + private val authenticationRepository = utils.authenticationRepository() + private val authenticationInteractor = + utils.authenticationInteractor( + repository = authenticationRepository, + sceneInteractor = sceneInteractor, + ) + + private val transitionState = + MutableStateFlow( + ObservableTransitionState.Idle(sceneContainerConfig.initialSceneKey) + ) + private val sceneContainerViewModel = + SceneContainerViewModel( + interactor = sceneInteractor, + ) + .apply { setTransitionState(transitionState) } + + private val bouncerInteractor = + utils.bouncerInteractor( + authenticationInteractor = authenticationInteractor, + sceneInteractor = sceneInteractor, + ) + private val bouncerViewModel = + utils.bouncerViewModel( + bouncerInteractor = bouncerInteractor, + authenticationInteractor = authenticationInteractor, + ) + + private val lockscreenSceneViewModel = + LockscreenSceneViewModel( + applicationScope = testScope.backgroundScope, + authenticationInteractor = authenticationInteractor, + bouncerInteractor = bouncerInteractor, + ) + + private val keyguardRepository = utils.keyguardRepository() + private val keyguardInteractor = + utils.keyguardInteractor( + repository = keyguardRepository, + ) + + @Before + fun setUp() { + val featureFlags = FakeFeatureFlags().apply { set(Flags.SCENE_CONTAINER, true) } + + authenticationRepository.setUnlocked(false) + + val displayTracker = FakeDisplayTracker(context) + val sysUiState = SysUiState(displayTracker) + val startable = + SceneContainerStartable( + applicationScope = testScope.backgroundScope, + sceneInteractor = sceneInteractor, + authenticationInteractor = authenticationInteractor, + keyguardInteractor = keyguardInteractor, + featureFlags = featureFlags, + sysUiState = sysUiState, + displayId = displayTracker.defaultDisplayId, + sceneLogger = mock(), + ) + startable.start() + + assertWithMessage("Initial scene key mismatch!") + .that(sceneContainerViewModel.currentScene.value.key) + .isEqualTo(sceneContainerConfig.initialSceneKey) + assertWithMessage("Initial scene container visibility mismatch!") + .that(sceneContainerViewModel.isVisible.value) + .isTrue() + } + + @Test + fun clickLockButtonAndEnterCorrectPin_unlocksDevice() = + testScope.runTest { + lockscreenSceneViewModel.onLockButtonClicked() + assertCurrentScene(SceneKey.Bouncer) + emulateUiSceneTransition() + + enterPin() + assertCurrentScene(SceneKey.Gone) + emulateUiSceneTransition( + expectedVisible = false, + ) + } + + @Test + fun swipeUpOnLockscreen_enterCorrectPin_unlocksDevice() = + testScope.runTest { + val upDestinationSceneKey by + collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey) + assertThat(upDestinationSceneKey).isEqualTo(SceneKey.Bouncer) + emulateUserDrivenTransition( + to = upDestinationSceneKey, + ) + + enterPin() + assertCurrentScene(SceneKey.Gone) + emulateUiSceneTransition( + expectedVisible = false, + ) + } + + @Test + fun swipeUpOnLockscreen_withAuthMethodSwipe_dismissesLockscreen() = + testScope.runTest { + setAuthMethod(DomainLayerAuthenticationMethodModel.Swipe) + + val upDestinationSceneKey by + collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey) + assertThat(upDestinationSceneKey).isEqualTo(SceneKey.Gone) + emulateUserDrivenTransition( + to = upDestinationSceneKey, + expectedVisible = false, + ) + } + + @Test + fun withAuthMethodNone_deviceWakeUp_skipsLockscreen() = + testScope.runTest { + setAuthMethod(AuthenticationMethodModel.None) + putDeviceToSleep(instantlyLockDevice = false) + assertCurrentScene(SceneKey.Lockscreen) + + wakeUpDevice() + assertCurrentScene(SceneKey.Gone) + } + + @Test + fun deviceGoesToSleep_switchesToLockscreen() = + testScope.runTest { + unlockDevice() + assertCurrentScene(SceneKey.Gone) + + putDeviceToSleep() + assertCurrentScene(SceneKey.Lockscreen) + } + + @Test + fun deviceGoesToSleep_wakeUp_unlock() = + testScope.runTest { + unlockDevice() + assertCurrentScene(SceneKey.Gone) + putDeviceToSleep() + assertCurrentScene(SceneKey.Lockscreen) + wakeUpDevice() + assertCurrentScene(SceneKey.Lockscreen) + + unlockDevice() + assertCurrentScene(SceneKey.Gone) + } + + @Test + fun deviceGoesToSleep_withLockTimeout_staysOnLockscreen() = + testScope.runTest { + unlockDevice() + assertCurrentScene(SceneKey.Gone) + putDeviceToSleep(instantlyLockDevice = false) + assertCurrentScene(SceneKey.Lockscreen) + + // Pretend like the timeout elapsed and now lock the device. + lockDevice() + assertCurrentScene(SceneKey.Lockscreen) + } + + /** + * Asserts that the current scene in the view-model matches what's expected. + * + * Note that this doesn't assert what the current scene is in the UI. + */ + private fun TestScope.assertCurrentScene(expected: SceneKey) { + runCurrent() + assertWithMessage("Current scene mismatch!") + .that(sceneContainerViewModel.currentScene.value.key) + .isEqualTo(expected) + } + + /** + * Returns the [SceneKey] of the current scene as displayed in the UI. + * + * This can be different than the value in [SceneContainerViewModel.currentScene], by design, as + * the UI must gradually transition between scenes. + */ + private fun getCurrentSceneInUi(): SceneKey { + return when (val state = transitionState.value) { + is ObservableTransitionState.Idle -> state.scene + is ObservableTransitionState.Transition -> state.fromScene + } + } + + /** Updates the current authentication method and related states in the data layer. */ + private fun TestScope.setAuthMethod( + authMethod: DomainLayerAuthenticationMethodModel, + ) { + // Set the lockscreen enabled bit _before_ set the auth method as the code picks up on the + // lockscreen enabled bit _after_ the auth method is changed and the lockscreen enabled bit + // is not an observable that can trigger a new evaluation. + authenticationRepository.setLockscreenEnabled(authMethod !is AuthenticationMethodModel.None) + authenticationRepository.setAuthenticationMethod(authMethod.toDataLayer()) + if (!authMethod.isSecure) { + // When the auth method is not secure, the device is never considered locked. + authenticationRepository.setUnlocked(true) + } + runCurrent() + } + + /** + * Emulates a complete transition in the UI from whatever the current scene is in the UI to + * whatever the current scene should be, based on the value in + * [SceneContainerViewModel.onSceneChanged]. + * + * This should post a series of values into [transitionState] to emulate a gradual scene + * transition and culminate with a call to [SceneContainerViewModel.onSceneChanged]. + * + * The method asserts that a transition is actually required. E.g. it will fail if the current + * scene in [transitionState] is already caught up with the scene in + * [SceneContainerViewModel.currentScene]. + * + * @param expectedVisible Whether [SceneContainerViewModel.isVisible] should be set at the end + * of the UI transition. + */ + private fun TestScope.emulateUiSceneTransition( + expectedVisible: Boolean = true, + ) { + val to = sceneContainerViewModel.currentScene.value + val from = getCurrentSceneInUi() + assertWithMessage("Cannot transition to ${to.key} as the UI is already on that scene!") + .that(to.key) + .isNotEqualTo(from) + + // Begin to transition. + val progressFlow = MutableStateFlow(0f) + transitionState.value = + ObservableTransitionState.Transition( + fromScene = getCurrentSceneInUi(), + toScene = to.key, + progress = progressFlow, + ) + runCurrent() + + // Report progress of transition. + while (progressFlow.value < 1f) { + progressFlow.value += 0.2f + runCurrent() + } + + // End the transition and report the change. + transitionState.value = ObservableTransitionState.Idle(to.key) + + sceneContainerViewModel.onSceneChanged(to) + runCurrent() + + assertWithMessage("Visibility mismatch after scene transition from $from to ${to.key}!") + .that(sceneContainerViewModel.isVisible.value) + .isEqualTo(expectedVisible) + } + + /** + * Emulates a fire-and-forget user action (a fling or back, not a pointer-tracking swipe) that + * causes a scene change to the [to] scene. + * + * This also includes the emulation of the resulting UI transition that culminates with the UI + * catching up with the requested scene change (see [emulateUiSceneTransition]). + * + * @param to The scene to transition to. + * @param expectedVisible Whether [SceneContainerViewModel.isVisible] should be set at the end + * of the UI transition. + */ + private fun TestScope.emulateUserDrivenTransition( + to: SceneKey?, + expectedVisible: Boolean = true, + ) { + checkNotNull(to) + + sceneInteractor.changeScene(SceneModel(to), "reason") + assertThat(sceneContainerViewModel.currentScene.value.key).isEqualTo(to) + + emulateUiSceneTransition( + expectedVisible = expectedVisible, + ) + } + + /** + * Locks the device immediately (without delay). + * + * Asserts the device to be lockable (e.g. that the current authentication is secure). + * + * Not to be confused with [putDeviceToSleep], which may also instantly lock the device. + */ + private suspend fun TestScope.lockDevice() { + val authMethod = authenticationInteractor.getAuthenticationMethod() + assertWithMessage("The authentication method of $authMethod is not secure, cannot lock!") + .that(authMethod.isSecure) + .isTrue() + + authenticationRepository.setUnlocked(false) + runCurrent() + } + + /** Unlocks the device by entering the correct PIN. Ends up in the Gone scene. */ + private fun TestScope.unlockDevice() { + assertWithMessage("Cannot unlock a device that's already unlocked!") + .that(authenticationInteractor.isUnlocked.value) + .isFalse() + + lockscreenSceneViewModel.onLockButtonClicked() + runCurrent() + emulateUiSceneTransition() + + enterPin() + emulateUiSceneTransition( + expectedVisible = false, + ) + } + + /** + * Enters the correct PIN in the bouncer UI. + * + * Asserts that the current scene is [SceneKey.Bouncer] and that the current bouncer UI is a PIN + * before proceeding. + * + * Does not assert that the device is locked or unlocked. + */ + private fun TestScope.enterPin() { + assertWithMessage("Cannot enter PIN when not on the Bouncer scene!") + .that(getCurrentSceneInUi()) + .isEqualTo(SceneKey.Bouncer) + val authMethodViewModel by collectLastValue(bouncerViewModel.authMethod) + assertWithMessage("Cannot enter PIN when not using a PIN authentication method!") + .that(authMethodViewModel) + .isInstanceOf(PinBouncerViewModel::class.java) + + val pinBouncerViewModel = authMethodViewModel as PinBouncerViewModel + FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit -> + pinBouncerViewModel.onPinButtonClicked(digit) + } + pinBouncerViewModel.onAuthenticateButtonClicked() + runCurrent() + } + + /** Changes device wakefulness state from asleep to awake, going through intermediary states. */ + private fun TestScope.wakeUpDevice() { + val wakefulnessModel = keyguardRepository.wakefulness.value + assertWithMessage("Cannot wake up device as it's already awake!") + .that(wakefulnessModel.isStartingToWakeOrAwake()) + .isFalse() + + keyguardRepository.setWakefulnessModel( + wakefulnessModel.copy(state = WakefulnessState.STARTING_TO_WAKE) + ) + runCurrent() + keyguardRepository.setWakefulnessModel( + wakefulnessModel.copy(state = WakefulnessState.AWAKE) + ) + runCurrent() + } + + /** Changes device wakefulness state from awake to asleep, going through intermediary states. */ + private suspend fun TestScope.putDeviceToSleep( + instantlyLockDevice: Boolean = true, + ) { + val wakefulnessModel = keyguardRepository.wakefulness.value + assertWithMessage("Cannot put device to sleep as it's already asleep!") + .that(wakefulnessModel.isStartingToWakeOrAwake()) + .isTrue() + + keyguardRepository.setWakefulnessModel( + wakefulnessModel.copy(state = WakefulnessState.STARTING_TO_SLEEP) + ) + runCurrent() + keyguardRepository.setWakefulnessModel( + wakefulnessModel.copy(state = WakefulnessState.ASLEEP) + ) + runCurrent() + + if (instantlyLockDevice) { + lockDevice() + } + } + + private fun DomainLayerAuthenticationMethodModel.toDataLayer(): + DataLayerAuthenticationMethodModel { + return when (this) { + DomainLayerAuthenticationMethodModel.None -> DataLayerAuthenticationMethodModel.None + DomainLayerAuthenticationMethodModel.Swipe -> DataLayerAuthenticationMethodModel.None + DomainLayerAuthenticationMethodModel.Pin -> DataLayerAuthenticationMethodModel.Pin + DomainLayerAuthenticationMethodModel.Password -> + DataLayerAuthenticationMethodModel.Password + DomainLayerAuthenticationMethodModel.Pattern -> + DataLayerAuthenticationMethodModel.Pattern + } + } +} From a6894d2dcbf6960f7c62af385c7a704bf1ef035e Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Mon, 7 Aug 2023 16:14:30 -0700 Subject: [PATCH 2/2] [flexiglass] Minor tweaks to support Trust Managers. The "trust manager" is a system that allows the user to select friendly networks, devices, or locations, where their device can become more relaxed and only automatically lock after 4 hours or when explicitly locked using the power menu's "lockdown" option. Since the trust system occurs in the upstream, Flexiglass didn't need to make any changes to have it work with it. The device simply doesn't enter locked state or enters lock state through the lockdown power menu option and Flexiglass obeys. The problem is with some transitions that Flexiglass didn't support: 1. Swiping up lockscreen while lockscreen is unlocked should dismiss the lockscreen (even if the auth method is not swipe). This supports dismissing the lockscreen from AOD if, ever needed (likely, this is no needed as the device must be woken up before user input can be processed) 2. (more important than #1), automatically dismiss the lockscreen and go to the Gone scene if the device wakes up while unlocked, regardless of which auth method is currently selected. This prevents the Flexiglass lockscreen scene UI from showing over the unlocked Launcher UI both while the device is unlocked due to the trust manager or due to the lock timeout not having elapsed yet. Fix: 281730986 Fix: 294283030 Test: updated unit tests Test: added new integration test cases Test: manually verified that trust managers and lockdown mode both work (see b/281730986#comment2 for details) Test: manually verified the the screen off + screen on (within less time than the lock timeout) scenario doesn't end up with a lockscreen over the launcher. Change-Id: Ia9d5b620d78c2ad7e47ceb218e7765bdf5eed52d --- .../ui/viewmodel/LockscreenSceneViewModel.kt | 18 +- .../startable/SceneContainerStartable.kt | 24 ++- .../scene/SceneFrameworkIntegrationTest.kt | 46 +++-- .../startable/SceneContainerStartableTest.kt | 159 ++++-------------- .../android/systemui/scene/SceneTestUtils.kt | 15 ++ 5 files changed, 108 insertions(+), 154 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt index 11e85d0d85e16..6d3b7f18e9745 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt @@ -26,6 +26,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.scene.shared.model.SceneKey import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map @@ -51,15 +52,14 @@ constructor( ) /** The key of the scene we should switch to when swiping up. */ - val upDestinationSceneKey = - authenticationInteractor.canSwipeToDismiss - .map { canSwipeToDismiss -> upDestinationSceneKey(canSwipeToDismiss) } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = - upDestinationSceneKey(authenticationInteractor.canSwipeToDismiss.value), - ) + val upDestinationSceneKey: Flow = + authenticationInteractor.isUnlocked.map { isUnlocked -> + if (isUnlocked) { + SceneKey.Gone + } else { + SceneKey.Bouncer + } + } /** Notifies that the lock button on the lock screen was clicked. */ fun onLockButtonClicked() { diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index afefccb272146..17470998cf74a 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -178,12 +178,24 @@ constructor( } WakefulnessState.STARTING_TO_WAKE -> { val authMethod = authenticationInteractor.getAuthenticationMethod() - if (authMethod == AuthenticationMethodModel.None) { - switchToScene( - targetSceneKey = SceneKey.Gone, - loggingReason = - "device is starting to wake up while auth method is None", - ) + val isUnlocked = authenticationInteractor.isUnlocked.value + when { + authMethod == AuthenticationMethodModel.None -> { + switchToScene( + targetSceneKey = SceneKey.Gone, + loggingReason = + "device is starting to wake up while auth method is" + + " none", + ) + } + authMethod.isSecure && isUnlocked -> { + switchToScene( + targetSceneKey = SceneKey.Gone, + loggingReason = + "device is starting to wake up while unlocked with a" + + " secure auth method", + ) + } } } else -> Unit diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index a537121758ea7..8caf6dc3e28aa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -20,7 +20,6 @@ package com.android.systemui.scene import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.authentication.data.model.AuthenticationMethodModel as DataLayerAuthenticationMethodModel import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainLayerAuthenticationMethodModel import com.android.systemui.authentication.domain.model.AuthenticationMethodModel @@ -31,6 +30,7 @@ import com.android.systemui.flags.Flags import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel import com.android.systemui.model.SysUiState +import com.android.systemui.scene.SceneTestUtils.Companion.toDataLayer import com.android.systemui.scene.domain.startable.SceneContainerStartable import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey @@ -211,6 +211,17 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { assertCurrentScene(SceneKey.Gone) } + @Test + fun withAuthMethodSwipe_deviceWakeUp_doesNotSkipLockscreen() = + testScope.runTest { + setAuthMethod(AuthenticationMethodModel.Swipe) + putDeviceToSleep(instantlyLockDevice = false) + assertCurrentScene(SceneKey.Lockscreen) + + wakeUpDevice() + assertCurrentScene(SceneKey.Lockscreen) + } + @Test fun deviceGoesToSleep_switchesToLockscreen() = testScope.runTest { @@ -235,6 +246,26 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { assertCurrentScene(SceneKey.Gone) } + @Test + fun deviceWakesUpWhileUnlocked_dismissesLockscreen() = + testScope.runTest { + unlockDevice() + assertCurrentScene(SceneKey.Gone) + putDeviceToSleep(instantlyLockDevice = false) + assertCurrentScene(SceneKey.Lockscreen) + wakeUpDevice() + assertCurrentScene(SceneKey.Gone) + } + + @Test + fun swipeUpOnLockscreenWhileUnlocked_dismissesLockscreen() = + testScope.runTest { + unlockDevice() + val upDestinationSceneKey by + collectLastValue(lockscreenSceneViewModel.upDestinationSceneKey) + assertThat(upDestinationSceneKey).isEqualTo(SceneKey.Gone) + } + @Test fun deviceGoesToSleep_withLockTimeout_staysOnLockscreen() = testScope.runTest { @@ -462,17 +493,4 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { lockDevice() } } - - private fun DomainLayerAuthenticationMethodModel.toDataLayer(): - DataLayerAuthenticationMethodModel { - return when (this) { - DomainLayerAuthenticationMethodModel.None -> DataLayerAuthenticationMethodModel.None - DomainLayerAuthenticationMethodModel.Swipe -> DataLayerAuthenticationMethodModel.None - DomainLayerAuthenticationMethodModel.Pin -> DataLayerAuthenticationMethodModel.Pin - DomainLayerAuthenticationMethodModel.Password -> - DataLayerAuthenticationMethodModel.Password - DomainLayerAuthenticationMethodModel.Pattern -> - DataLayerAuthenticationMethodModel.Pattern - } - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index 45db7a0b17f15..951cadd7664e5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -21,7 +21,7 @@ package com.android.systemui.scene.domain.startable import android.view.Display import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.authentication.data.model.AuthenticationMethodModel +import com.android.systemui.authentication.domain.model.AuthenticationMethodModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.Flags import com.android.systemui.keyguard.shared.model.WakeSleepReason @@ -29,6 +29,7 @@ import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.model.SysUiState import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.SceneTestUtils.Companion.toDataLayer import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel @@ -80,14 +81,13 @@ class SceneContainerStartableTest : SysuiTestCase() { ) @Test - fun hydrateVisibility_featureEnabled() = + fun hydrateVisibility() = testScope.runTest { val currentDesiredSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) val isVisible by collectLastValue(sceneInteractor.isVisible) val transitionStateFlow = prepareState( - isFeatureEnabled = true, isDeviceUnlocked = true, initialSceneKey = SceneKey.Gone, ) @@ -123,44 +123,10 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test - fun hydrateVisibility_featureDisabled() = - testScope.runTest { - val currentDesiredSceneKey by - collectLastValue(sceneInteractor.desiredScene.map { it.key }) - val isVisible by collectLastValue(sceneInteractor.isVisible) - val transitionStateFlow = - prepareState( - isFeatureEnabled = false, - isDeviceUnlocked = true, - initialSceneKey = SceneKey.Gone, - ) - assertThat(currentDesiredSceneKey).isEqualTo(SceneKey.Gone) - assertThat(isVisible).isTrue() - - underTest.start() - - assertThat(isVisible).isTrue() - - sceneInteractor.changeScene(SceneModel(SceneKey.Shade), "reason") - transitionStateFlow.value = - ObservableTransitionState.Transition( - fromScene = SceneKey.Gone, - toScene = SceneKey.Shade, - progress = flowOf(0.5f), - ) - assertThat(isVisible).isTrue() - - sceneInteractor.onSceneChanged(SceneModel(SceneKey.Shade), "reason") - transitionStateFlow.value = ObservableTransitionState.Idle(SceneKey.Shade) - assertThat(isVisible).isTrue() - } - - @Test - fun switchToLockscreenWhenDeviceLocks_featureEnabled() = + fun switchToLockscreenWhenDeviceLocks() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) prepareState( - isFeatureEnabled = true, isDeviceUnlocked = true, initialSceneKey = SceneKey.Gone, ) @@ -173,28 +139,10 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test - fun switchToLockscreenWhenDeviceLocks_featureDisabled() = + fun switchFromBouncerToGoneWhenDeviceUnlocked() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) prepareState( - isFeatureEnabled = false, - isDeviceUnlocked = false, - initialSceneKey = SceneKey.Gone, - ) - assertThat(currentSceneKey).isEqualTo(SceneKey.Gone) - underTest.start() - - authenticationRepository.setUnlocked(false) - - assertThat(currentSceneKey).isEqualTo(SceneKey.Gone) - } - - @Test - fun switchFromBouncerToGoneWhenDeviceUnlocked_featureEnabled() = - testScope.runTest { - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) - prepareState( - isFeatureEnabled = true, isDeviceUnlocked = false, initialSceneKey = SceneKey.Bouncer, ) @@ -207,28 +155,10 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test - fun switchFromBouncerToGoneWhenDeviceUnlocked_featureDisabled() = + fun switchFromLockscreenToGoneWhenDeviceUnlocksWithBypassOn() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) prepareState( - isFeatureEnabled = false, - isDeviceUnlocked = false, - initialSceneKey = SceneKey.Bouncer, - ) - assertThat(currentSceneKey).isEqualTo(SceneKey.Bouncer) - underTest.start() - - authenticationRepository.setUnlocked(true) - - assertThat(currentSceneKey).isEqualTo(SceneKey.Bouncer) - } - - @Test - fun switchFromLockscreenToGoneWhenDeviceUnlocksWithBypassOn_featureOn_bypassOn() = - testScope.runTest { - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) - prepareState( - isFeatureEnabled = true, isBypassEnabled = true, initialSceneKey = SceneKey.Lockscreen, ) @@ -241,11 +171,10 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test - fun switchFromLockscreenToGoneWhenDeviceUnlocksWithBypassOn_featureOn_bypassOff() = + fun stayOnLockscreenWhenDeviceUnlocksWithBypassOff() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) prepareState( - isFeatureEnabled = true, isBypassEnabled = false, initialSceneKey = SceneKey.Lockscreen, ) @@ -258,28 +187,10 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test - fun switchFromLockscreenToGoneWhenDeviceUnlocksWithBypassOn_featureOff_bypassOn() = + fun switchToLockscreenWhenDeviceSleepsLocked() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) prepareState( - isFeatureEnabled = false, - isBypassEnabled = true, - initialSceneKey = SceneKey.Lockscreen, - ) - assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) - underTest.start() - - authenticationRepository.setUnlocked(true) - - assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) - } - - @Test - fun switchToLockscreenWhenDeviceSleepsLocked_featureEnabled() = - testScope.runTest { - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) - prepareState( - isFeatureEnabled = true, isDeviceUnlocked = false, initialSceneKey = SceneKey.Shade, ) @@ -291,23 +202,6 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) } - @Test - fun switchToLockscreenWhenDeviceSleepsLocked_featureDisabled() = - testScope.runTest { - val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) - prepareState( - isFeatureEnabled = false, - isDeviceUnlocked = false, - initialSceneKey = SceneKey.Shade, - ) - assertThat(currentSceneKey).isEqualTo(SceneKey.Shade) - underTest.start() - - keyguardRepository.setWakefulnessModel(STARTING_TO_SLEEP) - - assertThat(currentSceneKey).isEqualTo(SceneKey.Shade) - } - @Test fun hydrateSystemUiState() = testScope.runTest { @@ -339,11 +233,10 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test - fun switchToGoneWhenDeviceStartsToWakeUp_authMethodNone_featureEnabled() = + fun switchToGoneWhenDeviceStartsToWakeUp_authMethodNone() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) prepareState( - isFeatureEnabled = true, initialSceneKey = SceneKey.Lockscreen, authenticationMethod = AuthenticationMethodModel.None, ) @@ -356,11 +249,26 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test - fun switchToGoneWhenDeviceStartsToWakeUp_authMethodNotNone_featureEnabled() = + fun stayOnLockscreenWhenDeviceStartsToWakeUp_authMethodSwipe() = + testScope.runTest { + val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) + prepareState( + initialSceneKey = SceneKey.Lockscreen, + authenticationMethod = AuthenticationMethodModel.Swipe, + ) + assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) + underTest.start() + + keyguardRepository.setWakefulnessModel(STARTING_TO_WAKE) + + assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) + } + + @Test + fun doesNotSwitchToGoneWhenDeviceStartsToWakeUp_authMethodSecure() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) prepareState( - isFeatureEnabled = true, initialSceneKey = SceneKey.Lockscreen, authenticationMethod = AuthenticationMethodModel.Pin, ) @@ -373,30 +281,31 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test - fun switchToGoneWhenDeviceStartsToWakeUp_authMethodNone_featureDisabled() = + fun switchToGoneWhenDeviceStartsToWakeUp_authMethodSecure_deviceUnlocked() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) prepareState( - isFeatureEnabled = false, initialSceneKey = SceneKey.Lockscreen, - authenticationMethod = AuthenticationMethodModel.None, + authenticationMethod = AuthenticationMethodModel.Pin, + isDeviceUnlocked = false, ) assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) underTest.start() + authenticationRepository.setUnlocked(true) + runCurrent() keyguardRepository.setWakefulnessModel(STARTING_TO_WAKE) - assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) + assertThat(currentSceneKey).isEqualTo(SceneKey.Gone) } private fun prepareState( - isFeatureEnabled: Boolean = true, isDeviceUnlocked: Boolean = false, isBypassEnabled: Boolean = false, initialSceneKey: SceneKey? = null, authenticationMethod: AuthenticationMethodModel? = null, ): MutableStateFlow { - featureFlags.set(Flags.SCENE_CONTAINER, isFeatureEnabled) + featureFlags.set(Flags.SCENE_CONTAINER, true) authenticationRepository.setUnlocked(isDeviceUnlocked) keyguardRepository.setBypassEnabled(isBypassEnabled) val transitionStateFlow = @@ -410,7 +319,7 @@ class SceneContainerStartableTest : SysuiTestCase() { sceneInteractor.onSceneChanged(SceneModel(it), "reason") } authenticationMethod?.let { - authenticationRepository.setAuthenticationMethod(authenticationMethod) + authenticationRepository.setAuthenticationMethod(authenticationMethod.toDataLayer()) authenticationRepository.setLockscreenEnabled( authenticationMethod != AuthenticationMethodModel.None ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt index 0829f31e3890d..893bbf38ff265 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt @@ -18,9 +18,11 @@ package com.android.systemui.scene import android.content.pm.UserInfo import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.model.AuthenticationMethodModel as DataLayerAuthenticationMethodModel import com.android.systemui.authentication.data.repository.AuthenticationRepository import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainLayerAuthenticationMethodModel import com.android.systemui.bouncer.data.repository.BouncerRepository import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository import com.android.systemui.bouncer.domain.interactor.BouncerInteractor @@ -201,5 +203,18 @@ class SceneTestUtils( RemoteUserInput(10f, 40f, RemoteUserInputAction.MOVE), RemoteUserInput(10f, 40f, RemoteUserInputAction.UP), ) + + fun DomainLayerAuthenticationMethodModel.toDataLayer(): DataLayerAuthenticationMethodModel { + return when (this) { + DomainLayerAuthenticationMethodModel.None -> DataLayerAuthenticationMethodModel.None + DomainLayerAuthenticationMethodModel.Swipe -> + DataLayerAuthenticationMethodModel.None + DomainLayerAuthenticationMethodModel.Pin -> DataLayerAuthenticationMethodModel.Pin + DomainLayerAuthenticationMethodModel.Password -> + DataLayerAuthenticationMethodModel.Password + DomainLayerAuthenticationMethodModel.Pattern -> + DataLayerAuthenticationMethodModel.Pattern + } + } } }