Add BouncerMessageRepository and BouncerMessageInteractor

Other changes:
 - Rename BouncerLog to BouncerTableLog
 - Make BouncerLog a normal LogBuffer and use it for logging bouncer state.

Test: atest BouncerMessageRepositoryTest
Test: atest BouncerMessageInteractorTest
Bug: 275600559
Change-Id: Ifb505f6dc7ecd3b9385d2c3d58aa5011b6902abf
This commit is contained in:
Chandru S
2023-05-08 07:10:32 -07:00
parent ed1479cc15
commit d421942980
13 changed files with 1366 additions and 14 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -393,6 +393,17 @@ public class LogModule {
return factory.create("DeviceEntryFaceAuthRepositoryLog", 300);
}
/**
* 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.
*/
@@ -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. */

View File

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

View File

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

View File

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