Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 21fe0f36 authored by Alexander Hendrich's avatar Alexander Hendrich
Browse files

[flexiglass] Use SecureTextField for PasswordBouncer

This CL updates PasswordBouncer to migrate from TextField to a SecureTextField so that the password's last character is visible while typing (see TextObfuscationode.RevealLastTyped). With b/416411014 this will also respect the System.TEXT_SHOW_PASSWORD setting then.

SecureTextField uses TextFieldState instead of a value and onValueChange, so I had to make some more changes than initially planned.

I've also updated the tests for other bouncer related views to validate that the onIntentionalUserInput lambda is being called correctly.

Fixes: 418163964
Test: atest PasswordBouncerViewModelTest
Test: atest PinBouncerViewModelTest
Test: atest PatternBouncerViewModelTest
Flag: com.android.systemui.compose_bouncer
Change-Id: Ic8049575bc773f62469b14b94de4841da699b497
parent 824b69fc
Loading
Loading
Loading
Loading
+9 −23
Original line number Original line Diff line number Diff line
@@ -18,12 +18,11 @@


package com.android.systemui.bouncer.ui.composable
package com.android.systemui.bouncer.ui.composable


import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TextField
import androidx.compose.material3.SecureTextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.LaunchedEffect
@@ -45,7 +44,6 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -57,10 +55,7 @@ import com.android.systemui.res.R


/** UI for the input part of a password-requiring version of the bouncer. */
/** UI for the input part of a password-requiring version of the bouncer. */
@Composable
@Composable
internal fun PasswordBouncer(
internal fun PasswordBouncer(viewModel: PasswordBouncerViewModel, modifier: Modifier = Modifier) {
    viewModel: PasswordBouncerViewModel,
    modifier: Modifier = Modifier,
) {
    val focusRequester = remember { FocusRequester() }
    val focusRequester = remember { FocusRequester() }
    val isTextFieldFocusRequested by
    val isTextFieldFocusRequested by
        viewModel.isTextFieldFocusRequested.collectAsStateWithLifecycle()
        viewModel.isTextFieldFocusRequested.collectAsStateWithLifecycle()
@@ -70,7 +65,6 @@ internal fun PasswordBouncer(
        }
        }
    }
    }


    val password: String by viewModel.password.collectAsStateWithLifecycle()
    val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsStateWithLifecycle()
    val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsStateWithLifecycle()
    val animateFailure: Boolean by viewModel.animateFailure.collectAsStateWithLifecycle()
    val animateFailure: Boolean by viewModel.animateFailure.collectAsStateWithLifecycle()
    val isImeSwitcherButtonVisible by
    val isImeSwitcherButtonVisible by
@@ -90,22 +84,17 @@ internal fun PasswordBouncer(
    val lineWidthPx = with(LocalDensity.current) { 2.dp.toPx() }
    val lineWidthPx = with(LocalDensity.current) { 2.dp.toPx() }


    SelectedUserAwareInputConnection(selectedUserId) {
    SelectedUserAwareInputConnection(selectedUserId) {
        TextField(
        SecureTextField(
            value = password,
            state = viewModel.textFieldState,
            onValueChange = viewModel::onPasswordInputChanged,
            enabled = isInputEnabled,
            enabled = isInputEnabled,
            visualTransformation = PasswordVisualTransformation(),
            singleLine = true,
            textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
            textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
            keyboardOptions =
            keyboardOptions =
                KeyboardOptions(
                KeyboardOptions(
                    autoCorrectEnabled = false,
                    keyboardType = KeyboardType.Password,
                    keyboardType = KeyboardType.Password,
                    imeAction = ImeAction.Done,
                    imeAction = ImeAction.Done,
                ),
                ),
            keyboardActions =
            onKeyboardAction = { viewModel.onAuthenticateKeyPressed() },
                KeyboardActions(
                    onDone = { viewModel.onAuthenticateKeyPressed() },
                ),
            modifier =
            modifier =
                modifier
                modifier
                    .sysuiResTag("bouncer_text_entry")
                    .sysuiResTag("bouncer_text_entry")
@@ -132,17 +121,14 @@ internal fun PasswordBouncer(
                    { ImeSwitcherButton(viewModel, color) }
                    { ImeSwitcherButton(viewModel, color) }
                } else {
                } else {
                    null
                    null
                }
                },
        )
        )
    }
    }
}
}


