From 778b918db21efd1a7c3358c7e54cff71e1cba8c5 Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Mon, 10 Jul 2023 16:43:15 -0700 Subject: [PATCH] [flexiglass] Centralizes automatic scene changing business logic. We had automatic scene changing business logic spread out amongst various scene-specific interactors. This was suboptimal for a few reasons: 1. Hard to reason about what changes the scenes and when, when it's all distributed 2. Hard to gate this logic based on the feature flag; causing memory and boot time regressions as seen in the attached files, because more resources were being used at startup even when the feature was off This CL moves business logic from LockScreenSceneInteractor and BouncerInteractor into SystemUiDefaultSceneContainerStartable and makes sure it's all gated behind the feature flag. Fix: 280883900, 289353099, 289726647 Test: new unit tests added Test: manually verified in system UI that locking the device moves back to the Lockscreen scene and unlocking the device moves to the Gone scene Change-Id: I1dc46fc2d1d71f1b9baddc822ef69f5ea7b21cfc --- .../domain/interactor/BouncerInteractor.kt | 40 +--- .../bouncer/ui/viewmodel/BouncerViewModel.kt | 85 +++---- .../interactor/LockscreenSceneInteractor.kt | 47 ---- .../SystemUiDefaultSceneContainerStartable.kt | 53 ++++- .../interactor/BouncerInteractorTest.kt | 17 -- .../ui/viewmodel/BouncerViewModelTest.kt | 5 +- .../ui/viewmodel/PinBouncerViewModelTest.kt | 13 +- .../LockscreenSceneInteractorTest.kt | 17 -- .../viewmodel/LockscreenSceneViewModelTest.kt | 1 - .../QuickSettingsSceneViewModelTest.kt | 1 - ...temUiDefaultSceneContainerStartableTest.kt | 222 +++++++++++++++++- .../ui/viewmodel/ShadeSceneViewModelTest.kt | 8 +- .../android/systemui/scene/SceneTestUtils.kt | 7 +- 13 files changed, 328 insertions(+), 188 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt index 2513c81c17f8d..62a484d42dec3 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt @@ -25,6 +25,8 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel import com.android.systemui.bouncer.data.repository.BouncerRepository import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel @@ -38,9 +40,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -53,6 +52,7 @@ constructor( private val repository: BouncerRepository, private val authenticationInteractor: AuthenticationInteractor, private val sceneInteractor: SceneInteractor, + featureFlags: FeatureFlags, @Assisted private val containerName: String, ) { @@ -95,36 +95,14 @@ constructor( val isPatternVisible: StateFlow = authenticationInteractor.isPatternVisible init { - // UNLOCKING SHOWS Gone. - // - // Move to the gone scene if the device becomes unlocked while on the bouncer scene. - applicationScope.launch { - sceneInteractor - .currentScene(containerName) - .flatMapLatest { currentScene -> - if (currentScene.key == SceneKey.Bouncer) { - authenticationInteractor.isUnlocked - } else { - flowOf(false) + if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) { + // Clear the message if moved from throttling to no-longer throttling. + applicationScope.launch { + isThrottled.pairwise().collect { (wasThrottled, currentlyThrottled) -> + if (wasThrottled && !currentlyThrottled) { + clearMessage() } } - .distinctUntilChanged() - .collect { isUnlocked -> - if (isUnlocked) { - sceneInteractor.setCurrentScene( - containerName = containerName, - scene = SceneModel(SceneKey.Gone), - ) - } - } - } - - // Clear the message if moved from throttling to no-longer throttling. - applicationScope.launch { - isThrottled.pairwise().collect { (wasThrottled, currentlyThrottled) -> - if (wasThrottled && !currentlyThrottled) { - clearMessage() - } } } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt index 5425022e11b78..a4ef5cec65258 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt @@ -23,6 +23,8 @@ import com.android.systemui.R import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.util.kotlin.pairwise import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -49,6 +51,7 @@ constructor( @Application private val applicationContext: Context, @Application private val applicationScope: CoroutineScope, interactorFactory: BouncerInteractor.Factory, + featureFlags: FeatureFlags, @Assisted containerName: String, ) { private val interactor: BouncerInteractor = interactorFactory.create(containerName) @@ -102,15 +105,48 @@ constructor( ) init { - applicationScope.launch { - _authMethod.subscriptionCount - .pairwise() - .map { (previousCount, currentCount) -> currentCount > previousCount } - .collect { subscriberAdded -> - if (subscriberAdded) { - reloadAuthMethod() + if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) { + applicationScope.launch { + interactor.isThrottled + .map { isThrottled -> + if (isThrottled) { + when (interactor.getAuthenticationMethod()) { + is AuthenticationMethodModel.Pin -> + R.string.kg_too_many_failed_pin_attempts_dialog_message + is AuthenticationMethodModel.Password -> + R.string.kg_too_many_failed_password_attempts_dialog_message + is AuthenticationMethodModel.Pattern -> + R.string.kg_too_many_failed_pattern_attempts_dialog_message + else -> null + }?.let { stringResourceId -> + applicationContext.getString( + stringResourceId, + interactor.throttling.value.failedAttemptCount, + ceil(interactor.throttling.value.remainingMs / 1000f).toInt(), + ) + } + } else { + null + } } - } + .distinctUntilChanged() + .collect { dialogMessageOrNull -> + if (dialogMessageOrNull != null) { + _throttlingDialogMessage.value = dialogMessageOrNull + } + } + } + + applicationScope.launch { + _authMethod.subscriptionCount + .pairwise() + .map { (previousCount, currentCount) -> currentCount > previousCount } + .collect { subscriberAdded -> + if (subscriberAdded) { + reloadAuthMethod() + } + } + } } } @@ -144,39 +180,6 @@ constructor( */ val throttlingDialogMessage: StateFlow = _throttlingDialogMessage.asStateFlow() - init { - applicationScope.launch { - interactor.isThrottled - .map { isThrottled -> - if (isThrottled) { - when (interactor.getAuthenticationMethod()) { - is AuthenticationMethodModel.Pin -> - R.string.kg_too_many_failed_pin_attempts_dialog_message - is AuthenticationMethodModel.Password -> - R.string.kg_too_many_failed_password_attempts_dialog_message - is AuthenticationMethodModel.Pattern -> - R.string.kg_too_many_failed_pattern_attempts_dialog_message - else -> null - }?.let { stringResourceId -> - applicationContext.getString( - stringResourceId, - interactor.throttling.value.failedAttemptCount, - ceil(interactor.throttling.value.remainingMs / 1000f).toInt(), - ) - } - } else { - null - } - } - .distinctUntilChanged() - .collect { dialogMessageOrNull -> - if (dialogMessageOrNull != null) { - _throttlingDialogMessage.value = dialogMessageOrNull - } - } - } - } - /** Notifies that the emergency services button was clicked. */ fun onEmergencyServicesButtonClicked() { // TODO(b/280877228): implement this diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractor.kt index c8f7efbeb3979..1c200b0869900 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractor.kt @@ -20,20 +20,14 @@ import com.android.systemui.authentication.domain.interactor.AuthenticationInter import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.scene.domain.interactor.SceneInteractor -import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.scene.shared.model.SceneModel import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch /** Hosts business and application state accessing logic for the lockscreen scene. */ class LockscreenSceneInteractor @@ -42,7 +36,6 @@ constructor( @Application applicationScope: CoroutineScope, private val authenticationInteractor: AuthenticationInteractor, bouncerInteractorFactory: BouncerInteractor.Factory, - private val sceneInteractor: SceneInteractor, @Assisted private val containerName: String, ) { private val bouncerInteractor: BouncerInteractor = @@ -72,46 +65,6 @@ constructor( initialValue = false, ) - init { - // LOCKING SHOWS Lockscreen. - // - // Move to the lockscreen scene if the device becomes locked while in any scene. - applicationScope.launch { - authenticationInteractor.isUnlocked - .map { !it } - .distinctUntilChanged() - .collect { isLocked -> - if (isLocked) { - sceneInteractor.setCurrentScene( - containerName = containerName, - scene = SceneModel(SceneKey.Lockscreen), - ) - } - } - } - - // BYPASS UNLOCK. - // - // Moves to the gone scene if bypass is enabled and the device becomes unlocked while in the - // lockscreen scene. - applicationScope.launch { - combine( - authenticationInteractor.isBypassEnabled, - authenticationInteractor.isUnlocked, - sceneInteractor.currentScene(containerName), - ::Triple, - ) - .collect { (isBypassEnabled, isUnlocked, currentScene) -> - if (isBypassEnabled && isUnlocked && currentScene.key == SceneKey.Lockscreen) { - sceneInteractor.setCurrentScene( - containerName = containerName, - scene = SceneModel(SceneKey.Gone), - ) - } - } - } - } - /** Attempts to dismiss the lockscreen. This will cause the bouncer to show, if needed. */ fun dismissLockscreen() { bouncerInteractor.showOrUnlockDevice(containerName = containerName) diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt index 91400196bbb16..59f82f0347234 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt @@ -17,6 +17,7 @@ package com.android.systemui.scene.domain.startable import com.android.systemui.CoreStartable +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.flags.FeatureFlags @@ -24,9 +25,11 @@ import com.android.systemui.flags.Flags import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.SceneContainerNames import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -41,17 +44,19 @@ class SystemUiDefaultSceneContainerStartable constructor( @Application private val applicationScope: CoroutineScope, private val sceneInteractor: SceneInteractor, + private val authenticationInteractor: AuthenticationInteractor, private val featureFlags: FeatureFlags, ) : CoreStartable { override fun start() { if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) { - keepVisibilityUpdated() + hydrateVisibility() + automaticallySwitchScenes() } } - /** Drives visibility of the scene container. */ - private fun keepVisibilityUpdated() { + /** Updates the visibility of the scene container based on the current scene. */ + private fun hydrateVisibility() { applicationScope.launch { sceneInteractor .currentScene(CONTAINER_NAME) @@ -63,6 +68,48 @@ constructor( } } + /** Switches between scenes based on ever-changing application state. */ + private fun automaticallySwitchScenes() { + applicationScope.launch { + authenticationInteractor.isUnlocked + .map { isUnlocked -> + val currentSceneKey = sceneInteractor.currentScene(CONTAINER_NAME).value.key + val isBypassEnabled = authenticationInteractor.isBypassEnabled.value + when { + isUnlocked -> + when (currentSceneKey) { + // When the device becomes unlocked in Bouncer, go to the Gone. + is SceneKey.Bouncer -> SceneKey.Gone + // When the device becomes unlocked in Lockscreen, go to Gone if + // bypass is enabled. + is SceneKey.Lockscreen -> SceneKey.Gone.takeIf { isBypassEnabled } + // We got unlocked while on a scene that's not Lockscreen or + // Bouncer, no need to change scenes. + else -> null + } + // When the device becomes locked, to Lockscreen. + !isUnlocked -> + when (currentSceneKey) { + // Already on lockscreen or bouncer, no need to change scenes. + is SceneKey.Lockscreen, + is SceneKey.Bouncer -> null + // We got locked while on a scene that's not Lockscreen or Bouncer, + // go to Lockscreen. + else -> SceneKey.Lockscreen + } + else -> null + } + } + .filterNotNull() + .collect { targetSceneKey -> + sceneInteractor.setCurrentScene( + containerName = CONTAINER_NAME, + scene = SceneModel(targetSceneKey), + ) + } + } + } + companion object { private const val CONTAINER_NAME = SceneContainerNames.SYSTEM_UI_DEFAULT } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt index c2219a4d82ebf..481f36e8ea388 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt @@ -345,23 +345,6 @@ class BouncerInteractorTest : SysuiTestCase() { assertThat(throttling).isEqualTo(AuthenticationThrottlingModel()) } - @Test - fun switchesToGone_whenUnlocked() = - testScope.runTest { - utils.authenticationRepository.setUnlocked(false) - sceneInteractor.setCurrentScene( - SceneTestUtils.CONTAINER_1, - SceneModel(SceneKey.Bouncer) - ) - val currentScene by - collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) - - utils.authenticationRepository.setUnlocked(true) - - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) - } - private fun assertTryAgainMessage( message: String?, time: Int, diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt index 0356036fa78f1..0df0a17931f47 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt @@ -52,7 +52,10 @@ class BouncerViewModelTest : SysuiTestCase() { authenticationInteractor = authenticationInteractor, sceneInteractor = utils.sceneInteractor(), ) - private val underTest = utils.bouncerViewModel(bouncerInteractor) + private val underTest = + utils.bouncerViewModel( + bouncerInteractor = bouncerInteractor, + ) @Test fun authMethod_nonNullForSecureMethods_nullForNotSecureMethods() = diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt index 7d6c4a1a1455f..5c6d4c69b50b0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt @@ -20,7 +20,6 @@ import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.shared.model.AuthenticationMethodModel -import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.shared.model.SceneKey @@ -54,16 +53,8 @@ class PinBouncerViewModelTest : SysuiTestCase() { sceneInteractor = sceneInteractor, ) private val bouncerViewModel = - BouncerViewModel( - applicationContext = context, - applicationScope = testScope.backgroundScope, - interactorFactory = - object : BouncerInteractor.Factory { - override fun create(containerName: String): BouncerInteractor { - return bouncerInteractor - } - }, - containerName = SceneTestUtils.CONTAINER_1, + utils.bouncerViewModel( + bouncerInteractor = bouncerInteractor, ) private val underTest = PinBouncerViewModel( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractorTest.kt index 8b36284b8620c..ca6a5b6234b9f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractorTest.kt @@ -47,7 +47,6 @@ class LockscreenSceneInteractorTest : SysuiTestCase() { private val underTest = utils.lockScreenSceneInteractor( authenticationInteractor = authenticationInteractor, - sceneInteractor = sceneInteractor, bouncerInteractor = utils.bouncerInteractor( authenticationInteractor = authenticationInteractor, @@ -128,22 +127,6 @@ class LockscreenSceneInteractorTest : SysuiTestCase() { assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) } - @Test - fun deviceLockedInNonLockScreenScene_switchesToLockScreenScene() = - testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) - runCurrent() - sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Gone)) - runCurrent() - utils.authenticationRepository.setUnlocked(true) - runCurrent() - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) - - utils.authenticationRepository.setUnlocked(false) - - assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) - } - @Test fun switchFromLockScreenToGone_authMethodNotSwipe_doesNotUnlockDevice() = testScope.runTest { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt index 161dd660671e2..ba8e0f277b6b8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt @@ -56,7 +56,6 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { override fun create(containerName: String): LockscreenSceneInteractor { return utils.lockScreenSceneInteractor( authenticationInteractor = authenticationInteractor, - sceneInteractor = sceneInteractor, bouncerInteractor = utils.bouncerInteractor( authenticationInteractor = authenticationInteractor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt index 4eedc99839c6e..ed7a59ea70329 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt @@ -53,7 +53,6 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { override fun create(containerName: String): LockscreenSceneInteractor { return utils.lockScreenSceneInteractor( authenticationInteractor = authenticationInteractor, - sceneInteractor = sceneInteractor, bouncerInteractor = utils.bouncerInteractor( authenticationInteractor = authenticationInteractor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt index df3701edc6ded..df5e7bcd16be3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt @@ -14,14 +14,11 @@ * limitations under the License. */ -@file:OptIn(ExperimentalCoroutinesApi::class, ExperimentalCoroutinesApi::class) - package com.android.systemui.scene.domain.startable import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.shared.model.SceneContainerNames @@ -29,11 +26,14 @@ import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.map import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) class SystemUiDefaultSceneContainerStartableTest : SysuiTestCase() { @@ -41,29 +41,47 @@ class SystemUiDefaultSceneContainerStartableTest : SysuiTestCase() { private val utils = SceneTestUtils(this) private val testScope = utils.testScope private val sceneInteractor = utils.sceneInteractor() - private val featureFlags = FakeFeatureFlags() + private val featureFlags = utils.featureFlags + private val authenticationRepository = utils.authenticationRepository() + private val authenticationInteractor = + utils.authenticationInteractor( + repository = authenticationRepository, + ) private val underTest = SystemUiDefaultSceneContainerStartable( applicationScope = testScope.backgroundScope, sceneInteractor = sceneInteractor, + authenticationInteractor = authenticationInteractor, featureFlags = featureFlags, ) + @Before + fun setUp() { + prepareState() + } + @Test - fun start_featureEnabled_keepsVisibilityUpdated() = + fun hydrateVisibility_featureEnabled() = testScope.runTest { - featureFlags.set(Flags.SCENE_CONTAINER, true) + val currentSceneKey by + collectLastValue( + sceneInteractor.currentScene(SceneContainerNames.SYSTEM_UI_DEFAULT).map { + it.key + } + ) val isVisible by collectLastValue(sceneInteractor.isVisible(SceneContainerNames.SYSTEM_UI_DEFAULT)) + prepareState( + isFeatureEnabled = true, + isDeviceUnlocked = true, + initialSceneKey = SceneKey.Gone, + ) + assertThat(currentSceneKey).isEqualTo(SceneKey.Gone) assertThat(isVisible).isTrue() underTest.start() - sceneInteractor.setCurrentScene( - SceneContainerNames.SYSTEM_UI_DEFAULT, - SceneModel(SceneKey.Gone) - ) assertThat(isVisible).isFalse() sceneInteractor.setCurrentScene( @@ -74,14 +92,26 @@ class SystemUiDefaultSceneContainerStartableTest : SysuiTestCase() { } @Test - fun start_featureDisabled_doesNotUpdateVisibility() = + fun hydrateVisibility_featureDisabled() = testScope.runTest { - featureFlags.set(Flags.SCENE_CONTAINER, false) + val currentSceneKey by + collectLastValue( + sceneInteractor.currentScene(SceneContainerNames.SYSTEM_UI_DEFAULT).map { + it.key + } + ) val isVisible by collectLastValue(sceneInteractor.isVisible(SceneContainerNames.SYSTEM_UI_DEFAULT)) + prepareState( + isFeatureEnabled = false, + isDeviceUnlocked = true, + initialSceneKey = SceneKey.Lockscreen, + ) + assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) assertThat(isVisible).isTrue() underTest.start() + assertThat(isVisible).isTrue() sceneInteractor.setCurrentScene( SceneContainerNames.SYSTEM_UI_DEFAULT, @@ -95,4 +125,172 @@ class SystemUiDefaultSceneContainerStartableTest : SysuiTestCase() { ) assertThat(isVisible).isTrue() } + + @Test + fun switchToLockscreenWhenDeviceLocks_featureEnabled() = + testScope.runTest { + val currentSceneKey by + collectLastValue( + sceneInteractor.currentScene(SceneContainerNames.SYSTEM_UI_DEFAULT).map { + it.key + } + ) + prepareState( + isFeatureEnabled = true, + isDeviceUnlocked = true, + initialSceneKey = SceneKey.Gone, + ) + assertThat(currentSceneKey).isEqualTo(SceneKey.Gone) + underTest.start() + + authenticationRepository.setUnlocked(false) + + assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) + } + + @Test + fun switchToLockscreenWhenDeviceLocks_featureDisabled() = + testScope.runTest { + val currentSceneKey by + collectLastValue( + sceneInteractor.currentScene(SceneContainerNames.SYSTEM_UI_DEFAULT).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.currentScene(SceneContainerNames.SYSTEM_UI_DEFAULT).map { + it.key + } + ) + prepareState( + isFeatureEnabled = true, + isDeviceUnlocked = false, + initialSceneKey = SceneKey.Bouncer, + ) + assertThat(currentSceneKey).isEqualTo(SceneKey.Bouncer) + underTest.start() + + authenticationRepository.setUnlocked(true) + + assertThat(currentSceneKey).isEqualTo(SceneKey.Gone) + } + + @Test + fun switchFromBouncerToGoneWhenDeviceUnlocked_featureDisabled() = + testScope.runTest { + val currentSceneKey by + collectLastValue( + sceneInteractor.currentScene(SceneContainerNames.SYSTEM_UI_DEFAULT).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.currentScene(SceneContainerNames.SYSTEM_UI_DEFAULT).map { + it.key + } + ) + prepareState( + isFeatureEnabled = true, + isBypassEnabled = true, + initialSceneKey = SceneKey.Lockscreen, + ) + assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) + underTest.start() + + authenticationRepository.setUnlocked(true) + + assertThat(currentSceneKey).isEqualTo(SceneKey.Gone) + } + + @Test + fun switchFromLockscreenToGoneWhenDeviceUnlocksWithBypassOn_featureOn_bypassOff() = + testScope.runTest { + val currentSceneKey by + collectLastValue( + sceneInteractor.currentScene(SceneContainerNames.SYSTEM_UI_DEFAULT).map { + it.key + } + ) + prepareState( + isFeatureEnabled = true, + isBypassEnabled = false, + initialSceneKey = SceneKey.Lockscreen, + ) + assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) + underTest.start() + + authenticationRepository.setUnlocked(true) + + assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) + } + + @Test + fun switchFromLockscreenToGoneWhenDeviceUnlocksWithBypassOn_featureOff_bypassOn() = + testScope.runTest { + val currentSceneKey by + collectLastValue( + sceneInteractor.currentScene(SceneContainerNames.SYSTEM_UI_DEFAULT).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) + } + + private fun prepareState( + isFeatureEnabled: Boolean = true, + isDeviceUnlocked: Boolean = false, + isBypassEnabled: Boolean = false, + initialSceneKey: SceneKey? = null, + ) { + featureFlags.set(Flags.SCENE_CONTAINER, isFeatureEnabled) + authenticationRepository.setUnlocked(isDeviceUnlocked) + authenticationRepository.setBypassEnabled(isBypassEnabled) + initialSceneKey?.let { + sceneInteractor.setCurrentScene(SceneContainerNames.SYSTEM_UI_DEFAULT, SceneModel(it)) + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt index 309ab058fb64c..6e9fba64263b9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt @@ -22,7 +22,6 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.domain.interactor.LockscreenSceneInteractor import com.android.systemui.scene.SceneTestUtils -import com.android.systemui.scene.SceneTestUtils.Companion.CONTAINER_1 import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat @@ -54,7 +53,6 @@ class ShadeSceneViewModelTest : SysuiTestCase() { override fun create(containerName: String): LockscreenSceneInteractor { return utils.lockScreenSceneInteractor( authenticationInteractor = authenticationInteractor, - sceneInteractor = sceneInteractor, bouncerInteractor = utils.bouncerInteractor( authenticationInteractor = authenticationInteractor, @@ -89,7 +87,8 @@ class ShadeSceneViewModelTest : SysuiTestCase() { @Test fun onContentClicked_deviceUnlocked_switchesToGone() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(true) runCurrent() @@ -102,7 +101,8 @@ class ShadeSceneViewModelTest : SysuiTestCase() { @Test fun onContentClicked_deviceLockedSecurely_switchesToBouncer() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) runCurrent() 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 fec1187d8d110..d797962bae8b8 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 @@ -24,6 +24,8 @@ import com.android.systemui.authentication.domain.interactor.AuthenticationInter import com.android.systemui.bouncer.data.repository.BouncerRepository import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.domain.interactor.LockscreenSceneInteractor import com.android.systemui.scene.data.repository.SceneContainerRepository import com.android.systemui.scene.domain.interactor.SceneInteractor @@ -51,6 +53,7 @@ class SceneTestUtils( ) { val testDispatcher = StandardTestDispatcher() val testScope = TestScope(testDispatcher) + val featureFlags = FakeFeatureFlags().apply { set(Flags.SCENE_CONTAINER, true) } private val userRepository: UserRepository by lazy { FakeUserRepository().apply { val users = listOf(UserInfo(/* id= */ 0, "name", /* flags= */ 0)) @@ -129,6 +132,7 @@ class SceneTestUtils( repository = BouncerRepository(), authenticationInteractor = authenticationInteractor, sceneInteractor = sceneInteractor, + featureFlags = featureFlags, containerName = CONTAINER_1, ) } @@ -145,13 +149,13 @@ class SceneTestUtils( return bouncerInteractor } }, + featureFlags = featureFlags, containerName = CONTAINER_1, ) } fun lockScreenSceneInteractor( authenticationInteractor: AuthenticationInteractor, - sceneInteractor: SceneInteractor, bouncerInteractor: BouncerInteractor, ): LockscreenSceneInteractor { return LockscreenSceneInteractor( @@ -163,7 +167,6 @@ class SceneTestUtils( return bouncerInteractor } }, - sceneInteractor = sceneInteractor, containerName = CONTAINER_1, ) }