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

Commit edefc7a7 authored by Joe Bolinger's avatar Joe Bolinger
Browse files

Refactor biometric prompt credential screen - data layer.

Step 1/3, add repo layer for global application state of biometric prompt.

Bug: 251476085
Test: atest PromptRepositoryImplTest
Change-Id: Ide096c435b58ece7962f8a5b6d1c3af992885554
parent f233145d
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -1068,6 +1068,11 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,
        return mUdfpsEnrolledForUser.get(userId);
    }

    /** If BiometricPrompt is currently being shown to the user. */
    public boolean isShowing() {
        return mCurrentDialog != null;
    }

    private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) {
        mCurrentDialogArgs = args;

+20 −13
Original line number Diff line number Diff line
@@ -16,19 +16,25 @@

package com.android.systemui.biometrics.dagger

import com.android.systemui.biometrics.data.repository.PromptRepository
import com.android.systemui.biometrics.data.repository.PromptRepositoryImpl
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.util.concurrency.ThreadFactory
import dagger.Binds
import dagger.Module
import dagger.Provides
import java.util.concurrent.Executor
import javax.inject.Qualifier

/**
 * Dagger module for all things biometric.
 */
/** Dagger module for all things biometric. */
@Module
object BiometricsModule {
interface BiometricsModule {

    @Binds
    @SysUISingleton
    fun biometricPromptRepository(impl: PromptRepositoryImpl): PromptRepository

    companion object {
        /** Background [Executor] for HAL related operations. */
        @Provides
        @SysUISingleton
@@ -37,11 +43,12 @@ object BiometricsModule {
        fun providesPluginExecutor(threadFactory: ThreadFactory): Executor =
            threadFactory.buildExecutorOnNewThread("biometrics")
    }
}

/**
 * Background executor for HAL operations that are latency sensitive but too
 * slow to run on the main thread. Prefer the shared executors, such as
 * [com.android.systemui.dagger.qualifiers.Background] when a HAL is not directly involved.
 * Background executor for HAL operations that are latency sensitive but too slow to run on the main
 * thread. Prefer the shared executors, such as [com.android.systemui.dagger.qualifiers.Background]
 * when a HAL is not directly involved.
 */
@Qualifier
@MustBeDocumented
+28 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.data.model

import com.android.systemui.biometrics.Utils

// TODO(b/251476085): this should eventually replace Utils.CredentialType
/** Credential options for biometric prompt. Shadows [Utils.CredentialType]. */
enum class PromptKind {
    ANY_BIOMETRIC,
    PIN,
    PATTERN,
    PASSWORD,
}
+102 −0
Original line number Diff line number Diff line
package com.android.systemui.biometrics.data.repository

import android.hardware.biometrics.PromptInfo
import com.android.systemui.biometrics.AuthController
import com.android.systemui.biometrics.data.model.PromptKind
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

/**
 * A repository for the global state of BiometricPrompt.
 *
 * There is never more than one instance of the prompt at any given time.
 */
interface PromptRepository {

    /** If the prompt is showing. */
    val isShowing: Flow<Boolean>

    /** The app-specific details to show in the prompt. */
    val promptInfo: StateFlow<PromptInfo?>

    /** The user that the prompt is for. */
    val userId: StateFlow<Int?>

    /** The gatekeeper challenge, if one is associated with this prompt. */
    val challenge: StateFlow<Long?>

    /** The kind of credential to use (biometric, pin, pattern, etc.). */
    val kind: StateFlow<PromptKind>

    /** Update the prompt configuration, which should be set before [isShowing]. */
    fun setPrompt(
        promptInfo: PromptInfo,
        userId: Int,
        gatekeeperChallenge: Long?,
        kind: PromptKind = PromptKind.ANY_BIOMETRIC,
    )

    /** Unset the prompt info. */
    fun unsetPrompt()
}

@SysUISingleton
class PromptRepositoryImpl @Inject constructor(private val authController: AuthController) :
    PromptRepository {

    override val isShowing: Flow<Boolean> = conflatedCallbackFlow {
        val callback =
            object : AuthController.Callback {
                override fun onBiometricPromptShown() =
                    trySendWithFailureLogging(true, TAG, "set isShowing")

                override fun onBiometricPromptDismissed() =
                    trySendWithFailureLogging(false, TAG, "unset isShowing")
            }
        authController.addCallback(callback)
        trySendWithFailureLogging(authController.isShowing, TAG, "update isShowing")
        awaitClose { authController.removeCallback(callback) }
    }

    private val _promptInfo: MutableStateFlow<PromptInfo?> = MutableStateFlow(null)
    override val promptInfo = _promptInfo.asStateFlow()

    private val _challenge: MutableStateFlow<Long?> = MutableStateFlow(null)
    override val challenge: StateFlow<Long?> = _challenge.asStateFlow()

    private val _userId: MutableStateFlow<Int?> = MutableStateFlow(null)
    override val userId = _userId.asStateFlow()

    private val _kind: MutableStateFlow<PromptKind> = MutableStateFlow(PromptKind.ANY_BIOMETRIC)
    override val kind = _kind.asStateFlow()

    override fun setPrompt(
        promptInfo: PromptInfo,
        userId: Int,
        gatekeeperChallenge: Long?,
        kind: PromptKind,
    ) {
        _kind.value = kind
        _userId.value = userId
        _challenge.value = gatekeeperChallenge
        _promptInfo.value = promptInfo
    }

    override fun unsetPrompt() {
        _promptInfo.value = null
        _userId.value = null
        _challenge.value = null
        _kind.value = PromptKind.ANY_BIOMETRIC
    }

    companion object {
        private const val TAG = "BiometricPromptRepository"
    }
}
+81 −0
Original line number Diff line number Diff line
package com.android.systemui.biometrics.data.repository

import android.hardware.biometrics.PromptInfo
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.AuthController
import com.android.systemui.biometrics.data.model.PromptKind
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit

@SmallTest
@RunWith(JUnit4::class)
class PromptRepositoryImplTest : SysuiTestCase() {

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

    @Mock private lateinit var authController: AuthController

    private lateinit var repository: PromptRepositoryImpl

    @Before
    fun setup() {
        repository = PromptRepositoryImpl(authController)
    }

    @Test
    fun isShowing() = runBlockingTest {
        whenever(authController.isShowing).thenReturn(true)

        val values = mutableListOf<Boolean>()
        val job = launch { repository.isShowing.toList(values) }
        assertThat(values).containsExactly(true)

        withArgCaptor<AuthController.Callback> {
            verify(authController).addCallback(capture())

            value.onBiometricPromptShown()
            assertThat(values).containsExactly(true, true)

            value.onBiometricPromptDismissed()
            assertThat(values).containsExactly(true, true, false).inOrder()

            job.cancel()
            verify(authController).removeCallback(eq(value))
        }
    }

    @Test
    fun setsAndUnsetsPrompt() = runBlockingTest {
        val kind = PromptKind.PIN
        val uid = 8
        val challenge = 90L
        val promptInfo = PromptInfo()

        repository.setPrompt(promptInfo, uid, challenge, kind)

        assertThat(repository.kind.value).isEqualTo(kind)
        assertThat(repository.userId.value).isEqualTo(uid)
        assertThat(repository.challenge.value).isEqualTo(challenge)
        assertThat(repository.promptInfo.value).isSameInstanceAs(promptInfo)

        repository.unsetPrompt()

        assertThat(repository.promptInfo.value).isNull()
        assertThat(repository.userId.value).isNull()
        assertThat(repository.challenge.value).isNull()
    }
}
Loading