diff options
24 files changed, 901 insertions, 727 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt index f655ac1d207b..d164eab5afeb 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt @@ -95,7 +95,7 @@ import com.android.systemui.bouncer.ui.BouncerDialogFactory import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.BouncerMessageViewModel -import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel import com.android.systemui.bouncer.ui.viewmodel.MessageViewModel import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel @@ -114,7 +114,7 @@ import platform.test.motion.compose.values.motionTestValues @Composable fun BouncerContent( - viewModel: BouncerViewModel, + viewModel: BouncerSceneContentViewModel, dialogFactory: BouncerDialogFactory, modifier: Modifier = Modifier, ) { @@ -128,7 +128,7 @@ fun BouncerContent( @VisibleForTesting fun BouncerContent( layout: BouncerSceneLayout, - viewModel: BouncerViewModel, + viewModel: BouncerSceneContentViewModel, dialogFactory: BouncerDialogFactory, modifier: Modifier ) { @@ -173,7 +173,7 @@ fun BouncerContent( */ @Composable private fun StandardLayout( - viewModel: BouncerViewModel, + viewModel: BouncerSceneContentViewModel, modifier: Modifier = Modifier, ) { val isHeightExpanded = @@ -235,7 +235,7 @@ private fun StandardLayout( */ @Composable private fun SplitLayout( - viewModel: BouncerViewModel, + viewModel: BouncerSceneContentViewModel, modifier: Modifier = Modifier, ) { val authMethod by viewModel.authMethodViewModel.collectAsStateWithLifecycle() @@ -326,7 +326,7 @@ private fun SplitLayout( */ @Composable private fun BesideUserSwitcherLayout( - viewModel: BouncerViewModel, + viewModel: BouncerSceneContentViewModel, modifier: Modifier = Modifier, ) { val layoutDirection = LocalLayoutDirection.current @@ -461,7 +461,7 @@ private fun BesideUserSwitcherLayout( /** Arranges the bouncer contents and user switcher contents one on top of the other, vertically. */ @Composable private fun BelowUserSwitcherLayout( - viewModel: BouncerViewModel, + viewModel: BouncerSceneContentViewModel, modifier: Modifier = Modifier, ) { Column( @@ -506,7 +506,7 @@ private fun BelowUserSwitcherLayout( @Composable private fun FoldAware( - viewModel: BouncerViewModel, + viewModel: BouncerSceneContentViewModel, aboveFold: @Composable BoxScope.() -> Unit, belowFold: @Composable BoxScope.() -> Unit, modifier: Modifier = Modifier, @@ -649,7 +649,7 @@ private fun StatusMessage( */ @Composable private fun OutputArea( - viewModel: BouncerViewModel, + viewModel: BouncerSceneContentViewModel, modifier: Modifier = Modifier, ) { val authMethodViewModel: AuthMethodBouncerViewModel? by @@ -677,7 +677,7 @@ private fun OutputArea( */ @Composable private fun InputArea( - viewModel: BouncerViewModel, + viewModel: BouncerSceneContentViewModel, pinButtonRowVerticalSpacing: Dp, centerPatternDotsVertically: Boolean, modifier: Modifier = Modifier, @@ -706,7 +706,7 @@ private fun InputArea( @Composable private fun ActionArea( - viewModel: BouncerViewModel, + viewModel: BouncerSceneContentViewModel, modifier: Modifier = Modifier, ) { val actionButton: BouncerActionButtonModel? by @@ -774,7 +774,7 @@ private fun ActionArea( @Composable private fun Dialog( - bouncerViewModel: BouncerViewModel, + bouncerViewModel: BouncerSceneContentViewModel, dialogFactory: BouncerDialogFactory, ) { val dialogViewModel by bouncerViewModel.dialogViewModel.collectAsStateWithLifecycle() @@ -803,7 +803,7 @@ private fun Dialog( /** Renders the UI of the user switcher that's displayed on large screens next to the bouncer UI. */ @Composable private fun UserSwitcher( - viewModel: BouncerViewModel, + viewModel: BouncerSceneContentViewModel, modifier: Modifier = Modifier, ) { if (!viewModel.isUserSwitcherVisible) { @@ -884,7 +884,7 @@ private fun UserSwitcher( @Composable private fun UserSwitcherDropdownMenu( isExpanded: Boolean, - items: List<BouncerViewModel.UserSwitcherDropdownItemViewModel>, + items: List<BouncerSceneContentViewModel.UserSwitcherDropdownItemViewModel>, onDismissed: () -> Unit, ) { val context = LocalContext.current diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt index 9fd30b499595..3a46882c0ab9 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt @@ -27,9 +27,11 @@ import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.bouncer.ui.BouncerDialogFactory -import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneActionsViewModel +import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.ComposableScene import javax.inject.Inject @@ -51,23 +53,37 @@ object Bouncer { class BouncerScene @Inject constructor( - private val viewModel: BouncerViewModel, + private val actionsViewModelFactory: BouncerSceneActionsViewModel.Factory, + private val contentViewModelFactory: BouncerSceneContentViewModel.Factory, private val dialogFactory: BouncerDialogFactory, ) : ComposableScene { override val key = Scenes.Bouncer + private val actionsViewModel: BouncerSceneActionsViewModel by lazy { + actionsViewModelFactory.create() + } + override val destinationScenes: Flow<Map<UserAction, UserActionResult>> = - viewModel.destinationScenes + actionsViewModel.actions + + override suspend fun activate() { + actionsViewModel.activate() + } @Composable override fun SceneScope.Content( modifier: Modifier, - ) = BouncerScene(viewModel, dialogFactory, modifier) + ) = + BouncerScene( + viewModel = rememberViewModel { contentViewModelFactory.create() }, + dialogFactory = dialogFactory, + modifier = modifier, + ) } @Composable private fun SceneScope.BouncerScene( - viewModel: BouncerViewModel, + viewModel: BouncerSceneContentViewModel, dialogFactory: BouncerDialogFactory, modifier: Modifier = Modifier, ) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt index c9fa671ad34f..deef65218c4b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt @@ -22,14 +22,14 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel -import com.android.systemui.bouncer.domain.interactor.bouncerInteractor -import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -39,17 +39,16 @@ class AuthMethodBouncerViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val bouncerInteractor by lazy { kosmos.bouncerInteractor } - private val underTest by lazy { - PinBouncerViewModel( - applicationContext = context, - viewModelScope = testScope.backgroundScope, - interactor = bouncerInteractor, + private val underTest = + kosmos.pinBouncerViewModelFactory.create( isInputEnabled = MutableStateFlow(true), - simBouncerInteractor = kosmos.simBouncerInteractor, - authenticationMethod = AuthenticationMethodModel.Pin, onIntentionalUserInput = {}, + authenticationMethod = AuthenticationMethodModel.Pin, ) + + @Before + fun setUp() { + underTest.activateIn(testScope) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt index 4f5d0e58ce01..b83ab7ef0c1b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt @@ -52,6 +52,7 @@ import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthentication import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn import com.android.systemui.res.R import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.fakeUserRepository @@ -87,6 +88,7 @@ class BouncerMessageViewModelTest : SysuiTestCase() { intArrayOf(ignoreHelpMessageId) ) underTest = kosmos.bouncerMessageViewModel + underTest.activateIn(testScope) overrideResource(R.string.kg_trust_agent_disabled, "Trust agent is unavailable") kosmos.fakeSystemPropertiesHelper.set( DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModelTest.kt new file mode 100644 index 000000000000..a86a0c022c21 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModelTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 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. + */ + +package com.android.systemui.bouncer.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.Back +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.SwipeDirection +import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.scene.domain.startable.sceneContainerStartable +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.shared.model.fakeSceneDataSource +import com.android.systemui.testKosmos +import com.android.systemui.truth.containsEntriesExactly +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableSceneContainer +class BouncerSceneActionsViewModelTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private lateinit var underTest: BouncerSceneActionsViewModel + + @Before + fun setUp() { + kosmos.sceneContainerStartable.start() + underTest = kosmos.bouncerSceneActionsViewModel + underTest.activateIn(testScope) + } + + @Test + fun actions() = + testScope.runTest { + val actions by collectLastValue(underTest.actions) + kosmos.fakeSceneDataSource.changeScene(Scenes.QuickSettings) + runCurrent() + + kosmos.fakeSceneDataSource.changeScene(Scenes.Bouncer) + runCurrent() + + assertThat(actions) + .containsEntriesExactly( + Back to UserActionResult(Scenes.QuickSettings), + Swipe(SwipeDirection.Down) to UserActionResult(Scenes.QuickSettings), + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModelTest.kt index ccddc9c7120f..9bddcd254556 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModelTest.kt @@ -18,10 +18,6 @@ package com.android.systemui.bouncer.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.compose.animation.scene.Back -import com.android.compose.animation.scene.Swipe -import com.android.compose.animation.scene.SwipeDirection -import com.android.compose.animation.scene.UserActionResult import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository @@ -38,11 +34,9 @@ import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn import com.android.systemui.scene.domain.startable.sceneContainerStartable -import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.scene.shared.model.fakeSceneDataSource import com.android.systemui.testKosmos -import com.android.systemui.truth.containsEntriesExactly import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -62,17 +56,18 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) @EnableSceneContainer -class BouncerViewModelTest : SysuiTestCase() { +class BouncerSceneContentViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private lateinit var underTest: BouncerViewModel + private lateinit var underTest: BouncerSceneContentViewModel @Before fun setUp() { kosmos.sceneContainerStartable.start() - underTest = kosmos.bouncerViewModel + underTest = kosmos.bouncerSceneContentViewModel + underTest.activateIn(testScope) } @Test @@ -201,23 +196,6 @@ class BouncerViewModelTest : SysuiTestCase() { assertThat(isFoldSplitRequired).isTrue() } - @Test - fun destinationScenes() = - testScope.runTest { - val destinationScenes by collectLastValue(underTest.destinationScenes) - kosmos.fakeSceneDataSource.changeScene(Scenes.QuickSettings) - runCurrent() - - kosmos.fakeSceneDataSource.changeScene(Scenes.Bouncer) - runCurrent() - - assertThat(destinationScenes) - .containsEntriesExactly( - Back to UserActionResult(Scenes.QuickSettings), - Swipe(SwipeDirection.Down) to UserActionResult(Scenes.QuickSettings), - ) - } - private fun authMethodsToTest(): List<AuthenticationMethodModel> { return listOf(None, Pin, Password, Pattern, Sim) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt index a09189efa41b..492543f215b7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt @@ -31,6 +31,7 @@ import com.android.systemui.inputmethod.data.model.InputMethodModel import com.android.systemui.inputmethod.data.repository.fakeInputMethodRepository import com.android.systemui.inputmethod.domain.interactor.inputMethodInteractor import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes @@ -44,7 +45,6 @@ import java.util.UUID import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent @@ -68,12 +68,8 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { private val isInputEnabled = MutableStateFlow(true) private val underTest = - PasswordBouncerViewModel( - viewModelScope = testScope.backgroundScope, - isInputEnabled = isInputEnabled.asStateFlow(), - interactor = bouncerInteractor, - inputMethodInteractor = inputMethodInteractor, - selectedUserInteractor = selectedUserInteractor, + kosmos.passwordBouncerViewModelFactory.create( + isInputEnabled = isInputEnabled, onIntentionalUserInput = {}, ) @@ -81,6 +77,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { fun setUp() { overrideResource(R.string.keyguard_enter_your_password, ENTER_YOUR_PASSWORD) overrideResource(R.string.kg_wrong_password, WRONG_PASSWORD) + underTest.activateIn(testScope) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt index 14d36343041d..7c773a902367 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt @@ -26,9 +26,9 @@ import com.android.systemui.authentication.data.repository.fakeAuthenticationRep import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate as Point -import com.android.systemui.bouncer.domain.interactor.bouncerInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes @@ -54,17 +54,12 @@ class PatternBouncerViewModelTest : SysuiTestCase() { private val testScope = kosmos.testScope private val authenticationInteractor by lazy { kosmos.authenticationInteractor } private val sceneInteractor by lazy { kosmos.sceneInteractor } - private val bouncerInteractor by lazy { kosmos.bouncerInteractor } - private val bouncerViewModel by lazy { kosmos.bouncerViewModel } - private val underTest by lazy { - PatternBouncerViewModel( - applicationContext = context, - viewModelScope = testScope.backgroundScope, - interactor = bouncerInteractor, + private val bouncerViewModel by lazy { kosmos.bouncerSceneContentViewModel } + private val underTest = + kosmos.patternBouncerViewModelFactory.create( isInputEnabled = MutableStateFlow(true).asStateFlow(), onIntentionalUserInput = {}, ) - } private val containerSize = 90 // px private val dotSize = 30 // px @@ -73,6 +68,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() { fun setUp() { overrideResource(R.string.keyguard_enter_your_pattern, ENTER_YOUR_PATTERN) overrideResource(R.string.kg_wrong_pattern, WRONG_PATTERN) + underTest.activateIn(testScope) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt index 89bafb952211..8d82e972bdaa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt @@ -31,10 +31,9 @@ import com.android.systemui.authentication.data.repository.fakeAuthenticationRep import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.data.repository.fakeSimBouncerRepository -import com.android.systemui.bouncer.domain.interactor.bouncerInteractor -import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes @@ -44,7 +43,6 @@ import kotlin.random.Random import kotlin.random.nextInt import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent @@ -62,24 +60,18 @@ class PinBouncerViewModelTest : SysuiTestCase() { private val testScope = kosmos.testScope private val sceneInteractor by lazy { kosmos.sceneInteractor } private val authenticationInteractor by lazy { kosmos.authenticationInteractor } - private val bouncerInteractor by lazy { kosmos.bouncerInteractor } - private lateinit var underTest: PinBouncerViewModel + private val underTest = + kosmos.pinBouncerViewModelFactory.create( + isInputEnabled = MutableStateFlow(true), + onIntentionalUserInput = {}, + authenticationMethod = AuthenticationMethodModel.Pin, + ) @Before fun setUp() { - underTest = - PinBouncerViewModel( - applicationContext = context, - viewModelScope = testScope.backgroundScope, - interactor = bouncerInteractor, - isInputEnabled = MutableStateFlow(true).asStateFlow(), - simBouncerInteractor = kosmos.simBouncerInteractor, - authenticationMethod = AuthenticationMethodModel.Pin, - onIntentionalUserInput = {}, - ) - overrideResource(R.string.keyguard_enter_your_pin, ENTER_YOUR_PIN) overrideResource(R.string.kg_wrong_pin, WRONG_PIN) + underTest.activateIn(testScope) } @Test @@ -96,14 +88,10 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun simBouncerViewModel_simAreaIsVisible() = testScope.runTest { val underTest = - PinBouncerViewModel( - applicationContext = context, - viewModelScope = testScope.backgroundScope, - interactor = bouncerInteractor, - isInputEnabled = MutableStateFlow(true).asStateFlow(), - simBouncerInteractor = kosmos.simBouncerInteractor, - authenticationMethod = AuthenticationMethodModel.Sim, + kosmos.pinBouncerViewModelFactory.create( + isInputEnabled = MutableStateFlow(true), onIntentionalUserInput = {}, + authenticationMethod = AuthenticationMethodModel.Sim, ) assertThat(underTest.isSimAreaVisible).isTrue() @@ -125,14 +113,10 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun simBouncerViewModel_autoConfirmEnabled_hintedPinLengthIsNull() = testScope.runTest { val underTest = - PinBouncerViewModel( - applicationContext = context, - viewModelScope = testScope.backgroundScope, - interactor = bouncerInteractor, - isInputEnabled = MutableStateFlow(true).asStateFlow(), - simBouncerInteractor = kosmos.simBouncerInteractor, - authenticationMethod = AuthenticationMethodModel.Sim, + kosmos.pinBouncerViewModelFactory.create( + isInputEnabled = MutableStateFlow(true), onIntentionalUserInput = {}, + authenticationMethod = AuthenticationMethodModel.Pin, ) kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) val hintedPinLength by collectLastValue(underTest.hintedPinLength) @@ -355,6 +339,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { AuthenticationMethodModel.Pin ) kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) + runCurrent() underTest.onPinButtonClicked(1) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index 66e45ab8ccbe..cd84abc50802 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -36,10 +36,10 @@ import com.android.systemui.authentication.domain.interactor.authenticationInter import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor import com.android.systemui.bouncer.domain.interactor.bouncerActionButtonInteractor -import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel -import com.android.systemui.bouncer.ui.viewmodel.bouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.bouncerSceneContentViewModel import com.android.systemui.classifier.domain.interactor.falsingInteractor import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.coroutines.collectLastValue @@ -139,7 +139,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository private lateinit var bouncerActionButtonInteractor: BouncerActionButtonInteractor - private lateinit var bouncerViewModel: BouncerViewModel + private lateinit var bouncerSceneContentViewModel: BouncerSceneContentViewModel private val lockscreenSceneActionsViewModel by lazy { LockscreenSceneActionsViewModel( @@ -187,7 +187,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { } bouncerActionButtonInteractor = kosmos.bouncerActionButtonInteractor - bouncerViewModel = kosmos.bouncerViewModel + bouncerSceneContentViewModel = kosmos.bouncerSceneContentViewModel shadeSceneContentViewModel = kosmos.shadeSceneContentViewModel shadeSceneActionsViewModel = kosmos.shadeSceneActionsViewModel @@ -198,6 +198,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { lockscreenSceneActionsViewModel.activateIn(testScope) shadeSceneContentViewModel.activateIn(testScope) shadeSceneActionsViewModel.activateIn(testScope) + bouncerSceneContentViewModel.activateIn(testScope) assertWithMessage("Initial scene key mismatch!") .that(sceneContainerViewModel.currentScene.value) @@ -397,7 +398,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer) emulateUserDrivenTransition(to = upDestinationSceneKey) - val bouncerActionButton by collectLastValue(bouncerViewModel.actionButton) + val bouncerActionButton by collectLastValue(bouncerSceneContentViewModel.actionButton) assertWithMessage("Bouncer action button not visible") .that(bouncerActionButton) .isNotNull() @@ -417,7 +418,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer) emulateUserDrivenTransition(to = upDestinationSceneKey) - val bouncerActionButton by collectLastValue(bouncerViewModel.actionButton) + val bouncerActionButton by collectLastValue(bouncerSceneContentViewModel.actionButton) assertWithMessage("Bouncer action button not visible during call") .that(bouncerActionButton) .isNotNull() @@ -568,7 +569,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { bouncerSceneJob = if (to == Scenes.Bouncer) { testScope.backgroundScope.launch { - bouncerViewModel.authMethodViewModel.collect { + bouncerSceneContentViewModel.authMethodViewModel.collect { // Do nothing. Need this to turn this otherwise cold flow, hot. } } @@ -644,7 +645,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { assertWithMessage("Cannot enter PIN when not on the Bouncer scene!") .that(getCurrentSceneInUi()) .isEqualTo(Scenes.Bouncer) - val authMethodViewModel by collectLastValue(bouncerViewModel.authMethodViewModel) + val authMethodViewModel by + collectLastValue(bouncerSceneContentViewModel.authMethodViewModel) assertWithMessage("Cannot enter PIN when not using a PIN authentication method!") .that(authMethodViewModel) .isInstanceOf(PinBouncerViewModel::class.java) @@ -672,7 +674,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { assertWithMessage("Cannot enter PIN when not on the Bouncer scene!") .that(getCurrentSceneInUi()) .isEqualTo(Scenes.Bouncer) - val authMethodViewModel by collectLastValue(bouncerViewModel.authMethodViewModel) + val authMethodViewModel by + collectLastValue(bouncerSceneContentViewModel.authMethodViewModel) assertWithMessage("Cannot enter PIN when not using a PIN authentication method!") .that(authMethodViewModel) .isInstanceOf(PinBouncerViewModel::class.java) @@ -719,7 +722,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { /** Emulates the dismissal of the IME (soft keyboard). */ private fun TestScope.dismissIme() { - (bouncerViewModel.authMethodViewModel.value as? PasswordBouncerViewModel)?.let { + (bouncerSceneContentViewModel.authMethodViewModel.value as? PasswordBouncerViewModel)?.let { it.onImeDismissed() runCurrent() } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt index aebc50f92e8d..34107821341d 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt @@ -18,8 +18,6 @@ package com.android.systemui.bouncer.ui import android.app.AlertDialog import android.content.Context -import com.android.systemui.bouncer.ui.viewmodel.BouncerMessageViewModelModule -import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModelModule import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.statusbar.phone.SystemUIDialog @@ -27,13 +25,7 @@ import dagger.Binds import dagger.Module import dagger.Provides -@Module( - includes = - [ - BouncerViewModelModule::class, - BouncerMessageViewModelModule::class, - ], -) +@Module interface BouncerViewModule { /** Binds BouncerView to BouncerViewImpl and makes it injectable. */ @Binds fun bindBouncerView(bouncerViewImpl: BouncerViewImpl): BouncerView diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt index 78811a96a026..ad93a25f39a5 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt @@ -9,7 +9,7 @@ import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags import com.android.systemui.bouncer.ui.BouncerDialogFactory -import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel @@ -40,7 +40,7 @@ data class ComposeBouncerDependencies @Inject constructor( val legacyInteractor: PrimaryBouncerInteractor, - val viewModel: BouncerViewModel, + val viewModelFactory: BouncerSceneContentViewModel.Factory, val dialogFactory: BouncerDialogFactory, val authenticationInteractor: AuthenticationInteractor, val viewMediatorCallback: ViewMediatorCallback?, @@ -65,7 +65,7 @@ constructor( ComposeBouncerViewBinder.bind( view, deps.legacyInteractor, - deps.viewModel, + deps.viewModelFactory, deps.dialogFactory, deps.authenticationInteractor, deps.selectedUserInteractor, diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt index eaca2767a2e8..c1f7d590d08e 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt @@ -14,7 +14,8 @@ import com.android.systemui.authentication.domain.interactor.AuthenticationInter import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.bouncer.ui.BouncerDialogFactory import com.android.systemui.bouncer.ui.composable.BouncerContent -import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel +import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.user.domain.interactor.SelectedUserInteractor import kotlinx.coroutines.flow.collectLatest @@ -25,7 +26,7 @@ object ComposeBouncerViewBinder { fun bind( view: ViewGroup, legacyInteractor: PrimaryBouncerInteractor, - viewModel: BouncerViewModel, + viewModelFactory: BouncerSceneContentViewModel.Factory, dialogFactory: BouncerDialogFactory, authenticationInteractor: AuthenticationInteractor, selectedUserInteractor: SelectedUserInteractor, @@ -48,7 +49,14 @@ object ComposeBouncerViewBinder { this@repeatWhenAttached.lifecycle } ) - setContent { PlatformTheme { BouncerContent(viewModel, dialogFactory) } } + setContent { + PlatformTheme { + BouncerContent( + rememberViewModel { viewModelFactory.create() }, + dialogFactory, + ) + } + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt index 4fbf735a62a2..e7dd974c44e5 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt @@ -17,17 +17,18 @@ package com.android.systemui.bouncer.ui.viewmodel import android.annotation.StringRes +import com.android.app.tracing.coroutines.flow.collectLatest import com.android.systemui.authentication.domain.interactor.AuthenticationResult import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.BouncerInteractor -import kotlinx.coroutines.CoroutineScope +import com.android.systemui.lifecycle.SysUiViewModel +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.receiveAsFlow sealed class AuthMethodBouncerViewModel( - protected val viewModelScope: CoroutineScope, protected val interactor: BouncerInteractor, /** @@ -37,7 +38,7 @@ sealed class AuthMethodBouncerViewModel( * being able to attempt to unlock the device. */ val isInputEnabled: StateFlow<Boolean>, -) { +) : SysUiViewModel() { private val _animateFailure = MutableStateFlow(false) /** @@ -57,6 +58,29 @@ sealed class AuthMethodBouncerViewModel( */ @get:StringRes abstract val lockoutMessageId: Int + private val authenticationRequests = Channel<AuthenticationRequest>(Channel.BUFFERED) + + override suspend fun onActivated() { + authenticationRequests.receiveAsFlow().collectLatest { request -> + if (!isInputEnabled.value) { + return@collectLatest + } + + val authenticationResult = + interactor.authenticate( + input = request.input, + tryAutoConfirm = request.useAutoConfirm, + ) + + if (authenticationResult == AuthenticationResult.SKIPPED && request.useAutoConfirm) { + return@collectLatest + } + + _animateFailure.value = authenticationResult != AuthenticationResult.SUCCEEDED + clearInput() + } + } + /** * Notifies that the UI has been hidden from the user (after any transitions have completed). */ @@ -92,14 +116,11 @@ sealed class AuthMethodBouncerViewModel( input: List<Any> = getInput(), useAutoConfirm: Boolean = false, ) { - viewModelScope.launch { - val authenticationResult = interactor.authenticate(input, useAutoConfirm) - if (authenticationResult == AuthenticationResult.SKIPPED && useAutoConfirm) { - return@launch - } - _animateFailure.value = authenticationResult != AuthenticationResult.SUCCEEDED - - clearInput() - } + authenticationRequests.trySend(AuthenticationRequest(input, useAutoConfirm)) } + + private data class AuthenticationRequest( + val input: List<Any>, + val useAutoConfirm: Boolean, + ) } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt index 31479f131ba3..c3215b4ada9e 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt @@ -27,7 +27,6 @@ import com.android.systemui.bouncer.shared.model.BouncerMessagePair import com.android.systemui.bouncer.shared.model.BouncerMessageStrings import com.android.systemui.bouncer.shared.model.primaryMessage import com.android.systemui.bouncer.shared.model.secondaryMessage -import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.domain.interactor.BiometricMessageInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceEntryBiometricsAllowedInteractor @@ -39,19 +38,19 @@ import com.android.systemui.deviceentry.shared.model.FaceLockoutMessage import com.android.systemui.deviceentry.shared.model.FaceTimeoutMessage import com.android.systemui.deviceentry.shared.model.FingerprintFailureMessage import com.android.systemui.deviceentry.shared.model.FingerprintLockoutMessage +import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel -import com.android.systemui.user.ui.viewmodel.UserViewModel import com.android.systemui.util.kotlin.Utils.Companion.sample import com.android.systemui.util.time.SystemClock -import dagger.Module -import dagger.Provides +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlin.math.ceil import kotlin.math.max import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -65,20 +64,21 @@ import kotlinx.coroutines.launch /** Holds UI state for the 2-line status message shown on the bouncer. */ @OptIn(ExperimentalCoroutinesApi::class) -class BouncerMessageViewModel( +class BouncerMessageViewModel +@AssistedInject +constructor( @Application private val applicationContext: Context, - @Application private val applicationScope: CoroutineScope, private val bouncerInteractor: BouncerInteractor, private val simBouncerInteractor: SimBouncerInteractor, private val authenticationInteractor: AuthenticationInteractor, - selectedUser: Flow<UserViewModel>, + private val userSwitcherViewModel: UserSwitcherViewModel, private val clock: SystemClock, private val biometricMessageInteractor: BiometricMessageInteractor, private val faceAuthInteractor: DeviceEntryFaceAuthInteractor, private val deviceUnlockedInteractor: DeviceUnlockedInteractor, private val deviceEntryBiometricsAllowedInteractor: DeviceEntryBiometricsAllowedInteractor, - flags: ComposeBouncerFlags, -) { + private val flags: ComposeBouncerFlags, +) : SysUiViewModel() { /** * A message shown when the user has attempted the wrong credential too many times and now must * wait a while before attempting to authenticate again. @@ -94,6 +94,25 @@ class BouncerMessageViewModel( /** The user-facing message to show in the bouncer. */ val message: MutableStateFlow<MessageViewModel?> = MutableStateFlow(null) + override suspend fun onActivated() { + if (!flags.isComposeBouncerOrSceneContainerEnabled()) { + return + } + + coroutineScope { + launch { + // Update the lockout countdown whenever the selected user is switched. + userSwitcherViewModel.selectedUser.collect { startLockoutCountdown() } + } + + launch { defaultBouncerMessageInitializer() } + launch { listenForSimBouncerEvents() } + launch { listenForBouncerEvents() } + launch { listenForFaceMessages() } + launch { listenForFingerprintMessages() } + } + } + /** Initializes the bouncer message to default whenever it is shown. */ fun onShown() { showDefaultMessage() @@ -108,173 +127,161 @@ class BouncerMessageViewModel( private var lockoutCountdownJob: Job? = null - private fun defaultBouncerMessageInitializer() { - applicationScope.launch { - resetToDefault.emit(Unit) - authenticationInteractor.authenticationMethod - .flatMapLatest { authMethod -> - if (authMethod == AuthenticationMethodModel.Sim) { - resetToDefault.map { - MessageViewModel(simBouncerInteractor.getDefaultMessage()) - } - } else if (authMethod.isSecure) { - combine( - deviceUnlockedInteractor.deviceEntryRestrictionReason, - lockoutMessage, - deviceEntryBiometricsAllowedInteractor - .isFingerprintCurrentlyAllowedOnBouncer, - resetToDefault, - ) { deviceEntryRestrictedReason, lockoutMsg, isFpAllowedInBouncer, _ -> - lockoutMsg - ?: deviceEntryRestrictedReason.toMessage( - authMethod, - isFpAllowedInBouncer - ) - } - } else { - emptyFlow() + private suspend fun defaultBouncerMessageInitializer() { + resetToDefault.emit(Unit) + authenticationInteractor.authenticationMethod + .flatMapLatest { authMethod -> + if (authMethod == AuthenticationMethodModel.Sim) { + resetToDefault.map { + MessageViewModel(simBouncerInteractor.getDefaultMessage()) } + } else if (authMethod.isSecure) { + combine( + deviceUnlockedInteractor.deviceEntryRestrictionReason, + lockoutMessage, + deviceEntryBiometricsAllowedInteractor + .isFingerprintCurrentlyAllowedOnBouncer, + resetToDefault, + ) { deviceEntryRestrictedReason, lockoutMsg, isFpAllowedInBouncer, _ -> + lockoutMsg + ?: deviceEntryRestrictedReason.toMessage( + authMethod, + isFpAllowedInBouncer + ) + } + } else { + emptyFlow() } - .collectLatest { messageViewModel -> message.value = messageViewModel } - } + } + .collectLatest { messageViewModel -> message.value = messageViewModel } } - private fun listenForSimBouncerEvents() { + private suspend fun listenForSimBouncerEvents() { // Listen for any events from the SIM bouncer and update the message shown on the bouncer. - applicationScope.launch { - authenticationInteractor.authenticationMethod - .flatMapLatest { authMethod -> - if (authMethod == AuthenticationMethodModel.Sim) { - simBouncerInteractor.bouncerMessageChanged.map { simMsg -> - simMsg?.let { MessageViewModel(it) } - } - } else { - emptyFlow() + authenticationInteractor.authenticationMethod + .flatMapLatest { authMethod -> + if (authMethod == AuthenticationMethodModel.Sim) { + simBouncerInteractor.bouncerMessageChanged.map { simMsg -> + simMsg?.let { MessageViewModel(it) } } + } else { + emptyFlow() } - .collectLatest { - if (it != null) { - message.value = it - } else { - resetToDefault.emit(Unit) - } + } + .collectLatest { + if (it != null) { + message.value = it + } else { + resetToDefault.emit(Unit) } - } + } } - private fun listenForFaceMessages() { + private suspend fun listenForFaceMessages() { // Listen for any events from face authentication and update the message shown on the // bouncer. - applicationScope.launch { - biometricMessageInteractor.faceMessage - .sample( - authenticationInteractor.authenticationMethod, - deviceEntryBiometricsAllowedInteractor.isFingerprintCurrentlyAllowedOnBouncer, - ) - .collectLatest { (faceMessage, authMethod, fingerprintAllowedOnBouncer) -> - val isFaceAuthStrong = faceAuthInteractor.isFaceAuthStrong() - val defaultPrimaryMessage = - BouncerMessageStrings.defaultMessage( - authMethod, - fingerprintAllowedOnBouncer + biometricMessageInteractor.faceMessage + .sample( + authenticationInteractor.authenticationMethod, + deviceEntryBiometricsAllowedInteractor.isFingerprintCurrentlyAllowedOnBouncer, + ) + .collectLatest { (faceMessage, authMethod, fingerprintAllowedOnBouncer) -> + val isFaceAuthStrong = faceAuthInteractor.isFaceAuthStrong() + val defaultPrimaryMessage = + BouncerMessageStrings.defaultMessage(authMethod, fingerprintAllowedOnBouncer) + .primaryMessage + .toResString() + message.value = + when (faceMessage) { + is FaceTimeoutMessage -> + MessageViewModel( + text = defaultPrimaryMessage, + secondaryText = faceMessage.message, + isUpdateAnimated = true ) - .primaryMessage - .toResString() - message.value = - when (faceMessage) { - is FaceTimeoutMessage -> - MessageViewModel( - text = defaultPrimaryMessage, - secondaryText = faceMessage.message, - isUpdateAnimated = true - ) - is FaceLockoutMessage -> - if (isFaceAuthStrong) - BouncerMessageStrings.class3AuthLockedOut(authMethod) - .toMessage() - else - BouncerMessageStrings.faceLockedOut( - authMethod, - fingerprintAllowedOnBouncer - ) - .toMessage() - is FaceFailureMessage -> - BouncerMessageStrings.incorrectFaceInput( + is FaceLockoutMessage -> + if (isFaceAuthStrong) + BouncerMessageStrings.class3AuthLockedOut(authMethod).toMessage() + else + BouncerMessageStrings.faceLockedOut( authMethod, fingerprintAllowedOnBouncer ) .toMessage() - else -> - MessageViewModel( - text = defaultPrimaryMessage, - secondaryText = faceMessage.message, - isUpdateAnimated = false + is FaceFailureMessage -> + BouncerMessageStrings.incorrectFaceInput( + authMethod, + fingerprintAllowedOnBouncer ) - } - delay(MESSAGE_DURATION) - resetToDefault.emit(Unit) - } - } + .toMessage() + else -> + MessageViewModel( + text = defaultPrimaryMessage, + secondaryText = faceMessage.message, + isUpdateAnimated = false + ) + } + delay(MESSAGE_DURATION) + resetToDefault.emit(Unit) + } } - private fun listenForFingerprintMessages() { - applicationScope.launch { - // Listen for any events from fingerprint authentication and update the message shown - // on the bouncer. - biometricMessageInteractor.fingerprintMessage - .sample( - authenticationInteractor.authenticationMethod, - deviceEntryBiometricsAllowedInteractor.isFingerprintCurrentlyAllowedOnBouncer - ) - .collectLatest { (fingerprintMessage, authMethod, isFingerprintAllowed) -> - val defaultPrimaryMessage = - BouncerMessageStrings.defaultMessage(authMethod, isFingerprintAllowed) - .primaryMessage - .toResString() - message.value = - when (fingerprintMessage) { - is FingerprintLockoutMessage -> - BouncerMessageStrings.class3AuthLockedOut(authMethod).toMessage() - is FingerprintFailureMessage -> - BouncerMessageStrings.incorrectFingerprintInput(authMethod) - .toMessage() - else -> - MessageViewModel( - text = defaultPrimaryMessage, - secondaryText = fingerprintMessage.message, - isUpdateAnimated = false - ) - } - delay(MESSAGE_DURATION) - resetToDefault.emit(Unit) - } - } + private suspend fun listenForFingerprintMessages() { + // Listen for any events from fingerprint authentication and update the message shown + // on the bouncer. + biometricMessageInteractor.fingerprintMessage + .sample( + authenticationInteractor.authenticationMethod, + deviceEntryBiometricsAllowedInteractor.isFingerprintCurrentlyAllowedOnBouncer + ) + .collectLatest { (fingerprintMessage, authMethod, isFingerprintAllowed) -> + val defaultPrimaryMessage = + BouncerMessageStrings.defaultMessage(authMethod, isFingerprintAllowed) + .primaryMessage + .toResString() + message.value = + when (fingerprintMessage) { + is FingerprintLockoutMessage -> + BouncerMessageStrings.class3AuthLockedOut(authMethod).toMessage() + is FingerprintFailureMessage -> + BouncerMessageStrings.incorrectFingerprintInput(authMethod).toMessage() + else -> + MessageViewModel( + text = defaultPrimaryMessage, + secondaryText = fingerprintMessage.message, + isUpdateAnimated = false + ) + } + delay(MESSAGE_DURATION) + resetToDefault.emit(Unit) + } } - private fun listenForBouncerEvents() { - // Keeps the lockout message up-to-date. - applicationScope.launch { - bouncerInteractor.onLockoutStarted.collect { startLockoutCountdown() } - } + private suspend fun listenForBouncerEvents() { + coroutineScope { + // Keeps the lockout message up-to-date. + launch { bouncerInteractor.onLockoutStarted.collect { startLockoutCountdown() } } - // Listens to relevant bouncer events - applicationScope.launch { - bouncerInteractor.onIncorrectBouncerInput - .sample( - authenticationInteractor.authenticationMethod, - deviceEntryBiometricsAllowedInteractor.isFingerprintCurrentlyAllowedOnBouncer - ) - .collectLatest { (_, authMethod, isFingerprintAllowed) -> - message.emit( - BouncerMessageStrings.incorrectSecurityInput( - authMethod, - isFingerprintAllowed - ) - .toMessage() + // Listens to relevant bouncer events + launch { + bouncerInteractor.onIncorrectBouncerInput + .sample( + authenticationInteractor.authenticationMethod, + deviceEntryBiometricsAllowedInteractor + .isFingerprintCurrentlyAllowedOnBouncer ) - delay(MESSAGE_DURATION) - resetToDefault.emit(Unit) - } + .collectLatest { (_, authMethod, isFingerprintAllowed) -> + message.emit( + BouncerMessageStrings.incorrectSecurityInput( + authMethod, + isFingerprintAllowed + ) + .toMessage() + ) + delay(MESSAGE_DURATION) + resetToDefault.emit(Unit) + } + } } } @@ -323,10 +330,10 @@ class BouncerMessageViewModel( } /** Shows the countdown message and refreshes it every second. */ - private fun startLockoutCountdown() { + private suspend fun startLockoutCountdown() { lockoutCountdownJob?.cancel() - lockoutCountdownJob = - applicationScope.launch { + lockoutCountdownJob = coroutineScope { + launch { authenticationInteractor.authenticationMethod.collectLatest { authMethod -> do { val remainingSeconds = remainingLockoutSeconds() @@ -352,6 +359,7 @@ class BouncerMessageViewModel( lockoutCountdownJob = null } } + } } private fun remainingLockoutSeconds(): Int { @@ -365,20 +373,9 @@ class BouncerMessageViewModel( private fun Int.toResString(): String = applicationContext.getString(this) - init { - if (flags.isComposeBouncerOrSceneContainerEnabled()) { - applicationScope.launch { - // Update the lockout countdown whenever the selected user is switched. - selectedUser.collect { startLockoutCountdown() } - } - - defaultBouncerMessageInitializer() - - listenForSimBouncerEvents() - listenForBouncerEvents() - listenForFaceMessages() - listenForFingerprintMessages() - } + @AssistedFactory + interface Factory { + fun create(): BouncerMessageViewModel } companion object { @@ -398,40 +395,3 @@ data class MessageViewModel( */ val isUpdateAnimated: Boolean = true, ) - -@OptIn(ExperimentalCoroutinesApi::class) -@Module -object BouncerMessageViewModelModule { - - @Provides - @SysUISingleton - fun viewModel( - @Application applicationContext: Context, - @Application applicationScope: CoroutineScope, - bouncerInteractor: BouncerInteractor, - simBouncerInteractor: SimBouncerInteractor, - authenticationInteractor: AuthenticationInteractor, - clock: SystemClock, - biometricMessageInteractor: BiometricMessageInteractor, - faceAuthInteractor: DeviceEntryFaceAuthInteractor, - deviceUnlockedInteractor: DeviceUnlockedInteractor, - deviceEntryBiometricsAllowedInteractor: DeviceEntryBiometricsAllowedInteractor, - flags: ComposeBouncerFlags, - userSwitcherViewModel: UserSwitcherViewModel, - ): BouncerMessageViewModel { - return BouncerMessageViewModel( - applicationContext = applicationContext, - applicationScope = applicationScope, - bouncerInteractor = bouncerInteractor, - simBouncerInteractor = simBouncerInteractor, - authenticationInteractor = authenticationInteractor, - clock = clock, - biometricMessageInteractor = biometricMessageInteractor, - faceAuthInteractor = faceAuthInteractor, - deviceUnlockedInteractor = deviceUnlockedInteractor, - deviceEntryBiometricsAllowedInteractor = deviceEntryBiometricsAllowedInteractor, - flags = flags, - selectedUser = userSwitcherViewModel.selectedUser, - ) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModel.kt new file mode 100644 index 000000000000..2a272714db37 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModel.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 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.bouncer.ui.viewmodel + +import com.android.compose.animation.scene.Back +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.SwipeDirection +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.map + +/** + * Models UI state for user actions that can lead to navigation to other scenes when showing the + * bouncer scene. + */ +class BouncerSceneActionsViewModel +@AssistedInject +constructor( + private val bouncerInteractor: BouncerInteractor, +) : SceneActionsViewModel() { + + override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { + bouncerInteractor.dismissDestination + .map { prevScene -> + mapOf( + Back to UserActionResult(prevScene), + Swipe(SwipeDirection.Down) to UserActionResult(prevScene), + ) + } + .collectLatest { actions -> setActions(actions) } + } + + @AssistedFactory + interface Factory { + fun create(): BouncerSceneActionsViewModel + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt index e2089bbb4504..aede63b0ac23 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt @@ -23,125 +23,62 @@ import android.graphics.Bitmap import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.type import androidx.core.graphics.drawable.toBitmap -import com.android.compose.animation.scene.Back -import com.android.compose.animation.scene.Swipe -import com.android.compose.animation.scene.SwipeDirection -import com.android.compose.animation.scene.UserAction -import com.android.compose.animation.scene.UserActionResult import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.authentication.shared.model.AuthenticationWipeModel import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor import com.android.systemui.bouncer.domain.interactor.BouncerInteractor -import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text -import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.inputmethod.domain.interactor.InputMethodInteractor -import com.android.systemui.user.domain.interactor.SelectedUserInteractor -import com.android.systemui.user.ui.viewmodel.UserActionViewModel +import com.android.systemui.lifecycle.SysUiViewModel import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel -import com.android.systemui.user.ui.viewmodel.UserViewModel -import dagger.Module -import dagger.Provides -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.Flow +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.job import kotlinx.coroutines.launch -/** Holds UI state and handles user input on bouncer UIs. */ -class BouncerViewModel( +/** Models UI state for the content of the bouncer scene. */ +class BouncerSceneContentViewModel +@AssistedInject +constructor( @Application private val applicationContext: Context, - @Deprecated("TODO(b/354270224): remove this. Injecting CoroutineScope to view-models is banned") - @Application - private val applicationScope: CoroutineScope, - @Main private val mainDispatcher: CoroutineDispatcher, private val bouncerInteractor: BouncerInteractor, - private val inputMethodInteractor: InputMethodInteractor, - private val simBouncerInteractor: SimBouncerInteractor, private val authenticationInteractor: AuthenticationInteractor, - private val selectedUserInteractor: SelectedUserInteractor, private val devicePolicyManager: DevicePolicyManager, - bouncerMessageViewModel: BouncerMessageViewModel, - flags: ComposeBouncerFlags, - selectedUser: Flow<UserViewModel>, - users: Flow<List<UserViewModel>>, - userSwitcherMenu: Flow<List<UserActionViewModel>>, - actionButton: Flow<BouncerActionButtonModel?>, -) { - val selectedUserImage: StateFlow<Bitmap?> = - selectedUser - .map { it.image.toBitmap() } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = null, - ) - - val destinationScenes: Flow<Map<UserAction, UserActionResult>> = - bouncerInteractor.dismissDestination.map { prevScene -> - mapOf( - Back to UserActionResult(prevScene), - Swipe(SwipeDirection.Down) to UserActionResult(prevScene), - ) - } - - val message: BouncerMessageViewModel = bouncerMessageViewModel - + private val bouncerMessageViewModelFactory: BouncerMessageViewModel.Factory, + private val flags: ComposeBouncerFlags, + private val userSwitcher: UserSwitcherViewModel, + private val actionButtonInteractor: BouncerActionButtonInteractor, + private val pinViewModelFactory: PinBouncerViewModel.Factory, + private val patternViewModelFactory: PatternBouncerViewModel.Factory, + private val passwordViewModelFactory: PasswordBouncerViewModel.Factory, +) : SysUiViewModel() { + private val _selectedUserImage = MutableStateFlow<Bitmap?>(null) + val selectedUserImage: StateFlow<Bitmap?> = _selectedUserImage.asStateFlow() + + val message: BouncerMessageViewModel by lazy { bouncerMessageViewModelFactory.create() } + + private val _userSwitcherDropdown = + MutableStateFlow<List<UserSwitcherDropdownItemViewModel>>(emptyList()) val userSwitcherDropdown: StateFlow<List<UserSwitcherDropdownItemViewModel>> = - combine( - users, - userSwitcherMenu, - ) { users, actions -> - users.map { user -> - UserSwitcherDropdownItemViewModel( - icon = Icon.Loaded(user.image, contentDescription = null), - text = user.name, - onClick = user.onClicked ?: {}, - ) - } + - actions.map { action -> - UserSwitcherDropdownItemViewModel( - icon = Icon.Resource(action.iconResourceId, contentDescription = null), - text = Text.Resource(action.textResourceId), - onClick = action.onClicked, - ) - } - } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = emptyList(), - ) + _userSwitcherDropdown.asStateFlow() val isUserSwitcherVisible: Boolean get() = bouncerInteractor.isUserSwitcherVisible - // Handle to the scope of the child ViewModel (stored in [authMethod]). - private var childViewModelScope: CoroutineScope? = null - /** View-model for the current UI, based on the current authentication method. */ + private val _authMethodViewModel = MutableStateFlow<AuthMethodBouncerViewModel?>(null) val authMethodViewModel: StateFlow<AuthMethodBouncerViewModel?> = - authenticationInteractor.authenticationMethod - .map(::getChildViewModel) - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = null, - ) + _authMethodViewModel.asStateFlow() /** * A message for a dialog to show when the user has attempted the wrong credential too many @@ -160,31 +97,24 @@ class BouncerViewModel( */ private val wipeDialogMessage = MutableStateFlow<String?>(null) + private val _dialogViewModel = MutableStateFlow<DialogViewModel?>(createDialogViewModel()) /** * Models the dialog to be shown to the user, or `null` if no dialog should be shown. * * Once the dialog is shown, the UI should call [DialogViewModel.onDismiss] when the user * dismisses this dialog. */ - val dialogViewModel: StateFlow<DialogViewModel?> = - combine(wipeDialogMessage, lockoutDialogMessage) { _, _ -> createDialogViewModel() } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = createDialogViewModel(), - ) + val dialogViewModel: StateFlow<DialogViewModel?> = _dialogViewModel.asStateFlow() + private val _actionButton = MutableStateFlow<BouncerActionButtonModel?>(null) /** * The bouncer action button (Return to Call / Emergency Call). If `null`, the button should not * be shown. */ - val actionButton: StateFlow<BouncerActionButtonModel?> = - actionButton.stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = null - ) + val actionButton: StateFlow<BouncerActionButtonModel?> = _actionButton.asStateFlow() + private val _isSideBySideSupported = + MutableStateFlow(isSideBySideSupported(authMethodViewModel.value)) /** * Whether the "side-by-side" layout is supported. * @@ -193,45 +123,97 @@ class BouncerViewModel( * side-by-side layout; these need to be shown with the standard layout so they can take up as * much width as possible. */ - val isSideBySideSupported: StateFlow<Boolean> = - authMethodViewModel - .map { authMethod -> isSideBySideSupported(authMethod) } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = isSideBySideSupported(authMethodViewModel.value), - ) + val isSideBySideSupported: StateFlow<Boolean> = _isSideBySideSupported.asStateFlow() + private val _isFoldSplitRequired = + MutableStateFlow(isFoldSplitRequired(authMethodViewModel.value)) /** * Whether the splitting the UI around the fold seam (where the hinge is on a foldable device) * is required. */ - val isFoldSplitRequired: StateFlow<Boolean> = - authMethodViewModel - .map { authMethod -> isFoldSplitRequired(authMethod) } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = isFoldSplitRequired(authMethodViewModel.value), - ) - - private val isInputEnabled: StateFlow<Boolean> = - bouncerMessageViewModel.isLockoutMessagePresent - .map { lockoutMessagePresent -> !lockoutMessagePresent } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = authenticationInteractor.lockoutEndTimestamp == null, - ) + val isFoldSplitRequired: StateFlow<Boolean> = _isFoldSplitRequired.asStateFlow() + + private val _isInputEnabled = + MutableStateFlow(authenticationInteractor.lockoutEndTimestamp == null) + private val isInputEnabled: StateFlow<Boolean> = _isInputEnabled.asStateFlow() + + override suspend fun onActivated() { + coroutineScope { + launch { message.activate() } + launch { + authenticationInteractor.authenticationMethod + .map(::getChildViewModel) + .collectLatest { childViewModelOrNull -> + _authMethodViewModel.value = childViewModelOrNull + childViewModelOrNull?.activate() + } + } - init { - if (flags.isComposeBouncerOrSceneContainerEnabled()) { - // Keeps the upcoming wipe dialog up-to-date. - applicationScope.launch { + launch { authenticationInteractor.upcomingWipe.collect { wipeModel -> wipeDialogMessage.value = wipeModel?.message } } + + launch { + userSwitcher.selectedUser + .map { it.image.toBitmap() } + .collectLatest { _selectedUserImage.value = it } + } + + launch { + combine( + userSwitcher.users, + userSwitcher.menu, + ) { users, actions -> + users.map { user -> + UserSwitcherDropdownItemViewModel( + icon = Icon.Loaded(user.image, contentDescription = null), + text = user.name, + onClick = user.onClicked ?: {}, + ) + } + + actions.map { action -> + UserSwitcherDropdownItemViewModel( + icon = + Icon.Resource( + action.iconResourceId, + contentDescription = null + ), + text = Text.Resource(action.textResourceId), + onClick = action.onClicked, + ) + } + } + .collectLatest { _userSwitcherDropdown.value = it } + } + + launch { + combine(wipeDialogMessage, lockoutDialogMessage) { _, _ -> createDialogViewModel() } + .collectLatest { _dialogViewModel.value = it } + } + + launch { + actionButtonInteractor.actionButton.collectLatest { _actionButton.value = it } + } + + launch { + authMethodViewModel + .map { authMethod -> isSideBySideSupported(authMethod) } + .collectLatest { _isSideBySideSupported.value = it } + } + + launch { + authMethodViewModel + .map { authMethod -> isFoldSplitRequired(authMethod) } + .collectLatest { _isFoldSplitRequired.value = it } + } + + launch { + message.isLockoutMessagePresent + .map { lockoutMessagePresent -> !lockoutMessagePresent } + .collectLatest { _isInputEnabled.value = it } + } } } @@ -253,46 +235,28 @@ class BouncerViewModel( return childViewModel } - childViewModelScope?.cancel() - val newViewModelScope = createChildCoroutineScope(applicationScope) - childViewModelScope = newViewModelScope return when (authenticationMethod) { is AuthenticationMethodModel.Pin -> - PinBouncerViewModel( - applicationContext = applicationContext, - viewModelScope = newViewModelScope, - interactor = bouncerInteractor, - isInputEnabled = isInputEnabled, - simBouncerInteractor = simBouncerInteractor, + pinViewModelFactory.create( authenticationMethod = authenticationMethod, - onIntentionalUserInput = ::onIntentionalUserInput + onIntentionalUserInput = ::onIntentionalUserInput, + isInputEnabled = isInputEnabled, ) is AuthenticationMethodModel.Sim -> - PinBouncerViewModel( - applicationContext = applicationContext, - viewModelScope = newViewModelScope, - interactor = bouncerInteractor, - isInputEnabled = isInputEnabled, - simBouncerInteractor = simBouncerInteractor, + pinViewModelFactory.create( authenticationMethod = authenticationMethod, - onIntentionalUserInput = ::onIntentionalUserInput + onIntentionalUserInput = ::onIntentionalUserInput, + isInputEnabled = isInputEnabled, ) is AuthenticationMethodModel.Password -> - PasswordBouncerViewModel( - viewModelScope = newViewModelScope, + passwordViewModelFactory.create( + onIntentionalUserInput = ::onIntentionalUserInput, isInputEnabled = isInputEnabled, - interactor = bouncerInteractor, - inputMethodInteractor = inputMethodInteractor, - selectedUserInteractor = selectedUserInteractor, - onIntentionalUserInput = ::onIntentionalUserInput ) is AuthenticationMethodModel.Pattern -> - PatternBouncerViewModel( - applicationContext = applicationContext, - viewModelScope = newViewModelScope, - interactor = bouncerInteractor, + patternViewModelFactory.create( + onIntentionalUserInput = ::onIntentionalUserInput, isInputEnabled = isInputEnabled, - onIntentionalUserInput = ::onIntentionalUserInput ) else -> null } @@ -303,12 +267,6 @@ class BouncerViewModel( bouncerInteractor.onIntentionalUserInput() } - private fun createChildCoroutineScope(parentScope: CoroutineScope): CoroutineScope { - return CoroutineScope( - SupervisorJob(parent = parentScope.coroutineContext.job) + mainDispatcher - ) - } - /** * @return A message warning the user that the user/profile/device will be wiped upon a further * [AuthenticationWipeModel.remainingAttempts] unsuccessful authentication attempts. @@ -396,44 +354,9 @@ class BouncerViewModel( val text: Text, val onClick: () -> Unit, ) -} -@Module -object BouncerViewModelModule { - - @Provides - @SysUISingleton - fun viewModel( - @Application applicationContext: Context, - @Application applicationScope: CoroutineScope, - @Main mainDispatcher: CoroutineDispatcher, - bouncerInteractor: BouncerInteractor, - imeInteractor: InputMethodInteractor, - simBouncerInteractor: SimBouncerInteractor, - actionButtonInteractor: BouncerActionButtonInteractor, - authenticationInteractor: AuthenticationInteractor, - selectedUserInteractor: SelectedUserInteractor, - flags: ComposeBouncerFlags, - userSwitcherViewModel: UserSwitcherViewModel, - devicePolicyManager: DevicePolicyManager, - bouncerMessageViewModel: BouncerMessageViewModel, - ): BouncerViewModel { - return BouncerViewModel( - applicationContext = applicationContext, - applicationScope = applicationScope, - mainDispatcher = mainDispatcher, - bouncerInteractor = bouncerInteractor, - inputMethodInteractor = imeInteractor, - simBouncerInteractor = simBouncerInteractor, - authenticationInteractor = authenticationInteractor, - selectedUserInteractor = selectedUserInteractor, - devicePolicyManager = devicePolicyManager, - bouncerMessageViewModel = bouncerMessageViewModel, - flags = flags, - selectedUser = userSwitcherViewModel.selectedUser, - users = userSwitcherViewModel.users, - userSwitcherMenu = userSwitcherViewModel.menu, - actionButton = actionButtonInteractor.actionButton, - ) + @AssistedFactory + interface Factory { + fun create(): BouncerSceneContentViewModel } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt index 052fb6b3c4d7..9ead7a0dcf4d 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt @@ -23,29 +23,33 @@ import com.android.systemui.inputmethod.domain.interactor.InputMethodInteractor import com.android.systemui.res.R import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.kotlin.onSubscriberAdded +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlin.time.Duration.Companion.milliseconds -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch /** Holds UI state and handles user input for the password bouncer UI. */ -class PasswordBouncerViewModel( - viewModelScope: CoroutineScope, - isInputEnabled: StateFlow<Boolean>, +class PasswordBouncerViewModel +@AssistedInject +constructor( interactor: BouncerInteractor, - private val onIntentionalUserInput: () -> Unit, private val inputMethodInteractor: InputMethodInteractor, private val selectedUserInteractor: SelectedUserInteractor, + @Assisted isInputEnabled: StateFlow<Boolean>, + @Assisted private val onIntentionalUserInput: () -> Unit, ) : AuthMethodBouncerViewModel( - viewModelScope = viewModelScope, interactor = interactor, isInputEnabled = isInputEnabled, ) { @@ -59,28 +63,70 @@ class PasswordBouncerViewModel( override val lockoutMessageId = R.string.kg_too_many_failed_password_attempts_dialog_message + private val _isImeSwitcherButtonVisible = MutableStateFlow(false) /** Informs the UI whether the input method switcher button should be visible. */ - val isImeSwitcherButtonVisible: StateFlow<Boolean> = imeSwitcherRefreshingFlow() + val isImeSwitcherButtonVisible: StateFlow<Boolean> = _isImeSwitcherButtonVisible.asStateFlow() /** Whether the text field element currently has focus. */ private val isTextFieldFocused = MutableStateFlow(false) + private val _isTextFieldFocusRequested = + MutableStateFlow(isInputEnabled.value && !isTextFieldFocused.value) /** Whether the UI should request focus on the text field element. */ - val isTextFieldFocusRequested = - combine(isInputEnabled, isTextFieldFocused) { hasInput, hasFocus -> hasInput && !hasFocus } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(), - initialValue = isInputEnabled.value && !isTextFieldFocused.value, - ) + val isTextFieldFocusRequested = _isTextFieldFocusRequested.asStateFlow() + private val _selectedUserId = MutableStateFlow(selectedUserInteractor.getSelectedUserId()) /** The ID of the currently-selected user. */ - val selectedUserId: StateFlow<Int> = - selectedUserInteractor.selectedUser.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(), - initialValue = selectedUserInteractor.getSelectedUserId(), - ) + val selectedUserId: StateFlow<Int> = _selectedUserId.asStateFlow() + + private val requests = Channel<Request>(Channel.BUFFERED) + + override suspend fun onActivated() { + coroutineScope { + launch { super.onActivated() } + launch { + requests.receiveAsFlow().collect { request -> + when (request) { + is OnImeSwitcherButtonClicked -> { + inputMethodInteractor.showInputMethodPicker( + displayId = request.displayId, + showAuxiliarySubtypes = false, + ) + } + is OnImeDismissed -> { + interactor.onImeHiddenByUser() + } + } + } + } + launch { + combine(isInputEnabled, isTextFieldFocused) { hasInput, hasFocus -> + hasInput && !hasFocus + } + .collectLatest { _isTextFieldFocusRequested.value = it } + } + launch { + selectedUserInteractor.selectedUser.collectLatest { _selectedUserId.value = it } + } + launch { + // Re-fetch the currently-enabled IMEs whenever the selected user changes, and + // whenever + // the UI subscribes to the `isImeSwitcherButtonVisible` flow. + combine( + // InputMethodManagerService sometimes takes some time to update its + // internal + // state when the selected user changes. As a workaround, delay fetching the + // IME + // info. + selectedUserInteractor.selectedUser.onEach { delay(DELAY_TO_FETCH_IMES) }, + _isImeSwitcherButtonVisible.onSubscriberAdded() + ) { selectedUserId, _ -> + inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(selectedUserId) + } + .collectLatest { _isImeSwitcherButtonVisible.value = it } + } + } + } override fun onHidden() { super.onHidden() @@ -106,9 +152,7 @@ class PasswordBouncerViewModel( /** Notifies that the user clicked the button to change the input method. */ fun onImeSwitcherButtonClicked(displayId: Int) { - viewModelScope.launch { - inputMethodInteractor.showInputMethodPicker(displayId, showAuxiliarySubtypes = false) - } + requests.trySend(OnImeSwitcherButtonClicked(displayId)) } /** Notifies that the user has pressed the key for attempting to authenticate the password. */ @@ -120,7 +164,7 @@ class PasswordBouncerViewModel( /** Notifies that the user has dismissed the software keyboard (IME). */ fun onImeDismissed() { - viewModelScope.launch { interactor.onImeHiddenByUser() } + requests.trySend(OnImeDismissed) } /** Notifies that the password text field has gained or lost focus. */ @@ -128,34 +172,21 @@ class PasswordBouncerViewModel( isTextFieldFocused.value = isFocused } - /** - * Whether the input method switcher button should be displayed in the password bouncer UI. The - * value may be stale at the moment of subscription to this flow, but it is guaranteed to be - * shortly updated with a fresh value. - * - * Note: Each added subscription triggers an IPC call in the background, so this should only be - * subscribed to by the UI once in its lifecycle (i.e. when the bouncer is shown). - */ - private fun imeSwitcherRefreshingFlow(): StateFlow<Boolean> { - val isImeSwitcherButtonVisible = MutableStateFlow(value = false) - viewModelScope.launch { - // Re-fetch the currently-enabled IMEs whenever the selected user changes, and whenever - // the UI subscribes to the `isImeSwitcherButtonVisible` flow. - combine( - // InputMethodManagerService sometimes takes some time to update its internal - // state when the selected user changes. As a workaround, delay fetching the IME - // info. - selectedUserInteractor.selectedUser.onEach { delay(DELAY_TO_FETCH_IMES) }, - isImeSwitcherButtonVisible.onSubscriberAdded() - ) { selectedUserId, _ -> - inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(selectedUserId) - } - .collect { isImeSwitcherButtonVisible.value = it } - } - return isImeSwitcherButtonVisible.asStateFlow() + @AssistedFactory + interface Factory { + fun create( + isInputEnabled: StateFlow<Boolean>, + onIntentionalUserInput: () -> Unit, + ): PasswordBouncerViewModel } companion object { @VisibleForTesting val DELAY_TO_FETCH_IMES = 300.milliseconds } + + private sealed interface Request + + private data class OnImeSwitcherButtonClicked(val displayId: Int) : Request + + private data object OnImeDismissed : Request } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt index 8b9c0a9a0bdd..b1df04b3f76b 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt @@ -22,28 +22,32 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.res.R +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlin.math.max import kotlin.math.min import kotlin.math.pow import kotlin.math.sqrt -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch /** Holds UI state and handles user input for the pattern bouncer UI. */ -class PatternBouncerViewModel( +class PatternBouncerViewModel +@AssistedInject +constructor( private val applicationContext: Context, - viewModelScope: CoroutineScope, interactor: BouncerInteractor, - isInputEnabled: StateFlow<Boolean>, - private val onIntentionalUserInput: () -> Unit, + @Assisted isInputEnabled: StateFlow<Boolean>, + @Assisted private val onIntentionalUserInput: () -> Unit, ) : AuthMethodBouncerViewModel( - viewModelScope = viewModelScope, interactor = interactor, isInputEnabled = isInputEnabled, ) { @@ -54,17 +58,10 @@ class PatternBouncerViewModel( /** The number of rows in the dot grid. */ val rowCount = 3 - private val _selectedDots = MutableStateFlow<LinkedHashSet<PatternDotViewModel>>(linkedSetOf()) - + private val selectedDotSet = MutableStateFlow<LinkedHashSet<PatternDotViewModel>>(linkedSetOf()) + private val selectedDotList = MutableStateFlow(selectedDotSet.value.toList()) /** The dots that were selected by the user, in the order of selection. */ - val selectedDots: StateFlow<List<PatternDotViewModel>> = - _selectedDots - .map { it.toList() } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(), - initialValue = emptyList(), - ) + val selectedDots: StateFlow<List<PatternDotViewModel>> = selectedDotList.asStateFlow() private val _currentDot = MutableStateFlow<PatternDotViewModel?>(null) @@ -83,6 +80,17 @@ class PatternBouncerViewModel( override val lockoutMessageId = R.string.kg_too_many_failed_pattern_attempts_dialog_message + override suspend fun onActivated() { + coroutineScope { + launch { super.onActivated() } + launch { + selectedDotSet + .map { it.toList() } + .collectLatest { selectedDotList.value = it.toList() } + } + } + } + /** Notifies that the user has started a drag gesture across the dot grid. */ fun onDragStart() { onIntentionalUserInput() @@ -120,7 +128,7 @@ class PatternBouncerViewModel( } val hitDot = dots.value.firstOrNull { dot -> dot.x == dotColumn && dot.y == dotRow } - if (hitDot != null && !_selectedDots.value.contains(hitDot)) { + if (hitDot != null && !selectedDotSet.value.contains(hitDot)) { val skippedOverDots = currentDot.value?.let { previousDot -> buildList { @@ -147,9 +155,9 @@ class PatternBouncerViewModel( } } ?: emptyList() - _selectedDots.value = + selectedDotSet.value = linkedSetOf<PatternDotViewModel>().apply { - addAll(_selectedDots.value) + addAll(selectedDotSet.value) addAll(skippedOverDots) add(hitDot) } @@ -172,11 +180,11 @@ class PatternBouncerViewModel( override fun clearInput() { _dots.value = defaultDots() _currentDot.value = null - _selectedDots.value = linkedSetOf() + selectedDotSet.value = linkedSetOf() } override fun getInput(): List<Any> { - return _selectedDots.value.map(PatternDotViewModel::toCoordinate) + return selectedDotSet.value.map(PatternDotViewModel::toCoordinate) } private fun defaultDots(): List<PatternDotViewModel> { @@ -204,6 +212,14 @@ class PatternBouncerViewModel( max(min(outValue.float, 1f), MIN_DOT_HIT_FACTOR) } + @AssistedFactory + interface Factory { + fun create( + isInputEnabled: StateFlow<Boolean>, + onIntentionalUserInput: () -> Unit, + ): PatternBouncerViewModel + } + companion object { private const val MIN_DOT_HIT_FACTOR = 0.2f } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt index aa447ffac154..cb36560545c8 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt @@ -32,29 +32,34 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor import com.android.systemui.res.R -import kotlinx.coroutines.CoroutineScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch /** Holds UI state and handles user input for the PIN code bouncer UI. */ -class PinBouncerViewModel( +class PinBouncerViewModel +@AssistedInject +constructor( applicationContext: Context, - viewModelScope: CoroutineScope, interactor: BouncerInteractor, - isInputEnabled: StateFlow<Boolean>, - private val onIntentionalUserInput: () -> Unit, private val simBouncerInteractor: SimBouncerInteractor, - authenticationMethod: AuthenticationMethodModel, + @Assisted isInputEnabled: StateFlow<Boolean>, + @Assisted private val onIntentionalUserInput: () -> Unit, + @Assisted override val authenticationMethod: AuthenticationMethodModel, ) : AuthMethodBouncerViewModel( - viewModelScope = viewModelScope, interactor = interactor, isInputEnabled = isInputEnabled, ) { @@ -73,69 +78,89 @@ class PinBouncerViewModel( /** Currently entered pin keys. */ val pinInput: StateFlow<PinInputViewModel> = mutablePinInput + private val _hintedPinLength = MutableStateFlow<Int?>(null) /** The length of the PIN for which we should show a hint. */ - val hintedPinLength: StateFlow<Int?> = - if (isSimAreaVisible) { - flowOf(null) - } else { - interactor.hintedPinLength - } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) + val hintedPinLength: StateFlow<Int?> = _hintedPinLength.asStateFlow() + private val _backspaceButtonAppearance = MutableStateFlow(ActionButtonAppearance.Hidden) /** Appearance of the backspace button. */ val backspaceButtonAppearance: StateFlow<ActionButtonAppearance> = - combine( - mutablePinInput, - interactor.isAutoConfirmEnabled, - ) { mutablePinEntries, isAutoConfirmEnabled -> - computeBackspaceButtonAppearance( - pinInput = mutablePinEntries, - isAutoConfirmEnabled = isAutoConfirmEnabled, - ) - } - .stateIn( - scope = viewModelScope, - // Make sure this is kept as WhileSubscribed or we can run into a bug where the - // downstream continues to receive old/stale/cached values. - started = SharingStarted.WhileSubscribed(), - initialValue = ActionButtonAppearance.Hidden, - ) + _backspaceButtonAppearance.asStateFlow() + private val _confirmButtonAppearance = MutableStateFlow(ActionButtonAppearance.Hidden) /** Appearance of the confirm button. */ val confirmButtonAppearance: StateFlow<ActionButtonAppearance> = - interactor.isAutoConfirmEnabled - .map { if (it) ActionButtonAppearance.Hidden else ActionButtonAppearance.Shown } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(), - initialValue = ActionButtonAppearance.Hidden, - ) - - override val authenticationMethod: AuthenticationMethodModel = authenticationMethod + _confirmButtonAppearance.asStateFlow() override val lockoutMessageId = R.string.kg_too_many_failed_pin_attempts_dialog_message - init { - viewModelScope.launch { simBouncerInteractor.subId.collect { onResetSimFlow() } } + private val requests = Channel<Request>(Channel.BUFFERED) + + override suspend fun onActivated() { + coroutineScope { + launch { super.onActivated() } + launch { + requests.receiveAsFlow().collect { request -> + when (request) { + is OnErrorDialogDismissed -> { + simBouncerInteractor.onErrorDialogDismissed() + } + is OnAuthenticateButtonClickedForSim -> { + isSimUnlockingDialogVisible.value = true + simBouncerInteractor.verifySim(getInput()) + isSimUnlockingDialogVisible.value = false + clearInput() + } + } + } + } + launch { simBouncerInteractor.subId.collect { onResetSimFlow() } } + launch { + if (isSimAreaVisible) { + flowOf(null) + } else { + interactor.hintedPinLength + } + .collectLatest { _hintedPinLength.value = it } + } + launch { + combine( + mutablePinInput, + interactor.isAutoConfirmEnabled, + ) { mutablePinEntries, isAutoConfirmEnabled -> + computeBackspaceButtonAppearance( + pinInput = mutablePinEntries, + isAutoConfirmEnabled = isAutoConfirmEnabled, + ) + } + .collectLatest { _backspaceButtonAppearance.value = it } + } + launch { + interactor.isAutoConfirmEnabled + .map { if (it) ActionButtonAppearance.Hidden else ActionButtonAppearance.Shown } + .collectLatest { _confirmButtonAppearance.value = it } + } + launch { + interactor.isPinEnhancedPrivacyEnabled + .map { !it } + .collectLatest { _isDigitButtonAnimationEnabled.value = it } + } + } } /** Notifies that the user dismissed the sim pin error dialog. */ fun onErrorDialogDismissed() { - viewModelScope.launch { simBouncerInteractor.onErrorDialogDismissed() } + requests.trySend(OnErrorDialogDismissed) } + private val _isDigitButtonAnimationEnabled = + MutableStateFlow(!interactor.isPinEnhancedPrivacyEnabled.value) /** * Whether the digit buttons should be animated when touched. Note that this doesn't affect the * delete or enter buttons; those should always animate. */ val isDigitButtonAnimationEnabled: StateFlow<Boolean> = - interactor.isPinEnhancedPrivacyEnabled - .map { !it } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(), - initialValue = !interactor.isPinEnhancedPrivacyEnabled.value, - ) + _isDigitButtonAnimationEnabled.asStateFlow() /** Notifies that the user clicked on a PIN button with the given digit value. */ fun onPinButtonClicked(input: Int) { @@ -163,19 +188,14 @@ class PinBouncerViewModel( /** Notifies that the user clicked the "enter" button. */ fun onAuthenticateButtonClicked() { if (authenticationMethod == AuthenticationMethodModel.Sim) { - viewModelScope.launch { - isSimUnlockingDialogVisible.value = true - simBouncerInteractor.verifySim(getInput()) - isSimUnlockingDialogVisible.value = false - clearInput() - } + requests.trySend(OnAuthenticateButtonClickedForSim) } else { tryAuthenticate(useAutoConfirm = false) } } fun onDisableEsimButtonClicked() { - viewModelScope.launch { simBouncerInteractor.disableEsim() } + simBouncerInteractor.disableEsim() } /** Resets the sim screen and shows a default message. */ @@ -242,6 +262,21 @@ class PinBouncerViewModel( else -> false } } + + @AssistedFactory + interface Factory { + fun create( + isInputEnabled: StateFlow<Boolean>, + onIntentionalUserInput: () -> Unit, + authenticationMethod: AuthenticationMethodModel, + ): PinBouncerViewModel + } + + private sealed interface Request + + private data object OnErrorDialogDismissed : Request + + private data object OnAuthenticateButtonClickedForSim : Request } /** Appearance of pin-pad action buttons. */ diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt index a4936e63df8f..8e215f994e4d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt @@ -33,9 +33,10 @@ import com.android.systemui.authentication.data.repository.fakeAuthenticationRep import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.ui.BouncerDialogFactory import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout -import com.android.systemui.bouncer.ui.viewmodel.bouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.bouncerSceneContentViewModelFactory import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.motion.createSysUiComposeMotionTestRule import com.android.systemui.scene.domain.startable.sceneContainerStartable import com.android.systemui.testKosmos @@ -81,7 +82,8 @@ class BouncerContentTest : SysuiTestCase() { private fun BouncerContentUnderTest() { PlatformTheme { BouncerContent( - viewModel = kosmos.bouncerViewModel, + viewModel = + rememberViewModel { kosmos.bouncerSceneContentViewModelFactory.create() }, layout = BouncerSceneLayout.BESIDE_USER_SWITCHER, modifier = Modifier.fillMaxSize().testTag("BouncerContent"), dialogFactory = bouncerDialogFactory diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt index 2948c0274525..4b61a0d02f1e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt @@ -24,14 +24,14 @@ import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import com.android.systemui.SysuiTestCase -import com.android.systemui.bouncer.domain.interactor.bouncerInteractor -import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel -import com.android.systemui.kosmos.testScope +import com.android.systemui.bouncer.ui.viewmodel.patternBouncerViewModelFactory +import com.android.systemui.lifecycle.activateIn import com.android.systemui.motion.createSysUiComposeMotionTestRule import com.android.systemui.testKosmos import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.takeWhile +import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -51,15 +51,15 @@ class PatternBouncerTest : SysuiTestCase() { @get:Rule val motionTestRule = createSysUiComposeMotionTestRule(kosmos) - private val bouncerInteractor by lazy { kosmos.bouncerInteractor } - private val viewModel by lazy { - PatternBouncerViewModel( - applicationContext = context, - viewModelScope = kosmos.testScope.backgroundScope, - interactor = bouncerInteractor, + private val viewModel = + kosmos.patternBouncerViewModelFactory.create( isInputEnabled = MutableStateFlow(true).asStateFlow(), onIntentionalUserInput = {}, ) + + @Before + fun setUp() { + viewModel.activateIn(motionTestRule.toolkit.testScope) } @Composable diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt index e70631e89939..e8612d084b14 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.bouncer.ui.viewmodel import android.content.applicationContext @@ -26,26 +28,31 @@ import com.android.systemui.deviceentry.domain.interactor.deviceEntryBiometricsA import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel import com.android.systemui.util.time.systemClock import kotlinx.coroutines.ExperimentalCoroutinesApi -@ExperimentalCoroutinesApi -val Kosmos.bouncerMessageViewModel by - Kosmos.Fixture { - BouncerMessageViewModel( - applicationContext = applicationContext, - applicationScope = testScope.backgroundScope, - bouncerInteractor = bouncerInteractor, - simBouncerInteractor = simBouncerInteractor, - authenticationInteractor = authenticationInteractor, - selectedUser = userSwitcherViewModel.selectedUser, - clock = systemClock, - biometricMessageInteractor = biometricMessageInteractor, - faceAuthInteractor = deviceEntryFaceAuthInteractor, - deviceUnlockedInteractor = deviceUnlockedInteractor, - deviceEntryBiometricsAllowedInteractor = deviceEntryBiometricsAllowedInteractor, - flags = composeBouncerFlags, - ) +val Kosmos.bouncerMessageViewModel by Fixture { + BouncerMessageViewModel( + applicationContext = applicationContext, + bouncerInteractor = bouncerInteractor, + simBouncerInteractor = simBouncerInteractor, + authenticationInteractor = authenticationInteractor, + userSwitcherViewModel = userSwitcherViewModel, + clock = systemClock, + biometricMessageInteractor = biometricMessageInteractor, + faceAuthInteractor = deviceEntryFaceAuthInteractor, + deviceUnlockedInteractor = deviceUnlockedInteractor, + deviceEntryBiometricsAllowedInteractor = deviceEntryBiometricsAllowedInteractor, + flags = composeBouncerFlags, + ) +} + +val Kosmos.bouncerMessageViewModelFactory by Fixture { + object : BouncerMessageViewModel.Factory { + override fun create(): BouncerMessageViewModel { + return bouncerMessageViewModel + } } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt index c3dad748064d..e405d17166b9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt @@ -21,6 +21,7 @@ package com.android.systemui.bouncer.ui.viewmodel import android.app.admin.devicePolicyManager import android.content.applicationContext import com.android.systemui.authentication.domain.interactor.authenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.bouncerActionButtonInteractor import com.android.systemui.bouncer.domain.interactor.bouncerInteractor import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor @@ -28,28 +29,97 @@ import com.android.systemui.bouncer.shared.flag.composeBouncerFlags import com.android.systemui.inputmethod.domain.interactor.inputMethodInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.kosmos.testDispatcher -import com.android.systemui.kosmos.testScope import com.android.systemui.user.domain.interactor.selectedUserInteractor import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.StateFlow -val Kosmos.bouncerViewModel by Fixture { - BouncerViewModel( +val Kosmos.bouncerSceneActionsViewModel by Fixture { + BouncerSceneActionsViewModel( + bouncerInteractor = bouncerInteractor, + ) +} + +val Kosmos.bouncerSceneActionsViewModelFactory by Fixture { + object : BouncerSceneActionsViewModel.Factory { + override fun create(): BouncerSceneActionsViewModel { + return bouncerSceneActionsViewModel + } + } +} + +val Kosmos.bouncerSceneContentViewModel by Fixture { + BouncerSceneContentViewModel( applicationContext = applicationContext, - applicationScope = testScope.backgroundScope, - mainDispatcher = testDispatcher, bouncerInteractor = bouncerInteractor, - inputMethodInteractor = inputMethodInteractor, - simBouncerInteractor = simBouncerInteractor, authenticationInteractor = authenticationInteractor, - selectedUserInteractor = selectedUserInteractor, devicePolicyManager = devicePolicyManager, - bouncerMessageViewModel = bouncerMessageViewModel, + bouncerMessageViewModelFactory = bouncerMessageViewModelFactory, flags = composeBouncerFlags, - selectedUser = userSwitcherViewModel.selectedUser, - users = userSwitcherViewModel.users, - userSwitcherMenu = userSwitcherViewModel.menu, - actionButton = bouncerActionButtonInteractor.actionButton, + userSwitcher = userSwitcherViewModel, + actionButtonInteractor = bouncerActionButtonInteractor, + pinViewModelFactory = pinBouncerViewModelFactory, + patternViewModelFactory = patternBouncerViewModelFactory, + passwordViewModelFactory = passwordBouncerViewModelFactory, ) } + +val Kosmos.bouncerSceneContentViewModelFactory by Fixture { + object : BouncerSceneContentViewModel.Factory { + override fun create(): BouncerSceneContentViewModel { + return bouncerSceneContentViewModel + } + } +} + +val Kosmos.pinBouncerViewModelFactory by Fixture { + object : PinBouncerViewModel.Factory { + override fun create( + isInputEnabled: StateFlow<Boolean>, + onIntentionalUserInput: () -> Unit, + authenticationMethod: AuthenticationMethodModel, + ): PinBouncerViewModel { + return PinBouncerViewModel( + applicationContext = applicationContext, + interactor = bouncerInteractor, + simBouncerInteractor = simBouncerInteractor, + isInputEnabled = isInputEnabled, + onIntentionalUserInput = onIntentionalUserInput, + authenticationMethod = authenticationMethod, + ) + } + } +} + +val Kosmos.patternBouncerViewModelFactory by Fixture { + object : PatternBouncerViewModel.Factory { + override fun create( + isInputEnabled: StateFlow<Boolean>, + onIntentionalUserInput: () -> Unit, + ): PatternBouncerViewModel { + return PatternBouncerViewModel( + applicationContext = applicationContext, + interactor = bouncerInteractor, + isInputEnabled = isInputEnabled, + onIntentionalUserInput = onIntentionalUserInput, + ) + } + } +} + +val Kosmos.passwordBouncerViewModelFactory by Fixture { + object : PasswordBouncerViewModel.Factory { + override fun create( + isInputEnabled: StateFlow<Boolean>, + onIntentionalUserInput: () -> Unit, + ): PasswordBouncerViewModel { + return PasswordBouncerViewModel( + interactor = bouncerInteractor, + inputMethodInteractor = inputMethodInteractor, + selectedUserInteractor = selectedUserInteractor, + isInputEnabled = isInputEnabled, + onIntentionalUserInput = onIntentionalUserInput, + ) + } + } +} |