/** Button for changing the password input method (IME). */
/** Button for changing the password input method (IME). */
@Composable
@Composable
private fun ImeSwitcherButton(
private fun ImeSwitcherButton(viewModel: PasswordBouncerViewModel, color: Color) {
    viewModel: PasswordBouncerViewModel,
    color: Color,
) {
    val context = LocalContext.current
    val context = LocalContext.current
    PlatformIconButton(
    PlatformIconButton(
        onClick = { viewModel.onImeSwitcherButtonClicked(context.displayId) },
        onClick = { viewModel.onImeSwitcherButtonClicked(context.displayId) },
@@ -152,6 +138,6 @@ private fun ImeSwitcherButton(
            IconButtonDefaults.filledIconButtonColors(
            IconButtonDefaults.filledIconButtonColors(
                contentColor = color,
                contentColor = color,
                containerColor = Color.Transparent,
                containerColor = Color.Transparent,
            )
            ),
    )
    )
}
}
+77 −74
Original line number Original line Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.systemui.bouncer.ui.viewmodel
package com.android.systemui.bouncer.ui.viewmodel


import android.content.pm.UserInfo
import android.content.pm.UserInfo
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.SysuiTestCase
@@ -24,11 +25,15 @@ import com.android.systemui.authentication.data.repository.fakeAuthenticationRep
import com.android.systemui.authentication.domain.interactor.authenticationInteractor
import com.android.systemui.authentication.domain.interactor.authenticationInteractor
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.inputmethod.data.model.InputMethodModel
import com.android.systemui.inputmethod.data.model.InputMethodModel
import com.android.systemui.inputmethod.data.repository.fakeInputMethodRepository
import com.android.systemui.inputmethod.data.repository.fakeInputMethodRepository
import com.android.systemui.inputmethod.domain.interactor.inputMethodInteractor
import com.android.systemui.inputmethod.domain.interactor.inputMethodInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.advanceTimeBy
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.collectValues
import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.res.R
import com.android.systemui.res.R
@@ -43,31 +48,26 @@ import com.google.common.truth.Truth.assertThat
import java.util.UUID
import java.util.UUID
import kotlin.time.Duration.Companion.seconds
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Before
import org.junit.Test
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.times
import org.mockito.kotlin.verify


@SmallTest
@SmallTest
@RunWith(AndroidJUnit4::class)
@RunWith(AndroidJUnit4::class)
class PasswordBouncerViewModelTest : SysuiTestCase() {
class PasswordBouncerViewModelTest : SysuiTestCase() {


    private val kosmos = testKosmos()
    private val kosmos = testKosmos()
    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 selectedUserInteractor by lazy { kosmos.selectedUserInteractor }
    private val inputMethodInteractor by lazy { kosmos.inputMethodInteractor }
    private val isInputEnabled = MutableStateFlow(true)
    private val isInputEnabled = MutableStateFlow(true)
    private val onIntentionalUserInputMock: () -> Unit = mock()


    private val underTest by lazy {
    private val underTest by lazy {
        kosmos.passwordBouncerViewModelFactory.create(
        kosmos.passwordBouncerViewModelFactory.create(
            isInputEnabled = isInputEnabled,
            isInputEnabled = isInputEnabled,
            onIntentionalUserInput = {},
            onIntentionalUserInput = onIntentionalUserInputMock,
        )
        )
    }
    }


@@ -75,54 +75,56 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
    fun setUp() {
    fun setUp() {
        overrideResource(R.string.keyguard_enter_your_password, ENTER_YOUR_PASSWORD)
        overrideResource(R.string.keyguard_enter_your_password, ENTER_YOUR_PASSWORD)
        overrideResource(R.string.kg_wrong_password, WRONG_PASSWORD)
        overrideResource(R.string.kg_wrong_password, WRONG_PASSWORD)
        underTest.activateIn(testScope)
        underTest.activateIn(kosmos.testScope)
    }
    }


    @Test
    @Test
    fun onShown() =
    fun onShown() =
        testScope.runTest {
        kosmos.runTest {
            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
            val currentOverlays by collectLastValue(kosmos.sceneInteractor.currentOverlays)
            val password by collectLastValue(underTest.password)
            lockDeviceAndOpenPasswordBouncer()
            lockDeviceAndOpenPasswordBouncer()


            assertThat(password).isEmpty()
            assertThat(underTest.textFieldState.text.toString()).isEmpty()
            assertThat(currentOverlays).contains(Overlays.Bouncer)
            assertThat(currentOverlays).contains(Overlays.Bouncer)
            assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Password)
            assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Password)
        }
        }


    @Test
    @Test
    fun onHidden_resetsPasswordInputAndMessage() =
    fun onHidden_resetsPasswordInputAndMessage() =
        testScope.runTest {
        kosmos.runTest {
            val password by collectLastValue(underTest.password)
            lockDeviceAndOpenPasswordBouncer()
            lockDeviceAndOpenPasswordBouncer()


            underTest.onPasswordInputChanged("password")
            underTest.textFieldState.setTextAndPlaceCursorAtEnd("password")
            assertThat(password).isNotEmpty()
            assertThat(underTest.textFieldState.text.toString()).isNotEmpty()


            underTest.onHidden()
            underTest.onHidden()
            assertThat(password).isEmpty()
            assertThat(underTest.textFieldState.text.toString()).isEmpty()
        }
        }


    @Test
    @Test
    fun onPasswordInputChanged() =
    fun onPasswordInputChanged() =
        testScope.runTest {
        kosmos.runTest {
            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
            val currentOverlays by collectLastValue(kosmos.sceneInteractor.currentOverlays)
            val password by collectLastValue(underTest.password)
            lockDeviceAndOpenPasswordBouncer()
            lockDeviceAndOpenPasswordBouncer()


            underTest.onPasswordInputChanged("password")
            verify(onIntentionalUserInputMock, never()).invoke()
            underTest.textFieldState.setTextAndPlaceCursorAtEnd("password")


            assertThat(password).isEqualTo("password")
            runCurrent()

            assertThat(underTest.textFieldState.text.toString()).isEqualTo("password")
            verify(onIntentionalUserInputMock, times(1)).invoke()
            assertThat(currentOverlays).contains(Overlays.Bouncer)
            assertThat(currentOverlays).contains(Overlays.Bouncer)
        }
        }


    @Test
    @Test
    fun onAuthenticateKeyPressed_whenCorrect() =
    fun onAuthenticateKeyPressed_whenCorrect() =
        testScope.runTest {
        kosmos.runTest {
            val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
            val authResult by
                collectLastValue(kosmos.authenticationInteractor.onAuthenticationResult)
            lockDeviceAndOpenPasswordBouncer()
            lockDeviceAndOpenPasswordBouncer()


            underTest.onPasswordInputChanged("password")
            underTest.textFieldState.setTextAndPlaceCursorAtEnd("password")
            underTest.onAuthenticateKeyPressed()
            underTest.onAuthenticateKeyPressed()


            assertThat(authResult).isTrue()
            assertThat(authResult).isTrue()
@@ -130,20 +132,21 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {


    @Test
    @Test
    fun onAuthenticateKeyPressed_whenWrong() =
    fun onAuthenticateKeyPressed_whenWrong() =
        testScope.runTest {
        kosmos.runTest {
            val password by collectLastValue(underTest.password)
            val authResult by
                collectLastValue(kosmos.authenticationInteractor.onAuthenticationResult)
            lockDeviceAndOpenPasswordBouncer()
            lockDeviceAndOpenPasswordBouncer()


            underTest.onPasswordInputChanged("wrong")
            underTest.textFieldState.setTextAndPlaceCursorAtEnd("wrong")
            underTest.onAuthenticateKeyPressed()
            underTest.onAuthenticateKeyPressed()


            assertThat(password).isEmpty()
            assertThat(authResult).isFalse()
            assertThat(underTest.textFieldState.text.toString()).isEmpty()
        }
        }


    @Test
    @Test
    fun onAuthenticateKeyPressed_whenEmpty() =
    fun onAuthenticateKeyPressed_whenEmpty() =
        testScope.runTest {
        kosmos.runTest {
            val password by collectLastValue(underTest.password)
            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
            kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
                AuthenticationMethodModel.Password
                AuthenticationMethodModel.Password
            )
            )
@@ -153,24 +156,24 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {


            underTest.onAuthenticateKeyPressed()
            underTest.onAuthenticateKeyPressed()


            assertThat(password).isEmpty()
            assertThat(underTest.textFieldState.text.toString()).isEmpty()
        }
        }


    @Test
    @Test
    fun onAuthenticateKeyPressed_correctAfterWrong() =
    fun onAuthenticateKeyPressed_correctAfterWrong() =
        testScope.runTest {
        kosmos.runTest {
            val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
            val authResult by
            val password by collectLastValue(underTest.password)
                collectLastValue(kosmos.authenticationInteractor.onAuthenticationResult)
            lockDeviceAndOpenPasswordBouncer()
            lockDeviceAndOpenPasswordBouncer()


            // Enter the wrong password:
            // Enter the wrong password:
            underTest.onPasswordInputChanged("wrong")
            underTest.textFieldState.setTextAndPlaceCursorAtEnd("wrong")
            underTest.onAuthenticateKeyPressed()
            underTest.onAuthenticateKeyPressed()
            assertThat(password).isEqualTo("")
            assertThat(authResult).isFalse()
            assertThat(authResult).isFalse()
            assertThat(underTest.textFieldState.text.toString()).isEmpty()


            // Enter the correct password:
            // Enter the correct password:
            underTest.onPasswordInputChanged("password")
            underTest.textFieldState.setTextAndPlaceCursorAtEnd("password")


            underTest.onAuthenticateKeyPressed()
            underTest.onAuthenticateKeyPressed()


@@ -179,14 +182,13 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {


    @Test
    @Test
    fun onShown_againAfterSceneChange_resetsPassword() =
    fun onShown_againAfterSceneChange_resetsPassword() =
        testScope.runTest {
        kosmos.runTest {
            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
            val currentOverlays by collectLastValue(kosmos.sceneInteractor.currentOverlays)
            val password by collectLastValue(underTest.password)
            lockDeviceAndOpenPasswordBouncer()
            lockDeviceAndOpenPasswordBouncer()


            // The user types a password.
            // The user types a password.
            underTest.onPasswordInputChanged("password")
            underTest.textFieldState.setTextAndPlaceCursorAtEnd("password")
            assertThat(password).isEqualTo("password")
            assertThat(underTest.textFieldState.text.toString()).isEqualTo("password")


            // The user doesn't confirm the password, but navigates back to the lockscreen instead.
            // The user doesn't confirm the password, but navigates back to the lockscreen instead.
            hideBouncer()
            hideBouncer()
@@ -195,14 +197,14 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
            showBouncer()
            showBouncer()


            // Ensure the previously-entered password is not shown.
            // Ensure the previously-entered password is not shown.
            assertThat(password).isEmpty()
            assertThat(underTest.textFieldState.text.toString()).isEmpty()
            assertThat(currentOverlays).contains(Overlays.Bouncer)
            assertThat(currentOverlays).contains(Overlays.Bouncer)
        }
        }


    @Test
    @Test
    fun onImeDismissed() =
    fun onImeDismissed() =
        testScope.runTest {
        kosmos.runTest {
            val events by collectValues(bouncerInteractor.onImeHiddenByUser)
            val events by collectValues(kosmos.bouncerInteractor.onImeHiddenByUser)
            assertThat(events).isEmpty()
            assertThat(events).isEmpty()


            underTest.onImeDismissed()
            underTest.onImeDismissed()
@@ -211,14 +213,14 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {


    @Test
    @Test
    fun isTextFieldFocusRequested_initiallyTrue() =
    fun isTextFieldFocusRequested_initiallyTrue() =
        testScope.runTest {
        kosmos.runTest {
            val isTextFieldFocusRequested by collectLastValue(underTest.isTextFieldFocusRequested)
            val isTextFieldFocusRequested by collectLastValue(underTest.isTextFieldFocusRequested)
            assertThat(isTextFieldFocusRequested).isTrue()
            assertThat(isTextFieldFocusRequested).isTrue()
        }
        }


    @Test
    @Test
    fun isTextFieldFocusRequested_focusGained_becomesFalse() =
    fun isTextFieldFocusRequested_focusGained_becomesFalse() =
        testScope.runTest {
        kosmos.runTest {
            val isTextFieldFocusRequested by collectLastValue(underTest.isTextFieldFocusRequested)
            val isTextFieldFocusRequested by collectLastValue(underTest.isTextFieldFocusRequested)


            underTest.onTextFieldFocusChanged(isFocused = true)
            underTest.onTextFieldFocusChanged(isFocused = true)
@@ -228,7 +230,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {


    @Test
    @Test
    fun isTextFieldFocusRequested_focusLost_becomesTrue() =
    fun isTextFieldFocusRequested_focusLost_becomesTrue() =
        testScope.runTest {
        kosmos.runTest {
            val isTextFieldFocusRequested by collectLastValue(underTest.isTextFieldFocusRequested)
            val isTextFieldFocusRequested by collectLastValue(underTest.isTextFieldFocusRequested)
            underTest.onTextFieldFocusChanged(isFocused = true)
            underTest.onTextFieldFocusChanged(isFocused = true)


@@ -239,7 +241,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {


    @Test
    @Test
    fun isTextFieldFocusRequested_focusLostWhileLockedOut_staysFalse() =
    fun isTextFieldFocusRequested_focusLostWhileLockedOut_staysFalse() =
        testScope.runTest {
        kosmos.runTest {
            val isTextFieldFocusRequested by collectLastValue(underTest.isTextFieldFocusRequested)
            val isTextFieldFocusRequested by collectLastValue(underTest.isTextFieldFocusRequested)
            underTest.onTextFieldFocusChanged(isFocused = true)
            underTest.onTextFieldFocusChanged(isFocused = true)
            setLockout(true)
            setLockout(true)
@@ -251,7 +253,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {


    @Test
    @Test
    fun isTextFieldFocusRequested_lockoutCountdownEnds_becomesTrue() =
    fun isTextFieldFocusRequested_lockoutCountdownEnds_becomesTrue() =
        testScope.runTest {
        kosmos.runTest {
            val isTextFieldFocusRequested by collectLastValue(underTest.isTextFieldFocusRequested)
            val isTextFieldFocusRequested by collectLastValue(underTest.isTextFieldFocusRequested)
            underTest.onTextFieldFocusChanged(isFocused = true)
            underTest.onTextFieldFocusChanged(isFocused = true)
            setLockout(true)
            setLockout(true)
@@ -264,8 +266,8 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {


    @Test
    @Test
    fun isImeSwitcherButtonVisible() =
    fun isImeSwitcherButtonVisible() =
        testScope.runTest {
        kosmos.runTest {
            val selectedUserId by collectLastValue(selectedUserInteractor.selectedUser)
            val selectedUserId by collectLastValue(kosmos.selectedUserInteractor.selectedUser)
            selectUser(USER_INFOS.first())
            selectUser(USER_INFOS.first())


            enableInputMethodsForUser(checkNotNull(selectedUserId))
            enableInputMethodsForUser(checkNotNull(selectedUserId))
@@ -281,7 +283,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
            selectUser(USER_INFOS.last())
            selectUser(USER_INFOS.last())


            assertThat(
            assertThat(
                    inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(
                    kosmos.inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(
                        checkNotNull(selectedUserId)
                        checkNotNull(selectedUserId)
                    )
                    )
                )
                )
@@ -296,7 +298,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {


    @Test
    @Test
    fun onImeSwitcherButtonClicked() =
    fun onImeSwitcherButtonClicked() =
        testScope.runTest {
        kosmos.runTest {
            val displayId = 7
            val displayId = 7
            assertThat(kosmos.fakeInputMethodRepository.inputMethodPickerShownDisplayId)
            assertThat(kosmos.fakeInputMethodRepository.inputMethodPickerShownDisplayId)
                .isNotEqualTo(displayId)
                .isNotEqualTo(displayId)
@@ -310,8 +312,9 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {


    @Test
    @Test
    fun afterSuccessfulAuthentication_focusIsNotRequested() =
    fun afterSuccessfulAuthentication_focusIsNotRequested() =
        testScope.runTest {
        kosmos.runTest {
            val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
            val authResult by
                collectLastValue(kosmos.authenticationInteractor.onAuthenticationResult)
            val textInputFocusRequested by collectLastValue(underTest.isTextFieldFocusRequested)
            val textInputFocusRequested by collectLastValue(underTest.isTextFieldFocusRequested)
            lockDeviceAndOpenPasswordBouncer()
            lockDeviceAndOpenPasswordBouncer()


@@ -330,7 +333,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
            assertThat(textInputFocusRequested).isFalse()
            assertThat(textInputFocusRequested).isFalse()


            // authenticate successfully.
            // authenticate successfully.
            underTest.onPasswordInputChanged("password")
            underTest.textFieldState.setTextAndPlaceCursorAtEnd("password")
            underTest.onAuthenticateKeyPressed()
            underTest.onAuthenticateKeyPressed()
            runCurrent()
            runCurrent()


@@ -343,31 +346,31 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
            assertThat(textInputFocusRequested).isFalse()
            assertThat(textInputFocusRequested).isFalse()
        }
        }


    private fun TestScope.showBouncer() {
    private fun Kosmos.showBouncer() {
        val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
        val currentOverlays by collectLastValue(kosmos.sceneInteractor.currentOverlays)
        sceneInteractor.showOverlay(Overlays.Bouncer, "reason")
        kosmos.sceneInteractor.showOverlay(Overlays.Bouncer, "reason")
        runCurrent()
        runCurrent()


        assertThat(currentOverlays).contains(Overlays.Bouncer)
        assertThat(currentOverlays).contains(Overlays.Bouncer)
    }
    }


    private fun TestScope.hideBouncer() {
    private fun Kosmos.hideBouncer() {
        val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
        val currentOverlays by collectLastValue(kosmos.sceneInteractor.currentOverlays)
        sceneInteractor.hideOverlay(Overlays.Bouncer, "reason")
        kosmos.sceneInteractor.hideOverlay(Overlays.Bouncer, "reason")
        underTest.onHidden()
        underTest.onHidden()
        runCurrent()
        runCurrent()


        assertThat(currentOverlays).doesNotContain(Overlays.Bouncer)
        assertThat(currentOverlays).doesNotContain(Overlays.Bouncer)
    }
    }


    private fun TestScope.lockDeviceAndOpenPasswordBouncer() {
    private fun Kosmos.lockDeviceAndOpenPasswordBouncer() {
        kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
        kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
            AuthenticationMethodModel.Password
            AuthenticationMethodModel.Password
        )
        )
        showBouncer()
        showBouncer()
    }
    }


    private suspend fun TestScope.setLockout(isLockedOut: Boolean, failedAttemptCount: Int = 5) {
    private suspend fun Kosmos.setLockout(isLockedOut: Boolean, failedAttemptCount: Int = 5) {
        if (isLockedOut) {
        if (isLockedOut) {
            repeat(failedAttemptCount) {
            repeat(failedAttemptCount) {
                kosmos.fakeAuthenticationRepository.reportAuthenticationAttempt(false)
                kosmos.fakeAuthenticationRepository.reportAuthenticationAttempt(false)
@@ -383,7 +386,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
        runCurrent()
        runCurrent()
    }
    }


    private fun TestScope.selectUser(userInfo: UserInfo) {
    private fun Kosmos.selectUser(userInfo: UserInfo) {
        kosmos.fakeUserRepository.selectedUser.value =
        kosmos.fakeUserRepository.selectedUser.value =
            SelectedUserModel(
            SelectedUserModel(
                userInfo = userInfo,
                userInfo = userInfo,
@@ -398,7 +401,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
            createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 0),
            createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 0),
            createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 1),
            createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 1),
        )
        )
        assertThat(inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(userId)).isTrue()
        assertThat(kosmos.inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(userId)).isTrue()
    }
    }


    private fun createInputMethodWithSubtypes(
    private fun createInputMethodWithSubtypes(
+43 −37

File changed.

Preview size limit exceeded, changes collapsed.

+67 −59

File changed.

Preview size limit exceeded, changes collapsed.

+15 −16
Original line number Original line Diff line number Diff line
@@ -17,6 +17,9 @@
package com.android.systemui.bouncer.ui.viewmodel
package com.android.systemui.bouncer.ui.viewmodel


import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.clearText
import androidx.compose.runtime.snapshotFlow
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
@@ -55,10 +58,7 @@ constructor(
        traceName = "PasswordBouncerViewModel",
        traceName = "PasswordBouncerViewModel",
    ) {
    ) {


    private val _password = MutableStateFlow("")
    val textFieldState = TextFieldState()

    /** The password entered so far. */
    val password: StateFlow<String> = _password.asStateFlow()


    override val authenticationMethod = AuthenticationMethodModel.Password
    override val authenticationMethod = AuthenticationMethodModel.Password


@@ -129,6 +129,14 @@ constructor(
                        }
                        }
                        .collect { _isImeSwitcherButtonVisible.value = it }
                        .collect { _isImeSwitcherButtonVisible.value = it }
                }
                }
                launch {
                    snapshotFlow { textFieldState.text.toString() }
                        .collect {
                            if (it.isNotEmpty()) {
                                onIntentionalUserInput()
                            }
                        }
                }
                awaitCancellation()
                awaitCancellation()
            }
            }
        } finally {
        } finally {
@@ -143,26 +151,17 @@ constructor(
    }
    }


    override fun clearInput() {
    override fun clearInput() {
        _password.value = ""
        textFieldState.clearText()
    }
    }


    override fun getInput(): List<Any> {
    override fun getInput(): List<Any> {
        return _password.value.toCharArray().toList()
        return textFieldState.text.toList()
    }
    }


    override fun onSuccessfulAuthentication() {
    override fun onSuccessfulAuthentication() {
        wasSuccessfullyAuthenticated = true
        wasSuccessfullyAuthenticated = true
    }
    }


    /** Notifies that the user has changed the password input. */
    fun onPasswordInputChanged(newPassword: String) {
        if (newPassword.isNotEmpty()) {
            onIntentionalUserInput()
        }

        _password.value = newPassword
    }

    /** Notifies that the user clicked the button to change the input method. */
    /** Notifies that the user clicked the button to change the input method. */
    fun onImeSwitcherButtonClicked(displayId: Int) {
    fun onImeSwitcherButtonClicked(displayId: Int) {
        requests.trySend(OnImeSwitcherButtonClicked(displayId))
        requests.trySend(OnImeSwitcherButtonClicked(displayId))
@@ -170,7 +169,7 @@ constructor(


    /** Notifies that the user has pressed the key for attempting to authenticate the password. */
    /** Notifies that the user has pressed the key for attempting to authenticate the password. */
    fun onAuthenticateKeyPressed() {
    fun onAuthenticateKeyPressed() {
        if (_password.value.isNotEmpty()) {
        if (textFieldState.text.isNotEmpty()) {
            tryAuthenticate()
            tryAuthenticate()
        }
        }
    }
    }