summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt28
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt26
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt19
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModelTest.kt77
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModelTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt)32
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt11
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt14
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt45
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt47
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt374
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModel.kt56
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt (renamed from packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt)333
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt133
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt62
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt151
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt18
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt43
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt98
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,
+ )
+ }
+ }
+}