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
This commit is contained in:
Joe Bolinger
2022-10-17 17:48:30 +00:00
parent f233145d7d
commit edefc7a7b2
6 changed files with 284 additions and 13 deletions

View File

@@ -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;

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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<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"
}
}

View File

@@ -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<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()
}
}

View File

@@ -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<PromptInfo?>(null)
override val promptInfo = _promptInfo.asStateFlow()
private val _userId = MutableStateFlow<Int?>(null)
override val userId = _userId.asStateFlow()
private var _challenge = MutableStateFlow<Long?>(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
}
}