From edefc7a7b253533d723417f63ac05d3cbc1fa5ca Mon Sep 17 00:00:00 2001 From: Joe Bolinger Date: Mon, 17 Oct 2022 17:48:30 +0000 Subject: [PATCH] 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 --- .../systemui/biometrics/AuthController.java | 5 + .../biometrics/dagger/BiometricsModule.kt | 33 +++--- .../biometrics/data/model/PromptKind.kt | 28 +++++ .../data/repository/PromptRepository.kt | 102 ++++++++++++++++++ .../repository/PromptRepositoryImplTest.kt | 81 ++++++++++++++ .../data/repository/FakePromptRepository.kt | 48 +++++++++ 6 files changed, 284 insertions(+), 13 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt create mode 100644 packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt create mode 100644 packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 8c7e0efee7e67..0a3da88d68394 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -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; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt index b5d81f2539161..a242d4d71b754 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt @@ -16,32 +16,39 @@ 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 { - /** Background [Executor] for HAL related operations. */ - @Provides + @Binds @SysUISingleton - @JvmStatic - @BiometricsBackground - fun providesPluginExecutor(threadFactory: ThreadFactory): Executor = - threadFactory.buildExecutorOnNewThread("biometrics") + fun biometricPromptRepository(impl: PromptRepositoryImpl): PromptRepository + + companion object { + /** Background [Executor] for HAL related operations. */ + @Provides + @SysUISingleton + @JvmStatic + @BiometricsBackground + 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 diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt new file mode 100644 index 0000000000000..e82646f0d8617 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt @@ -0,0 +1,28 @@ +/* + * 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, +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt new file mode 100644 index 0000000000000..92a13cfe538b3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt @@ -0,0 +1,102 @@ +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 + + /** The app-specific details to show in the prompt. */ + val promptInfo: StateFlow + + /** The user that the prompt is for. */ + val userId: StateFlow + + /** The gatekeeper challenge, if one is associated with this prompt. */ + val challenge: StateFlow + + /** The kind of credential to use (biometric, pin, pattern, etc.). */ + val kind: StateFlow + + /** 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 = 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 = MutableStateFlow(null) + override val promptInfo = _promptInfo.asStateFlow() + + private val _challenge: MutableStateFlow = MutableStateFlow(null) + override val challenge: StateFlow = _challenge.asStateFlow() + + private val _userId: MutableStateFlow = MutableStateFlow(null) + override val userId = _userId.asStateFlow() + + private val _kind: MutableStateFlow = 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" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt new file mode 100644 index 0000000000000..2d5614c15173f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt @@ -0,0 +1,81 @@ +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() + val job = launch { repository.isShowing.toList(values) } + assertThat(values).containsExactly(true) + + withArgCaptor { + 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() + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt new file mode 100644 index 0000000000000..96658c61109dd --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt @@ -0,0 +1,48 @@ +package com.android.systemui.biometrics.data.repository + +import android.hardware.biometrics.PromptInfo +import com.android.systemui.biometrics.data.model.PromptKind +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** Fake implementation of [PromptRepository] for tests. */ +class FakePromptRepository : PromptRepository { + + private val _isShowing = MutableStateFlow(false) + override val isShowing = _isShowing.asStateFlow() + + private val _promptInfo = MutableStateFlow(null) + override val promptInfo = _promptInfo.asStateFlow() + + private val _userId = MutableStateFlow(null) + override val userId = _userId.asStateFlow() + + private var _challenge = MutableStateFlow(null) + override val challenge = _challenge.asStateFlow() + + private val _kind = MutableStateFlow(PromptKind.ANY_BIOMETRIC) + override val kind = _kind.asStateFlow() + + override fun setPrompt( + promptInfo: PromptInfo, + userId: Int, + gatekeeperChallenge: Long?, + kind: PromptKind + ) { + _promptInfo.value = promptInfo + _userId.value = userId + _challenge.value = gatekeeperChallenge + _kind.value = kind + } + + override fun unsetPrompt() { + _promptInfo.value = null + _userId.value = null + _challenge.value = null + _kind.value = PromptKind.ANY_BIOMETRIC + } + + fun setIsShowing(showing: Boolean) { + _isShowing.value = showing + } +}