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

Commit 78f39ef0 authored by Austin Delgado's avatar Austin Delgado
Browse files

Update BP fallback view for when watch ranging is idle

Bug: 439835083
Test: atest PromptFallbackViewModelTest
Flag: android.hardware.biometrics.bp_fallback_options
Change-Id: I9d947f7ffe15c27c6f3aef716d7a234c4b9f264d
parent a8d0684b
Loading
Loading
Loading
Loading
+4 −2
Original line number Diff line number Diff line
@@ -62,7 +62,6 @@ constructor(val promptSelectorInteractor: PromptSelectorInteractor) {
    val icCredentialSubtitle: Flow<Int?> =
        watchRangingState.map { status ->
            when (status) {
                WatchRangingState.WATCH_RANGING_IDLE,
                WatchRangingState.WATCH_RANGING_STARTED -> {
                    R.string.biometric_dialog_identity_check_watch_ranging
                }
@@ -77,7 +76,10 @@ constructor(val promptSelectorInteractor: PromptSelectorInteractor) {

    /** Whether to show the identity check footer text */
    val icShowFooter: Flow<Boolean> =
        watchRangingState.map { status -> status == WatchRangingState.WATCH_RANGING_STOPPED }
        watchRangingState.map { status ->
            status == WatchRangingState.WATCH_RANGING_STOPPED ||
                status == WatchRangingState.WATCH_RANGING_IDLE
        }

    /** Whether the credential fallback button should be shown */
    val showCredential: Flow<Boolean> =
+162 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.biometrics.ui.viewmodel

import android.hardware.biometrics.IIdentityCheckStateListener
import android.hardware.biometrics.PromptInfo
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.biometricManager
import com.android.systemui.biometrics.domain.interactor.promptSelectorInteractor
import com.android.systemui.biometrics.shared.model.BiometricModalities
import com.android.systemui.biometrics.shared.model.WatchRangingState
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R
import com.android.systemui.testKosmos
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.Rule
import org.junit.Test
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit

@SmallTest
class PromptFallbackViewModelTest : SysuiTestCase() {

    @JvmField @Rule var mockitoRule = MockitoJUnit.rule()

    @Captor
    private lateinit var identityCheckStateListenerCaptor:
        ArgumentCaptor<IIdentityCheckStateListener>

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val interactor = kosmos.promptSelectorInteractor
    private val biometricManager = kosmos.biometricManager

    private lateinit var viewModel: PromptFallbackViewModel

    @Before
    fun setUp() {
        viewModel = PromptFallbackViewModel(interactor)
    }

    @OptIn(ExperimentalCoroutinesApi::class)
    @Test
    fun icCredentialButtonSubtitleAndFooter() =
        testScope.runTest {
            val isEnabled by collectLastValue(viewModel.icCredentialButtonEnabled)
            val subtitle by collectLastValue(viewModel.icCredentialSubtitle)
            val showFooter by collectLastValue(viewModel.icShowFooter)

            runCurrent()

            verify(biometricManager)
                .registerIdentityCheckStateListener(identityCheckStateListenerCaptor.capture())
            val listener = identityCheckStateListenerCaptor.value

            // WATCH_RANGING_IDLE - Button disabled, footer, no subtitle
            listener.onWatchRangingStateChanged(WatchRangingState.WATCH_RANGING_IDLE.ordinal)
            assertThat(isEnabled).isFalse()
            assertThat(subtitle).isNull()
            assertThat(showFooter).isTrue()

            // WATCH_RANGING_STARTED - Button disabled, no footer, ranging subtitle
            listener.onWatchRangingStateChanged(WatchRangingState.WATCH_RANGING_STARTED.ordinal)
            assertThat(isEnabled).isFalse()
            assertThat(subtitle).isEqualTo(R.string.biometric_dialog_identity_check_watch_ranging)
            assertThat(showFooter).isFalse()

            // WATCH_RANGING_SUCCESSFUL - Button enabled, no footer, no subtitle
            listener.onWatchRangingStateChanged(WatchRangingState.WATCH_RANGING_SUCCESSFUL.ordinal)
            assertThat(isEnabled).isTrue()
            assertThat(subtitle).isNull()
            assertThat(showFooter).isFalse()

            // WATCH_RANGING_STOPPED - Button disabled, footer, disabled subtitle
            listener.onWatchRangingStateChanged(WatchRangingState.WATCH_RANGING_STOPPED.ordinal)
            assertThat(isEnabled).isFalse()
            assertThat(subtitle).isEqualTo(R.string.biometric_dialog_unavailable)
            assertThat(showFooter).isTrue()
        }

    @Test
    fun showCredentialAndManageIdentityCheckButtons() =
        testScope.runTest {
            val showCredential by collectLastValue(viewModel.showCredential)
            val showManageIdentityCheck by collectLastValue(viewModel.showManageIdentityCheck)

            // When credential is allowed and identity check is inactive, show credential button
            setPrompt(
                PromptInfo().apply {
                    isDeviceCredentialAllowed = true
                    isIdentityCheckActive = false
                }
            )
            assertThat(showCredential).isTrue()
            assertThat(showManageIdentityCheck).isFalse()

            // When credential is allowed and identity check is active, show manage button
            setPrompt(
                PromptInfo().apply {
                    isDeviceCredentialAllowed = true
                    isIdentityCheckActive = true
                }
            )
            assertThat(showCredential).isFalse()
            assertThat(showManageIdentityCheck).isTrue()

            // When credential is not allowed, show neither button
            setPrompt(
                PromptInfo().apply {
                    isDeviceCredentialAllowed = false
                    isIdentityCheckActive = false
                }
            )
            assertThat(showCredential).isFalse()
            assertThat(showManageIdentityCheck).isFalse()

            // When credential is allowed and identity check is not active, show neither
            setPrompt(
                PromptInfo().apply {
                    isDeviceCredentialAllowed = false
                    isIdentityCheckActive = true
                }
            )
            assertThat(showCredential).isFalse()
            assertThat(showManageIdentityCheck).isFalse()
        }

    private fun setPrompt(promptInfo: PromptInfo) {
        interactor.setPrompt(
            promptInfo,
            0,
            0,
            BiometricModalities(),
            0L,
            "",
            onSwitchToCredential = false,
            isLandscape = false,
        )
    }
}
+23 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.biometrics

import android.hardware.biometrics.BiometricManager
import com.android.systemui.kosmos.Kosmos
import org.mockito.kotlin.mock

var Kosmos.biometricManager by Kosmos.Fixture { mock<BiometricManager>() }
+2 −2
Original line number Diff line number Diff line
@@ -16,9 +16,9 @@

package com.android.systemui.biometrics.domain.interactor

import android.hardware.biometrics.BiometricManager
import com.android.internal.widget.lockPatternUtils
import com.android.systemui.biometrics.BiometricPromptLogger
import com.android.systemui.biometrics.biometricManager
import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
import com.android.systemui.biometrics.data.repository.promptRepository
import com.android.systemui.display.domain.interactor.displayStateInteractor
@@ -35,7 +35,7 @@ val Kosmos.promptSelectorInteractor by Fixture {
        promptRepository = promptRepository,
        credentialInteractor = credentialInteractor,
        lockPatternUtils = lockPatternUtils,
        biometricManager = mock<BiometricManager>(),
        biometricManager = biometricManager,
        bgScope = testScope.backgroundScope,
        sessionTracker = mock<SessionTracker>(),
        biometricPromptLogger = mock<BiometricPromptLogger>(),