diff options
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 + } +} |