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

Commit 49447103 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

[flexiglass] Ignores PIN input past the hinted length.

If the user already put in a PIN that's the same length as the hinted
length, ignore any further digit input.

Fix: 330484299
Flag: ACONFIG com.android.systemui.scene_container DEVELOPMENT
Test: added unit tests
Test: manually verified that quickly entering numbers past the correct
hinted text stops at the max length and unlocks the device
Test: that repeating the above works
Test: that leaving the bouncer when the PIN is partially entered and
returning correctly clears out the entered PIN and correctly accepts the
correct PIN to unlock
Test: that entering the wrong PIN fails properly
Test: that entering a PIN longer than the real one when not hinted and
not autoconfirm still works as well

Change-Id: Ie6b429f906d88a63cae4c410fd2b97d247288c4c
parent cc057fbd
Loading
Loading
Loading
Loading
+55 −0
Original line number Diff line number Diff line
@@ -389,6 +389,61 @@ class PinBouncerViewModelTest : SysuiTestCase() {
            assertThat(isAnimationEnabled).isTrue()
        }

    @Test
    fun onPinButtonClicked_whenInputSameLengthAsHintedPin_ignoresClick() =
        testScope.runTest {
            val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
            kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true)
            val hintedPinLength by collectLastValue(underTest.hintedPinLength)
            assertThat(hintedPinLength).isEqualTo(FakeAuthenticationRepository.HINTING_PIN_LENGTH)
            lockDeviceAndOpenPinBouncer()

            repeat(FakeAuthenticationRepository.HINTING_PIN_LENGTH - 1) { repetition ->
                underTest.onPinButtonClicked(repetition + 1)
                runCurrent()
            }
            kosmos.fakeAuthenticationRepository.pauseCredentialChecking()
            // If credential checking were not paused, this would check the credentials and succeed.
            underTest.onPinButtonClicked(FakeAuthenticationRepository.HINTING_PIN_LENGTH)
            runCurrent()

            // This one should be ignored because the user has already entered a number of digits
            // that's equal to the length of the hinting PIN length. It should result in a PIN
            // that's exactly the same length as the hinting PIN length.
            underTest.onPinButtonClicked(FakeAuthenticationRepository.HINTING_PIN_LENGTH + 1)
            runCurrent()

            assertThat(pin)
                .isEqualTo(
                    buildList {
                        repeat(FakeAuthenticationRepository.HINTING_PIN_LENGTH) { index ->
                            add(index + 1)
                        }
                    }
                )

            kosmos.fakeAuthenticationRepository.unpauseCredentialChecking()
            runCurrent()
            assertThat(pin).isEmpty()
        }

    @Test
    fun onPinButtonClicked_whenPinNotHinted_doesNotIgnoreClick() =
        testScope.runTest {
            val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
            kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(false)
            val hintedPinLength by collectLastValue(underTest.hintedPinLength)
            assertThat(hintedPinLength).isNull()
            lockDeviceAndOpenPinBouncer()

            repeat(FakeAuthenticationRepository.HINTING_PIN_LENGTH + 1) { repetition ->
                underTest.onPinButtonClicked(repetition + 1)
                runCurrent()
            }

            assertThat(pin).hasSize(FakeAuthenticationRepository.HINTING_PIN_LENGTH + 1)
        }

    private fun TestScope.switchToScene(toScene: SceneKey) {
        val currentScene by collectLastValue(sceneInteractor.currentScene)
        val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer
+5 −2
Original line number Diff line number Diff line
@@ -135,9 +135,12 @@ class PinBouncerViewModel(

        onIntentionalUserInput()

        val maxInputLength = hintedPinLength.value ?: Int.MAX_VALUE
        if (pinInput.getPin().size < maxInputLength) {
            mutablePinInput.value = pinInput.append(input)
            tryAuthenticate(useAutoConfirm = true)
        }
    }

    /** Notifies that the user clicked the backspace button. */
    fun onBackspaceButtonClicked() {
+46 −23
Original line number Diff line number Diff line
@@ -32,6 +32,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.currentTime

@@ -68,6 +70,8 @@ class FakeAuthenticationRepository(

    var lockoutStartedReportCount = 0

    private val credentialCheckingMutex = Mutex(locked = false)

    override suspend fun getAuthenticationMethod(): AuthenticationMethodModel {
        return authenticationMethod.value
    }
@@ -124,6 +128,7 @@ class FakeAuthenticationRepository(
    override suspend fun checkCredential(
        credential: LockscreenCredential
    ): AuthenticationResultModel {
        return credentialCheckingMutex.withLock {
            val expectedCredential = credentialOverride ?: getExpectedCredential(securityMode)
            val isSuccessful =
                when {
@@ -138,7 +143,7 @@ class FakeAuthenticationRepository(
                }

            val failedAttempts = _failedAuthenticationAttempts.value
        return if (isSuccessful || failedAttempts < MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
            if (isSuccessful || failedAttempts < MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
                AuthenticationResultModel(
                    isSuccessful = isSuccessful,
                    lockoutDurationMs = 0,
@@ -150,11 +155,29 @@ class FakeAuthenticationRepository(
                )
            }
        }
    }

    fun setPinEnhancedPrivacyEnabled(isEnabled: Boolean) {
        _isPinEnhancedPrivacyEnabled.value = isEnabled
    }

    /**
     * Pauses any future credential checking. The test must call [unpauseCredentialChecking] to
     * flush the accumulated credential checks.
     */
    suspend fun pauseCredentialChecking() {
        credentialCheckingMutex.lock()
    }

    /**
     * Unpauses future credential checking, if it was paused using [pauseCredentialChecking]. This
     * doesn't flush any pending coroutine jobs; the test code may still choose to do that using
     * `runCurrent`.
     */
    fun unpauseCredentialChecking() {
        credentialCheckingMutex.unlock()
    }

    private fun getExpectedCredential(securityMode: SecurityMode): List<Any> {
        return when (val credentialType = getCurrentCredentialType(securityMode)) {
            LockPatternUtils.CREDENTIAL_TYPE_PIN -> credentialOverride ?: DEFAULT_PIN