summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/bouncer/data/repository/BouncerMessageRepository.kt332
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageInteractor.kt190
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AuthenticationFlags.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/log/BouncerLogger.kt62
-rw-r--r--packages/SystemUI/src/com/android/systemui/log/dagger/BouncerLog.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/log/dagger/BouncerTableLog.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java15
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/data/repository/BouncerMessageRepositoryTest.kt363
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageInteractorTest.kt285
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBouncerMessageRepository.kt84
13 files changed, 1366 insertions, 14 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/data/repository/BouncerMessageRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/data/repository/BouncerMessageRepository.kt
new file mode 100644
index 000000000000..c4400bcb6b21
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/data/repository/BouncerMessageRepository.kt
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2023 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.keyguard.bouncer.data.repository
+
+import android.hardware.biometrics.BiometricSourceType
+import android.hardware.biometrics.BiometricSourceType.FACE
+import android.hardware.biometrics.BiometricSourceType.FINGERPRINT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEVICE_ADMIN
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_FACE_LOCKED_OUT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_FINGERPRINT_LOCKED_OUT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_FACE_INPUT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NONE
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.bouncer.data.factory.BouncerMessageFactory
+import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
+import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.TrustRepository
+import com.android.systemui.user.data.repository.UserRepository
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+
+/** Provide different sources of messages that needs to be shown on the bouncer. */
+interface BouncerMessageRepository {
+ /**
+ * Messages that are shown in response to the incorrect security attempts on the bouncer and
+ * primary authentication method being locked out, along with countdown messages before primary
+ * auth is active again.
+ */
+ val primaryAuthMessage: Flow<BouncerMessageModel?>
+
+ /**
+ * Help messages that are shown to the user on how to successfully perform authentication using
+ * face.
+ */
+ val faceAcquisitionMessage: Flow<BouncerMessageModel?>
+
+ /**
+ * Help messages that are shown to the user on how to successfully perform authentication using
+ * fingerprint.
+ */
+ val fingerprintAcquisitionMessage: Flow<BouncerMessageModel?>
+
+ /** Custom message that is displayed when the bouncer is being shown to launch an app. */
+ val customMessage: Flow<BouncerMessageModel?>
+
+ /**
+ * Messages that are shown in response to biometric authentication attempts through face or
+ * fingerprint.
+ */
+ val biometricAuthMessage: Flow<BouncerMessageModel?>
+
+ /** Messages that are shown when certain auth flags are set. */
+ val authFlagsMessage: Flow<BouncerMessageModel?>
+
+ /** Messages that are show after biometrics are locked out temporarily or permanently */
+ val biometricLockedOutMessage: Flow<BouncerMessageModel?>
+
+ /** Set the value for [primaryAuthMessage] */
+ fun setPrimaryAuthMessage(value: BouncerMessageModel?)
+
+ /** Set the value for [faceAcquisitionMessage] */
+ fun setFaceAcquisitionMessage(value: BouncerMessageModel?)
+ /** Set the value for [fingerprintAcquisitionMessage] */
+ fun setFingerprintAcquisitionMessage(value: BouncerMessageModel?)
+
+ /** Set the value for [customMessage] */
+ fun setCustomMessage(value: BouncerMessageModel?)
+
+ /**
+ * Clear any previously set messages for [primaryAuthMessage], [faceAcquisitionMessage],
+ * [fingerprintAcquisitionMessage] & [customMessage]
+ */
+ fun clearMessage()
+}
+
+@SysUISingleton
+class BouncerMessageRepositoryImpl
+@Inject
+constructor(
+ trustRepository: TrustRepository,
+ biometricSettingsRepository: BiometricSettingsRepository,
+ updateMonitor: KeyguardUpdateMonitor,
+ private val bouncerMessageFactory: BouncerMessageFactory,
+ private val userRepository: UserRepository,
+ fingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
+) : BouncerMessageRepository {
+
+ private val isFaceEnrolledAndEnabled =
+ and(
+ biometricSettingsRepository.isFaceAuthenticationEnabled,
+ biometricSettingsRepository.isFaceEnrolled
+ )
+
+ private val isFingerprintEnrolledAndEnabled =
+ and(
+ biometricSettingsRepository.isFingerprintEnabledByDevicePolicy,
+ biometricSettingsRepository.isFingerprintEnrolled
+ )
+
+ private val isAnyBiometricsEnabledAndEnrolled =
+ or(isFaceEnrolledAndEnabled, isFingerprintEnrolledAndEnabled)
+
+ private val authFlagsBasedPromptReason: Flow<Int> =
+ combine(
+ biometricSettingsRepository.authenticationFlags,
+ trustRepository.isCurrentUserTrustManaged,
+ isAnyBiometricsEnabledAndEnrolled,
+ ::Triple
+ )
+ .map { (flags, isTrustManaged, biometricsEnrolledAndEnabled) ->
+ val trustOrBiometricsAvailable = (isTrustManaged || biometricsEnrolledAndEnabled)
+ return@map if (
+ trustOrBiometricsAvailable && flags.isPrimaryAuthRequiredAfterReboot
+ ) {
+ PROMPT_REASON_RESTART
+ } else if (trustOrBiometricsAvailable && flags.isPrimaryAuthRequiredAfterTimeout) {
+ PROMPT_REASON_TIMEOUT
+ } else if (flags.isPrimaryAuthRequiredAfterDpmLockdown) {
+ PROMPT_REASON_DEVICE_ADMIN
+ } else if (isTrustManaged && flags.someAuthRequiredAfterUserRequest) {
+ PROMPT_REASON_TRUSTAGENT_EXPIRED
+ } else if (isTrustManaged && flags.someAuthRequiredAfterTrustAgentExpired) {
+ PROMPT_REASON_TRUSTAGENT_EXPIRED
+ } else if (trustOrBiometricsAvailable && flags.isInUserLockdown) {
+ PROMPT_REASON_USER_REQUEST
+ } else if (
+ trustOrBiometricsAvailable && flags.primaryAuthRequiredForUnattendedUpdate
+ ) {
+ PROMPT_REASON_PREPARE_FOR_UPDATE
+ } else if (
+ trustOrBiometricsAvailable &&
+ flags.strongerAuthRequiredAfterNonStrongBiometricsTimeout
+ ) {
+ PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT
+ } else {
+ PROMPT_REASON_NONE
+ }
+ }
+
+ private val biometricAuthReason: Flow<Int> =
+ conflatedCallbackFlow {
+ val callback =
+ object : KeyguardUpdateMonitorCallback() {
+ override fun onBiometricAuthFailed(
+ biometricSourceType: BiometricSourceType?
+ ) {
+ val promptReason =
+ if (biometricSourceType == FINGERPRINT)
+ PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT
+ else if (
+ biometricSourceType == FACE && !updateMonitor.isFaceLockedOut
+ ) {
+ PROMPT_REASON_INCORRECT_FACE_INPUT
+ } else PROMPT_REASON_NONE
+ trySendWithFailureLogging(promptReason, TAG, "onBiometricAuthFailed")
+ }
+
+ override fun onBiometricsCleared() {
+ trySendWithFailureLogging(
+ PROMPT_REASON_NONE,
+ TAG,
+ "onBiometricsCleared"
+ )
+ }
+
+ override fun onBiometricAcquired(
+ biometricSourceType: BiometricSourceType?,
+ acquireInfo: Int
+ ) {
+ trySendWithFailureLogging(
+ PROMPT_REASON_NONE,
+ TAG,
+ "clearBiometricPrompt for new auth session."
+ )
+ }
+
+ override fun onBiometricAuthenticated(
+ userId: Int,
+ biometricSourceType: BiometricSourceType?,
+ isStrongBiometric: Boolean
+ ) {
+ trySendWithFailureLogging(
+ PROMPT_REASON_NONE,
+ TAG,
+ "onBiometricAuthenticated"
+ )
+ }
+ }
+ updateMonitor.registerCallback(callback)
+ awaitClose { updateMonitor.removeCallback(callback) }
+ }
+ .distinctUntilChanged()
+
+ private val _primaryAuthMessage = MutableStateFlow<BouncerMessageModel?>(null)
+ override val primaryAuthMessage: Flow<BouncerMessageModel?> = _primaryAuthMessage
+
+ private val _faceAcquisitionMessage = MutableStateFlow<BouncerMessageModel?>(null)
+ override val faceAcquisitionMessage: Flow<BouncerMessageModel?> = _faceAcquisitionMessage
+
+ private val _fingerprintAcquisitionMessage = MutableStateFlow<BouncerMessageModel?>(null)
+ override val fingerprintAcquisitionMessage: Flow<BouncerMessageModel?> =
+ _fingerprintAcquisitionMessage
+
+ private val _customMessage = MutableStateFlow<BouncerMessageModel?>(null)
+ override val customMessage: Flow<BouncerMessageModel?> = _customMessage
+
+ override val biometricAuthMessage: Flow<BouncerMessageModel?> =
+ biometricAuthReason
+ .map {
+ if (it == PROMPT_REASON_NONE) null
+ else
+ bouncerMessageFactory.createFromPromptReason(
+ it,
+ userRepository.getSelectedUserInfo().id
+ )
+ }
+ .onStart { emit(null) }
+ .distinctUntilChanged()
+
+ override val authFlagsMessage: Flow<BouncerMessageModel?> =
+ authFlagsBasedPromptReason
+ .map {
+ if (it == PROMPT_REASON_NONE) null
+ else
+ bouncerMessageFactory.createFromPromptReason(
+ it,
+ userRepository.getSelectedUserInfo().id
+ )
+ }
+ .onStart { emit(null) }
+ .distinctUntilChanged()
+
+ // TODO (b/262838215): Replace with DeviceEntryFaceAuthRepository when the new face auth system
+ // has been launched.
+ private val faceLockedOut: Flow<Boolean> = conflatedCallbackFlow {
+ val callback =
+ object : KeyguardUpdateMonitorCallback() {
+ override fun onLockedOutStateChanged(biometricSourceType: BiometricSourceType?) {
+ if (biometricSourceType == FACE) {
+ trySendWithFailureLogging(
+ updateMonitor.isFaceLockedOut,
+ TAG,
+ "face lock out state changed."
+ )
+ }
+ }
+ }
+ updateMonitor.registerCallback(callback)
+ trySendWithFailureLogging(updateMonitor.isFaceLockedOut, TAG, "face lockout initial value")
+ awaitClose { updateMonitor.removeCallback(callback) }
+ }
+
+ override val biometricLockedOutMessage: Flow<BouncerMessageModel?> =
+ combine(fingerprintAuthRepository.isLockedOut, faceLockedOut) { fp, face ->
+ return@combine if (fp) {
+ bouncerMessageFactory.createFromPromptReason(
+ PROMPT_REASON_FINGERPRINT_LOCKED_OUT,
+ userRepository.getSelectedUserInfo().id
+ )
+ } else if (face) {
+ bouncerMessageFactory.createFromPromptReason(
+ PROMPT_REASON_FACE_LOCKED_OUT,
+ userRepository.getSelectedUserInfo().id
+ )
+ } else null
+ }
+
+ override fun setPrimaryAuthMessage(value: BouncerMessageModel?) {
+ _primaryAuthMessage.value = value
+ }
+
+ override fun setFaceAcquisitionMessage(value: BouncerMessageModel?) {
+ _faceAcquisitionMessage.value = value
+ }
+
+ override fun setFingerprintAcquisitionMessage(value: BouncerMessageModel?) {
+ _fingerprintAcquisitionMessage.value = value
+ }
+
+ override fun setCustomMessage(value: BouncerMessageModel?) {
+ _customMessage.value = value
+ }
+
+ override fun clearMessage() {
+ _fingerprintAcquisitionMessage.value = null
+ _faceAcquisitionMessage.value = null
+ _primaryAuthMessage.value = null
+ _customMessage.value = null
+ }
+
+ companion object {
+ const val TAG = "BouncerDetailedMessageRepository"
+ }
+}
+
+private fun and(flow: Flow<Boolean>, anotherFlow: Flow<Boolean>) =
+ flow.combine(anotherFlow) { a, b -> a && b }
+
+private fun or(flow: Flow<Boolean>, anotherFlow: Flow<Boolean>) =
+ flow.combine(anotherFlow) { a, b -> a || b }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageInteractor.kt
new file mode 100644
index 000000000000..1754d934ee3a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageInteractor.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2023 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.keyguard.bouncer.domain.interactor
+
+import android.os.CountDownTimer
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEFAULT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.REVAMPED_BOUNCER_MESSAGES
+import com.android.systemui.keyguard.bouncer.data.factory.BouncerMessageFactory
+import com.android.systemui.keyguard.bouncer.data.repository.BouncerMessageRepository
+import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
+import com.android.systemui.user.data.repository.UserRepository
+import javax.inject.Inject
+import kotlin.math.roundToInt
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+@SysUISingleton
+class BouncerMessageInteractor
+@Inject
+constructor(
+ private val repository: BouncerMessageRepository,
+ private val factory: BouncerMessageFactory,
+ private val userRepository: UserRepository,
+ private val countDownTimerUtil: CountDownTimerUtil,
+ private val featureFlags: FeatureFlags,
+) {
+ fun onPrimaryAuthLockedOut(secondsBeforeLockoutReset: Long) {
+ if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
+
+ val callback =
+ object : CountDownTimerCallback {
+ override fun onFinish() {
+ repository.clearMessage()
+ }
+
+ override fun onTick(millisUntilFinished: Long) {
+ val secondsRemaining = (millisUntilFinished / 1000.0).roundToInt()
+ val message =
+ factory.createFromPromptReason(
+ reason = PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT,
+ userId = userRepository.getSelectedUserInfo().id
+ )
+ message?.message?.animate = false
+ message?.message?.formatterArgs =
+ mutableMapOf<String, Any>(Pair("count", secondsRemaining))
+ repository.setPrimaryAuthMessage(message)
+ }
+ }
+ countDownTimerUtil.startNewTimer(secondsBeforeLockoutReset * 1000, 1000, callback)
+ }
+
+ fun onPrimaryAuthIncorrectAttempt() {
+ if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
+
+ repository.setPrimaryAuthMessage(
+ factory.createFromPromptReason(
+ PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
+ userRepository.getSelectedUserInfo().id
+ )
+ )
+ }
+
+ fun setFingerprintAcquisitionMessage(value: String?) {
+ if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
+
+ repository.setFingerprintAcquisitionMessage(
+ if (value != null) {
+ factory.createFromString(secondaryMsg = value)
+ } else {
+ null
+ }
+ )
+ }
+
+ fun setFaceAcquisitionMessage(value: String?) {
+ if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
+
+ repository.setFaceAcquisitionMessage(
+ if (value != null) {
+ factory.createFromString(secondaryMsg = value)
+ } else {
+ null
+ }
+ )
+ }
+
+ fun setCustomMessage(value: String?) {
+ if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
+
+ repository.setCustomMessage(
+ if (value != null) {
+ factory.createFromString(secondaryMsg = value)
+ } else {
+ null
+ }
+ )
+ }
+
+ fun onPrimaryBouncerUserInput() {
+ if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
+
+ repository.clearMessage()
+ }
+
+ fun onBouncerBeingHidden() {
+ if (!featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) return
+
+ repository.clearMessage()
+ }
+
+ private fun firstNonNullMessage(
+ oneMessageModel: Flow<BouncerMessageModel?>,
+ anotherMessageModel: Flow<BouncerMessageModel?>
+ ): Flow<BouncerMessageModel?> {
+ return oneMessageModel.combine(anotherMessageModel) { a, b -> a ?: b }
+ }
+
+ // Null if feature flag is enabled which gets ignored always or empty bouncer message model that
+ // always maps to an empty string.
+ private fun nullOrEmptyMessage() =
+ flowOf(
+ if (featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) null
+ else factory.createFromString("", "")
+ )
+
+ val bouncerMessage =
+ listOf(
+ nullOrEmptyMessage(),
+ repository.primaryAuthMessage,
+ repository.biometricAuthMessage,
+ repository.fingerprintAcquisitionMessage,
+ repository.faceAcquisitionMessage,
+ repository.customMessage,
+ repository.authFlagsMessage,
+ repository.biometricLockedOutMessage,
+ userRepository.selectedUserInfo.map {
+ factory.createFromPromptReason(PROMPT_REASON_DEFAULT, it.id)
+ },
+ )
+ .reduce(::firstNonNullMessage)
+ .distinctUntilChanged()
+}
+
+interface CountDownTimerCallback {
+ fun onFinish()
+ fun onTick(millisUntilFinished: Long)
+}
+
+@SysUISingleton
+open class CountDownTimerUtil @Inject constructor() {
+
+ /**
+ * Start a new count down timer that runs for [millisInFuture] with a tick every
+ * [millisInterval]
+ */
+ fun startNewTimer(
+ millisInFuture: Long,
+ millisInterval: Long,
+ callback: CountDownTimerCallback,
+ ): CountDownTimer {
+ return object : CountDownTimer(millisInFuture, millisInterval) {
+ override fun onFinish() = callback.onFinish()
+
+ override fun onTick(millisUntilFinished: Long) =
+ callback.onTick(millisUntilFinished)
+ }
+ .start()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
index 7c14280a7858..5d15e69f0162 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
@@ -22,7 +22,7 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN
import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
-import com.android.systemui.log.dagger.BouncerLog
+import com.android.systemui.log.dagger.BouncerTableLog
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.util.time.SystemClock
@@ -105,7 +105,7 @@ class KeyguardBouncerRepositoryImpl
constructor(
private val clock: SystemClock,
@Application private val applicationScope: CoroutineScope,
- @BouncerLog private val buffer: TableLogBuffer,
+ @BouncerTableLog private val buffer: TableLogBuffer,
) : KeyguardBouncerRepository {
/** Values associated with the PrimaryBouncer (pin/pattern/password) input. */
private val _primaryBouncerShow = MutableStateFlow(false)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt
index e7b9af62dacf..6081c47d29b2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt
@@ -16,6 +16,8 @@
package com.android.systemui.keyguard.data.repository
+import com.android.systemui.keyguard.bouncer.data.repository.BouncerMessageRepository
+import com.android.systemui.keyguard.bouncer.data.repository.BouncerMessageRepositoryImpl
import dagger.Binds
import dagger.Module
@@ -46,5 +48,8 @@ interface KeyguardRepositoryModule {
@Binds
fun keyguardBouncerRepository(impl: KeyguardBouncerRepositoryImpl): KeyguardBouncerRepository
+ @Binds
+ fun bouncerMessageRepository(impl: BouncerMessageRepositoryImpl): BouncerMessageRepository
+
@Binds fun trustRepository(impl: TrustRepositoryImpl): TrustRepository
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AuthenticationFlags.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AuthenticationFlags.kt
index 0bbf67fb49e8..cf5b88fde3dc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AuthenticationFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AuthenticationFlags.kt
@@ -47,9 +47,6 @@ data class AuthenticationFlags(val userId: Int, val flag: Int) {
LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED
)
- val primaryAuthRequiredAfterLockout =
- containsFlag(flag, LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT)
-
val primaryAuthRequiredForUnattendedUpdate =
containsFlag(
flag,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
index c8bd958615cf..9e7dec4dc1d0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt
@@ -38,7 +38,8 @@ data class AcquiredAuthenticationStatus(val acquiredInfo: Int) : AuthenticationS
object FailedAuthenticationStatus : AuthenticationStatus()
/** Face authentication error message */
-data class ErrorAuthenticationStatus(val msgId: Int, val msg: String?) : AuthenticationStatus() {
+data class ErrorAuthenticationStatus(val msgId: Int, val msg: String? = null) :
+ AuthenticationStatus() {
/**
* Method that checks if [msgId] is a lockout error. A lockout error means that face
* authentication is locked out.
diff --git a/packages/SystemUI/src/com/android/systemui/log/BouncerLogger.kt b/packages/SystemUI/src/com/android/systemui/log/BouncerLogger.kt
new file mode 100644
index 000000000000..3be4499517b9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/BouncerLogger.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2023 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.log
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
+import com.android.systemui.log.dagger.BouncerLog
+import javax.inject.Inject
+
+private const val TAG = "BouncerLog"
+
+/**
+ * Helper class for logging for classes in the [com.android.systemui.keyguard.bouncer] package.
+ *
+ * To enable logcat echoing for an entire buffer:
+ * ```
+ * adb shell settings put global systemui/buffer/BouncerLog <logLevel>
+ *
+ * ```
+ */
+@SysUISingleton
+class BouncerLogger @Inject constructor(@BouncerLog private val buffer: LogBuffer) {
+ fun startBouncerMessageInteractor() {
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ "Starting BouncerMessageInteractor.bouncerMessage collector"
+ )
+ }
+
+ fun bouncerMessageUpdated(bouncerMsg: BouncerMessageModel?) {
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ {
+ int1 = bouncerMsg?.message?.messageResId ?: -1
+ str1 = bouncerMsg?.message?.message
+ int2 = bouncerMsg?.secondaryMessage?.messageResId ?: -1
+ str2 = bouncerMsg?.secondaryMessage?.message
+ },
+ { "Bouncer message update received: $int1, $str1, $int2, $str2" }
+ )
+ }
+
+ fun bindingBouncerMessageView() {
+ buffer.log(TAG, LogLevel.DEBUG, "Binding BouncerMessageView")
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/BouncerLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/BouncerLog.kt
index 2251a7b243aa..0c2e7319efc1 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/BouncerLog.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/BouncerLog.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 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.
@@ -16,10 +16,7 @@
package com.android.systemui.log.dagger
-import java.lang.annotation.Documented
-import java.lang.annotation.Retention
-import java.lang.annotation.RetentionPolicy
import javax.inject.Qualifier
-/** Logger for the primary and alternative bouncers. */
-@Qualifier @Documented @Retention(RetentionPolicy.RUNTIME) annotation class BouncerLog
+/** A [com.android.systemui.log.LogBuffer] for bouncer and its child views. */
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class BouncerLog()
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/BouncerTableLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/BouncerTableLog.kt
new file mode 100644
index 000000000000..08df7db65af1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/BouncerTableLog.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.log.dagger
+
+import java.lang.annotation.Documented
+import java.lang.annotation.Retention
+import java.lang.annotation.RetentionPolicy
+import javax.inject.Qualifier
+
+/** Logger for the primary and alternative bouncers. */
+@Qualifier @Documented @Retention(RetentionPolicy.RUNTIME) annotation class BouncerTableLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 408628f3842b..105ae3a4f12c 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -394,6 +394,17 @@ public class LogModule {
}
/**
+ * Provides a {@link LogBuffer} for use by classes in the
+ * {@link com.android.systemui.keyguard.bouncer} package.
+ */
+ @Provides
+ @SysUISingleton
+ @BouncerLog
+ public static LogBuffer provideBouncerLog(LogBufferFactory factory) {
+ return factory.create("BouncerLog", 100);
+ }
+
+ /**
* Provides a {@link LogBuffer} for Device State Auto-Rotation logs.
*/
@Provides
@@ -416,9 +427,9 @@ public class LogModule {
/** Provides a logging buffer for the primary bouncer. */
@Provides
@SysUISingleton
- @BouncerLog
+ @BouncerTableLog
public static TableLogBuffer provideBouncerLogBuffer(TableLogBufferFactory factory) {
- return factory.create("BouncerLog", 250);
+ return factory.create("BouncerTableLog", 250);
}
/** Provides a table logging buffer for the Monitor. */
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/data/repository/BouncerMessageRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/data/repository/BouncerMessageRepositoryTest.kt
new file mode 100644
index 000000000000..1277fc0e1bdd
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/data/repository/BouncerMessageRepositoryTest.kt
@@ -0,0 +1,363 @@
+/*
+ * Copyright (C) 2023 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.keyguard.bouncer.data.repository
+
+import android.content.pm.UserInfo
+import android.hardware.biometrics.BiometricSourceType
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.systemui.R
+import com.android.systemui.R.string.keyguard_enter_pin
+import com.android.systemui.R.string.kg_prompt_after_dpm_lock
+import com.android.systemui.R.string.kg_prompt_after_user_lockdown_pin
+import com.android.systemui.R.string.kg_prompt_auth_timeout
+import com.android.systemui.R.string.kg_prompt_pin_auth_timeout
+import com.android.systemui.R.string.kg_prompt_reason_restart_pin
+import com.android.systemui.R.string.kg_prompt_unattended_update
+import com.android.systemui.R.string.kg_trust_agent_disabled
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.bouncer.data.factory.BouncerMessageFactory
+import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
+import com.android.systemui.keyguard.bouncer.shared.model.Message
+import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.FakeTrustRepository
+import com.android.systemui.keyguard.shared.model.AuthenticationFlags
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidJUnit4::class)
+class BouncerMessageRepositoryTest : SysuiTestCase() {
+
+ @Mock private lateinit var updateMonitor: KeyguardUpdateMonitor
+ @Mock private lateinit var securityModel: KeyguardSecurityModel
+ @Captor
+ private lateinit var updateMonitorCallback: ArgumentCaptor<KeyguardUpdateMonitorCallback>
+
+ private lateinit var underTest: BouncerMessageRepository
+ private lateinit var trustRepository: FakeTrustRepository
+ private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
+ private lateinit var userRepository: FakeUserRepository
+ private lateinit var fingerprintRepository: FakeDeviceEntryFingerprintAuthRepository
+ private lateinit var testScope: TestScope
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ trustRepository = FakeTrustRepository()
+ biometricSettingsRepository = FakeBiometricSettingsRepository()
+ userRepository = FakeUserRepository()
+ userRepository.setUserInfos(listOf(PRIMARY_USER))
+ fingerprintRepository = FakeDeviceEntryFingerprintAuthRepository()
+ testScope = TestScope()
+
+ whenever(updateMonitor.isFingerprintAllowedInBouncer).thenReturn(false)
+ whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN)
+ underTest =
+ BouncerMessageRepositoryImpl(
+ trustRepository = trustRepository,
+ biometricSettingsRepository = biometricSettingsRepository,
+ updateMonitor = updateMonitor,
+ bouncerMessageFactory = BouncerMessageFactory(updateMonitor, securityModel),
+ userRepository = userRepository,
+ fingerprintAuthRepository = fingerprintRepository
+ )
+ }
+
+ @Test
+ fun setCustomMessage_propagatesState() =
+ testScope.runTest {
+ underTest.setCustomMessage(message("not empty"))
+
+ val customMessage = collectLastValue(underTest.customMessage)
+
+ assertThat(customMessage()).isEqualTo(message("not empty"))
+ }
+
+ @Test
+ fun setFaceMessage_propagatesState() =
+ testScope.runTest {
+ underTest.setFaceAcquisitionMessage(message("not empty"))
+
+ val faceAcquisitionMessage = collectLastValue(underTest.faceAcquisitionMessage)
+
+ assertThat(faceAcquisitionMessage()).isEqualTo(message("not empty"))
+ }
+
+ @Test
+ fun setFpMessage_propagatesState() =
+ testScope.runTest {
+ underTest.setFingerprintAcquisitionMessage(message("not empty"))
+
+ val fpAcquisitionMsg = collectLastValue(underTest.fingerprintAcquisitionMessage)
+
+ assertThat(fpAcquisitionMsg()).isEqualTo(message("not empty"))
+ }
+
+ @Test
+ fun setPrimaryAuthMessage_propagatesState() =
+ testScope.runTest {
+ underTest.setPrimaryAuthMessage(message("not empty"))
+
+ val primaryAuthMessage = collectLastValue(underTest.primaryAuthMessage)
+
+ assertThat(primaryAuthMessage()).isEqualTo(message("not empty"))
+ }
+
+ @Test
+ fun biometricAuthMessage_propagatesBiometricAuthMessages() =
+ testScope.runTest {
+ userRepository.setSelectedUserInfo(PRIMARY_USER)
+ val biometricAuthMessage = collectLastValue(underTest.biometricAuthMessage)
+ runCurrent()
+
+ verify(updateMonitor).registerCallback(updateMonitorCallback.capture())
+
+ updateMonitorCallback.value.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT)
+
+ assertThat(biometricAuthMessage())
+ .isEqualTo(message(R.string.kg_fp_not_recognized, R.string.kg_bio_try_again_or_pin))
+
+ updateMonitorCallback.value.onBiometricAuthFailed(BiometricSourceType.FACE)
+
+ assertThat(biometricAuthMessage())
+ .isEqualTo(
+ message(R.string.bouncer_face_not_recognized, R.string.kg_bio_try_again_or_pin)
+ )
+
+ updateMonitorCallback.value.onBiometricAcquired(BiometricSourceType.FACE, 0)
+
+ assertThat(biometricAuthMessage()).isNull()
+ }
+
+ @Test
+ fun onFaceLockout_propagatesState() =
+ testScope.runTest {
+ userRepository.setSelectedUserInfo(PRIMARY_USER)
+ val lockoutMessage = collectLastValue(underTest.biometricLockedOutMessage)
+ runCurrent()
+ verify(updateMonitor).registerCallback(updateMonitorCallback.capture())
+
+ whenever(updateMonitor.isFaceLockedOut).thenReturn(true)
+ updateMonitorCallback.value.onLockedOutStateChanged(BiometricSourceType.FACE)
+
+ assertThat(lockoutMessage())
+ .isEqualTo(message(keyguard_enter_pin, R.string.kg_face_locked_out))
+
+ whenever(updateMonitor.isFaceLockedOut).thenReturn(false)
+ updateMonitorCallback.value.onLockedOutStateChanged(BiometricSourceType.FACE)
+ assertThat(lockoutMessage()).isNull()
+ }
+
+ @Test
+ fun onFingerprintLockout_propagatesState() =
+ testScope.runTest {
+ userRepository.setSelectedUserInfo(PRIMARY_USER)
+ val lockedOutMessage = collectLastValue(underTest.biometricLockedOutMessage)
+ runCurrent()
+
+ fingerprintRepository.setLockedOut(true)
+
+ assertThat(lockedOutMessage())
+ .isEqualTo(message(keyguard_enter_pin, R.string.kg_fp_locked_out))
+
+ fingerprintRepository.setLockedOut(false)
+ assertThat(lockedOutMessage()).isNull()
+ }
+
+ @Test
+ fun onAuthFlagsChanged_withTrustNotManagedAndNoBiometrics_isANoop() =
+ testScope.runTest {
+ userRepository.setSelectedUserInfo(PRIMARY_USER)
+ trustRepository.setCurrentUserTrustManaged(false)
+ biometricSettingsRepository.setFaceEnrolled(false)
+ biometricSettingsRepository.setFingerprintEnrolled(false)
+
+ verifyMessagesForAuthFlag(
+ STRONG_AUTH_NOT_REQUIRED to null,
+ STRONG_AUTH_REQUIRED_AFTER_BOOT to null,
+ SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null,
+ STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null,
+ STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to null,
+ STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to null,
+ STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to null,
+ SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to null,
+ STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to null,
+ STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
+ Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock),
+ )
+ }
+
+ @Test
+ fun authFlagsChanges_withTrustManaged_providesDifferentMessages() =
+ testScope.runTest {
+ userRepository.setSelectedUserInfo(PRIMARY_USER)
+ biometricSettingsRepository.setFaceEnrolled(false)
+ biometricSettingsRepository.setFingerprintEnrolled(false)
+
+ trustRepository.setCurrentUserTrustManaged(true)
+
+ verifyMessagesForAuthFlag(
+ STRONG_AUTH_NOT_REQUIRED to null,
+ STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null,
+ STRONG_AUTH_REQUIRED_AFTER_BOOT to
+ Pair(keyguard_enter_pin, kg_prompt_reason_restart_pin),
+ STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
+ Pair(keyguard_enter_pin, kg_prompt_pin_auth_timeout),
+ STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
+ Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock),
+ SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to
+ Pair(keyguard_enter_pin, kg_trust_agent_disabled),
+ SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to
+ Pair(keyguard_enter_pin, kg_trust_agent_disabled),
+ STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
+ Pair(keyguard_enter_pin, kg_prompt_after_user_lockdown_pin),
+ STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
+ Pair(keyguard_enter_pin, kg_prompt_unattended_update),
+ STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
+ Pair(keyguard_enter_pin, kg_prompt_auth_timeout),
+ )
+ }
+
+ @Test
+ fun authFlagsChanges_withFaceEnrolled_providesDifferentMessages() =
+ testScope.runTest {
+ userRepository.setSelectedUserInfo(PRIMARY_USER)
+ trustRepository.setCurrentUserTrustManaged(false)
+ biometricSettingsRepository.setFingerprintEnrolled(false)
+
+ biometricSettingsRepository.setIsFaceAuthEnabled(true)
+ biometricSettingsRepository.setFaceEnrolled(true)
+
+ verifyMessagesForAuthFlag(
+ STRONG_AUTH_NOT_REQUIRED to null,
+ STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null,
+ SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null,
+ SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to null,
+ STRONG_AUTH_REQUIRED_AFTER_BOOT to
+ Pair(keyguard_enter_pin, kg_prompt_reason_restart_pin),
+ STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
+ Pair(keyguard_enter_pin, kg_prompt_pin_auth_timeout),
+ STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
+ Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock),
+ STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
+ Pair(keyguard_enter_pin, kg_prompt_after_user_lockdown_pin),
+ STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
+ Pair(keyguard_enter_pin, kg_prompt_unattended_update),
+ STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
+ Pair(keyguard_enter_pin, kg_prompt_auth_timeout),
+ )
+ }
+
+ @Test
+ fun authFlagsChanges_withFingerprintEnrolled_providesDifferentMessages() =
+ testScope.runTest {
+ userRepository.setSelectedUserInfo(PRIMARY_USER)
+ trustRepository.setCurrentUserTrustManaged(false)
+ biometricSettingsRepository.setIsFaceAuthEnabled(false)
+ biometricSettingsRepository.setFaceEnrolled(false)
+
+ biometricSettingsRepository.setFingerprintEnrolled(true)
+ biometricSettingsRepository.setFingerprintEnabledByDevicePolicy(true)
+
+ verifyMessagesForAuthFlag(
+ STRONG_AUTH_NOT_REQUIRED to null,
+ STRONG_AUTH_REQUIRED_AFTER_LOCKOUT to null,
+ SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to null,
+ SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to null,
+ STRONG_AUTH_REQUIRED_AFTER_BOOT to
+ Pair(keyguard_enter_pin, kg_prompt_reason_restart_pin),
+ STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
+ Pair(keyguard_enter_pin, kg_prompt_pin_auth_timeout),
+ STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
+ Pair(keyguard_enter_pin, kg_prompt_after_dpm_lock),
+ STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
+ Pair(keyguard_enter_pin, kg_prompt_after_user_lockdown_pin),
+ STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
+ Pair(keyguard_enter_pin, kg_prompt_unattended_update),
+ STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
+ Pair(keyguard_enter_pin, kg_prompt_auth_timeout),
+ )
+ }
+
+ private fun TestScope.verifyMessagesForAuthFlag(
+ vararg authFlagToExpectedMessages: Pair<Int, Pair<Int, Int>?>
+ ) {
+ val authFlagsMessage = collectLastValue(underTest.authFlagsMessage)
+
+ authFlagToExpectedMessages.forEach { (flag, messagePair) ->
+ biometricSettingsRepository.setAuthenticationFlags(
+ AuthenticationFlags(PRIMARY_USER_ID, flag)
+ )
+
+ assertThat(authFlagsMessage())
+ .isEqualTo(messagePair?.let { message(it.first, it.second) })
+ }
+ }
+
+ private fun message(primaryResId: Int, secondaryResId: Int): BouncerMessageModel {
+ return BouncerMessageModel(
+ message = Message(messageResId = primaryResId),
+ secondaryMessage = Message(messageResId = secondaryResId)
+ )
+ }
+ private fun message(value: String): BouncerMessageModel {
+ return BouncerMessageModel(message = Message(message = value))
+ }
+
+ companion object {
+ private const val PRIMARY_USER_ID = 0
+ private val PRIMARY_USER =
+ UserInfo(
+ /* id= */ PRIMARY_USER_ID,
+ /* name= */ "primary user",
+ /* flags= */ UserInfo.FLAG_PRIMARY
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
new file mode 100644
index 000000000000..b0af3104a3ae
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2023 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.keyguard.bouncer.domain.interactor
+
+import android.content.pm.UserInfo
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.R.string.keyguard_enter_pin
+import com.android.systemui.R.string.kg_too_many_failed_attempts_countdown
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.FlowValue
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.bouncer.data.factory.BouncerMessageFactory
+import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
+import com.android.systemui.keyguard.bouncer.shared.model.Message
+import com.android.systemui.keyguard.data.repository.FakeBouncerMessageRepository
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.util.mockito.KotlinArgumentCaptor
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidJUnit4::class)
+class BouncerMessageInteractorTest : SysuiTestCase() {
+
+ @Mock private lateinit var securityModel: KeyguardSecurityModel
+ @Mock private lateinit var updateMonitor: KeyguardUpdateMonitor
+ @Mock private lateinit var countDownTimerUtil: CountDownTimerUtil
+ private lateinit var countDownTimerCallback: KotlinArgumentCaptor<CountDownTimerCallback>
+ private lateinit var underTest: BouncerMessageInteractor
+ private lateinit var repository: FakeBouncerMessageRepository
+ private lateinit var userRepository: FakeUserRepository
+ private lateinit var testScope: TestScope
+ private lateinit var bouncerMessage: FlowValue<BouncerMessageModel?>
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ repository = FakeBouncerMessageRepository()
+ userRepository = FakeUserRepository()
+ userRepository.setUserInfos(listOf(PRIMARY_USER))
+ testScope = TestScope()
+ countDownTimerCallback = KotlinArgumentCaptor(CountDownTimerCallback::class.java)
+
+ allowTestableLooperAsMainThread()
+ whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN)
+ whenever(updateMonitor.isFingerprintAllowedInBouncer).thenReturn(false)
+ }
+
+ suspend fun TestScope.init() {
+ userRepository.setSelectedUserInfo(PRIMARY_USER)
+ val featureFlags = FakeFeatureFlags()
+ featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true)
+ underTest =
+ BouncerMessageInteractor(
+ repository = repository,
+ factory = BouncerMessageFactory(updateMonitor, securityModel),
+ userRepository = userRepository,
+ countDownTimerUtil = countDownTimerUtil,
+ featureFlags = featureFlags
+ )
+ bouncerMessage = collectLastValue(underTest.bouncerMessage)
+ }
+
+ @Test
+ fun onIncorrectSecurityInput_setsTheBouncerModelInTheRepository() =
+ testScope.runTest {
+ init()
+ underTest.onPrimaryAuthIncorrectAttempt()
+
+ assertThat(repository.primaryAuthMessage).isNotNull()
+ assertThat(
+ context.resources.getString(
+ repository.primaryAuthMessage.value!!.message!!.messageResId!!
+ )
+ )
+ .isEqualTo("Wrong PIN. Try again.")
+ }
+
+ @Test
+ fun onUserStartsPrimaryAuthInput_clearsAllSetBouncerMessages() =
+ testScope.runTest {
+ init()
+ repository.setCustomMessage(message("not empty"))
+ repository.setFaceAcquisitionMessage(message("not empty"))
+ repository.setFingerprintAcquisitionMessage(message("not empty"))
+ repository.setPrimaryAuthMessage(message("not empty"))
+
+ underTest.onPrimaryBouncerUserInput()
+
+ assertThat(repository.customMessage.value).isNull()
+ assertThat(repository.faceAcquisitionMessage.value).isNull()
+ assertThat(repository.fingerprintAcquisitionMessage.value).isNull()
+ assertThat(repository.primaryAuthMessage.value).isNull()
+ }
+
+ @Test
+ fun onBouncerBeingHidden_clearsAllSetBouncerMessages() =
+ testScope.runTest {
+ init()
+ repository.setCustomMessage(message("not empty"))
+ repository.setFaceAcquisitionMessage(message("not empty"))
+ repository.setFingerprintAcquisitionMessage(message("not empty"))
+ repository.setPrimaryAuthMessage(message("not empty"))
+
+ underTest.onBouncerBeingHidden()
+
+ assertThat(repository.customMessage.value).isNull()
+ assertThat(repository.faceAcquisitionMessage.value).isNull()
+ assertThat(repository.fingerprintAcquisitionMessage.value).isNull()
+ assertThat(repository.primaryAuthMessage.value).isNull()
+ }
+
+ @Test
+ fun setCustomMessage_setsRepositoryValue() =
+ testScope.runTest {
+ init()
+
+ underTest.setCustomMessage("not empty")
+
+ assertThat(repository.customMessage.value)
+ .isEqualTo(BouncerMessageModel(secondaryMessage = Message(message = "not empty")))
+
+ underTest.setCustomMessage(null)
+ assertThat(repository.customMessage.value).isNull()
+ }
+
+ @Test
+ fun setFaceMessage_setsRepositoryValue() =
+ testScope.runTest {
+ init()
+
+ underTest.setFaceAcquisitionMessage("not empty")
+
+ assertThat(repository.faceAcquisitionMessage.value)
+ .isEqualTo(BouncerMessageModel(secondaryMessage = Message(message = "not empty")))
+
+ underTest.setFaceAcquisitionMessage(null)
+ assertThat(repository.faceAcquisitionMessage.value).isNull()
+ }
+
+ @Test
+ fun setFingerprintMessage_setsRepositoryValue() =
+ testScope.runTest {
+ init()
+
+ underTest.setFingerprintAcquisitionMessage("not empty")
+
+ assertThat(repository.fingerprintAcquisitionMessage.value)
+ .isEqualTo(BouncerMessageModel(secondaryMessage = Message(message = "not empty")))
+
+ underTest.setFingerprintAcquisitionMessage(null)
+ assertThat(repository.fingerprintAcquisitionMessage.value).isNull()
+ }
+
+ @Test
+ fun onPrimaryAuthLockout_startsTimerForSpecifiedNumberOfSeconds() =
+ testScope.runTest {
+ init()
+
+ underTest.onPrimaryAuthLockedOut(3)
+
+ verify(countDownTimerUtil)
+ .startNewTimer(eq(3000L), eq(1000L), countDownTimerCallback.capture())
+
+ countDownTimerCallback.value.onTick(2000L)
+
+ val primaryMessage = repository.primaryAuthMessage.value!!.message!!
+ assertThat(primaryMessage.messageResId!!)
+ .isEqualTo(kg_too_many_failed_attempts_countdown)
+ assertThat(primaryMessage.formatterArgs).isEqualTo(mapOf(Pair("count", 2)))
+ }
+
+ @Test
+ fun onPrimaryAuthLockout_timerComplete_resetsRepositoryMessages() =
+ testScope.runTest {
+ init()
+ repository.setCustomMessage(message("not empty"))
+ repository.setFaceAcquisitionMessage(message("not empty"))
+ repository.setFingerprintAcquisitionMessage(message("not empty"))
+ repository.setPrimaryAuthMessage(message("not empty"))
+
+ underTest.onPrimaryAuthLockedOut(3)
+
+ verify(countDownTimerUtil)
+ .startNewTimer(eq(3000L), eq(1000L), countDownTimerCallback.capture())
+
+ countDownTimerCallback.value.onFinish()
+
+ assertThat(repository.customMessage.value).isNull()
+ assertThat(repository.faceAcquisitionMessage.value).isNull()
+ assertThat(repository.fingerprintAcquisitionMessage.value).isNull()
+ assertThat(repository.primaryAuthMessage.value).isNull()
+ }
+
+ @Test
+ fun bouncerMessage_hasPriorityOrderOfMessages() =
+ testScope.runTest {
+ init()
+ repository.setBiometricAuthMessage(message("biometric message"))
+ repository.setFaceAcquisitionMessage(message("face acquisition message"))
+ repository.setFingerprintAcquisitionMessage(message("fingerprint acquisition message"))
+ repository.setPrimaryAuthMessage(message("primary auth message"))
+ repository.setAuthFlagsMessage(message("auth flags message"))
+ repository.setBiometricLockedOutMessage(message("biometrics locked out"))
+ repository.setCustomMessage(message("custom message"))
+
+ assertThat(bouncerMessage()).isEqualTo(message("primary auth message"))
+
+ repository.setPrimaryAuthMessage(null)
+
+ assertThat(bouncerMessage()).isEqualTo(message("biometric message"))
+
+ repository.setBiometricAuthMessage(null)
+
+ assertThat(bouncerMessage()).isEqualTo(message("fingerprint acquisition message"))
+
+ repository.setFingerprintAcquisitionMessage(null)
+
+ assertThat(bouncerMessage()).isEqualTo(message("face acquisition message"))
+
+ repository.setFaceAcquisitionMessage(null)
+
+ assertThat(bouncerMessage()).isEqualTo(message("custom message"))
+
+ repository.setCustomMessage(null)
+
+ assertThat(bouncerMessage()).isEqualTo(message("auth flags message"))
+
+ repository.setAuthFlagsMessage(null)
+
+ assertThat(bouncerMessage()).isEqualTo(message("biometrics locked out"))
+
+ repository.setBiometricLockedOutMessage(null)
+
+ // sets the default message if everything else is null
+ assertThat(bouncerMessage()!!.message!!.messageResId).isEqualTo(keyguard_enter_pin)
+ }
+
+ private fun message(value: String): BouncerMessageModel {
+ return BouncerMessageModel(message = Message(message = value))
+ }
+
+ companion object {
+ private const val PRIMARY_USER_ID = 0
+ private val PRIMARY_USER =
+ UserInfo(
+ /* id= */ PRIMARY_USER_ID,
+ /* name= */ "primary user",
+ /* flags= */ UserInfo.FLAG_PRIMARY
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBouncerMessageRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBouncerMessageRepository.kt
new file mode 100644
index 000000000000..b03b4ba3687d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeBouncerMessageRepository.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2023 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.keyguard.data.repository
+
+import com.android.systemui.keyguard.bouncer.data.repository.BouncerMessageRepository
+import com.android.systemui.keyguard.bouncer.shared.model.BouncerMessageModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+class FakeBouncerMessageRepository : BouncerMessageRepository {
+ private val _primaryAuthMessage = MutableStateFlow<BouncerMessageModel?>(null)
+ override val primaryAuthMessage: StateFlow<BouncerMessageModel?>
+ get() = _primaryAuthMessage
+
+ private val _faceAcquisitionMessage = MutableStateFlow<BouncerMessageModel?>(null)
+ override val faceAcquisitionMessage: StateFlow<BouncerMessageModel?>
+ get() = _faceAcquisitionMessage
+ private val _fingerprintAcquisitionMessage = MutableStateFlow<BouncerMessageModel?>(null)
+ override val fingerprintAcquisitionMessage: StateFlow<BouncerMessageModel?>
+ get() = _fingerprintAcquisitionMessage
+ private val _customMessage = MutableStateFlow<BouncerMessageModel?>(null)
+ override val customMessage: StateFlow<BouncerMessageModel?>
+ get() = _customMessage
+ private val _biometricAuthMessage = MutableStateFlow<BouncerMessageModel?>(null)
+ override val biometricAuthMessage: StateFlow<BouncerMessageModel?>
+ get() = _biometricAuthMessage
+ private val _authFlagsMessage = MutableStateFlow<BouncerMessageModel?>(null)
+ override val authFlagsMessage: StateFlow<BouncerMessageModel?>
+ get() = _authFlagsMessage
+
+ private val _biometricLockedOutMessage = MutableStateFlow<BouncerMessageModel?>(null)
+ override val biometricLockedOutMessage: Flow<BouncerMessageModel?>
+ get() = _biometricLockedOutMessage
+
+ override fun setPrimaryAuthMessage(value: BouncerMessageModel?) {
+ _primaryAuthMessage.value = value
+ }
+
+ override fun setFaceAcquisitionMessage(value: BouncerMessageModel?) {
+ _faceAcquisitionMessage.value = value
+ }
+
+ override fun setFingerprintAcquisitionMessage(value: BouncerMessageModel?) {
+ _fingerprintAcquisitionMessage.value = value
+ }
+
+ override fun setCustomMessage(value: BouncerMessageModel?) {
+ _customMessage.value = value
+ }
+
+ fun setBiometricAuthMessage(value: BouncerMessageModel?) {
+ _biometricAuthMessage.value = value
+ }
+
+ fun setAuthFlagsMessage(value: BouncerMessageModel?) {
+ _authFlagsMessage.value = value
+ }
+
+ fun setBiometricLockedOutMessage(value: BouncerMessageModel?) {
+ _biometricLockedOutMessage.value = value
+ }
+
+ override fun clearMessage() {
+ _primaryAuthMessage.value = null
+ _faceAcquisitionMessage.value = null
+ _fingerprintAcquisitionMessage.value = null
+ _customMessage.value = null
+ }
+}