diff options
35 files changed, 2201 insertions, 1469 deletions
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 458ca2b17c6d..f9523370adb1 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -79,17 +79,25 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteracto import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.scene.domain.interactor.SceneInteractor; +import com.android.systemui.scene.shared.model.SceneContainerNames; +import com.android.systemui.scene.shared.model.SceneKey; import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.UserSwitcherController; +import com.android.systemui.user.domain.interactor.UserInteractor; import com.android.systemui.util.ViewController; +import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.settings.GlobalSettings; import java.io.File; import java.util.Optional; import javax.inject.Inject; +import javax.inject.Provider; + +import kotlinx.coroutines.Job; /** Controller for {@link KeyguardSecurityContainer} */ @KeyguardBouncerScope @@ -378,6 +386,10 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard showPrimarySecurityScreen(false); } }; + private final UserInteractor mUserInteractor; + private final Provider<SceneInteractor> mSceneInteractor; + private final Provider<JavaAdapter> mJavaAdapter; + @Nullable private Job mSceneTransitionCollectionJob; @Inject public KeyguardSecurityContainerController(KeyguardSecurityContainer view, @@ -402,7 +414,10 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard ViewMediatorCallback viewMediatorCallback, AudioManager audioManager, KeyguardFaceAuthInteractor keyguardFaceAuthInteractor, - BouncerMessageInteractor bouncerMessageInteractor + BouncerMessageInteractor bouncerMessageInteractor, + Provider<JavaAdapter> javaAdapter, + UserInteractor userInteractor, + Provider<SceneInteractor> sceneInteractor ) { super(view); mLockPatternUtils = lockPatternUtils; @@ -429,6 +444,9 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard mAudioManager = audioManager; mKeyguardFaceAuthInteractor = keyguardFaceAuthInteractor; mBouncerMessageInteractor = bouncerMessageInteractor; + mUserInteractor = userInteractor; + mSceneInteractor = sceneInteractor; + mJavaAdapter = javaAdapter; } @Override @@ -451,6 +469,24 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard mView.setOnKeyListener(mOnKeyListener); showPrimarySecurityScreen(false); + + if (mFeatureFlags.isEnabled(Flags.SCENE_CONTAINER)) { + // When the scene framework transitions from bouncer to gone, we dismiss the keyguard. + mSceneTransitionCollectionJob = mJavaAdapter.get().alwaysCollectFlow( + mSceneInteractor.get().sceneTransitions(SceneContainerNames.SYSTEM_UI_DEFAULT), + sceneTransitionModel -> { + if (sceneTransitionModel != null + && sceneTransitionModel.getFrom() == SceneKey.Bouncer.INSTANCE + && sceneTransitionModel.getTo() == SceneKey.Gone.INSTANCE) { + final int selectedUserId = mUserInteractor.getSelectedUserId(); + showNextSecurityScreenOrFinish( + /* authenticated= */ true, + selectedUserId, + /* bypassSecondaryLockScreen= */ true, + mSecurityModel.getSecurityMode(selectedUserId)); + } + }); + } } @Override @@ -459,6 +495,11 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard mConfigurationController.removeCallback(mConfigurationListener); mView.removeMotionEventListener(mGlobalTouchListener); mUserSwitcherController.removeUserSwitchCallback(mUserSwitchCallback); + + if (mSceneTransitionCollectionJob != null) { + mSceneTransitionCollectionJob.cancel(null); + mSceneTransitionCollectionJob = null; + } } /** */ diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt index 0b251840ce4e..43fb3630bd19 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt @@ -14,25 +14,39 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.authentication.data.repository +import com.android.internal.widget.LockPatternChecker import com.android.internal.widget.LockPatternUtils +import com.android.internal.widget.LockscreenCredential import com.android.keyguard.KeyguardSecurityModel import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.authentication.shared.model.AuthenticationResultModel +import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.util.time.SystemClock import dagger.Binds import dagger.Module import java.util.function.Function import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext @@ -57,11 +71,17 @@ interface AuthenticationRepository { */ val isBypassEnabled: StateFlow<Boolean> - /** - * Number of consecutively failed authentication attempts. This resets to `0` when - * authentication succeeds. - */ - val failedAuthenticationAttempts: StateFlow<Int> + /** Whether the auto confirm feature is enabled for the currently-selected user. */ + val isAutoConfirmEnabled: StateFlow<Boolean> + + /** The length of the PIN for which we should show a hint. */ + val hintedPinLength: Int + + /** Whether the pattern should be visible for the currently-selected user. */ + val isPatternVisible: StateFlow<Boolean> + + /** The current throttling state, as cached via [setThrottling]. */ + val throttling: StateFlow<AuthenticationThrottlingModel> /** * Returns the currently-configured authentication method. This determines how the @@ -69,11 +89,48 @@ interface AuthenticationRepository { */ suspend fun getAuthenticationMethod(): AuthenticationMethodModel + /** Returns the length of the PIN or `0` if the current auth method is not PIN. */ + suspend fun getPinLength(): Int + + /** + * Returns whether the lockscreen is enabled. + * + * When the lockscreen is not enabled, it shouldn't show in cases when the authentication method + * is considered not secure (for example, "swipe" is considered to be "none"). + */ + suspend fun isLockscreenEnabled(): Boolean + /** See [isBypassEnabled]. */ fun setBypassEnabled(isBypassEnabled: Boolean) - /** See [failedAuthenticationAttempts]. */ - fun setFailedAuthenticationAttempts(failedAuthenticationAttempts: Int) + /** Reports an authentication attempt. */ + suspend fun reportAuthenticationAttempt(isSuccessful: Boolean) + + /** Returns the current number of failed authentication attempts. */ + suspend fun getFailedAuthenticationAttemptCount(): Int + + /** + * Returns the timestamp for when the current throttling will end, allowing the user to attempt + * authentication again. + * + * Note that this is in milliseconds and it matches [SystemClock.elapsedRealtime]. + */ + suspend fun getThrottlingEndTimestamp(): Long + + /** Sets the cached throttling state, updating the [throttling] flow. */ + fun setThrottling(throttlingModel: AuthenticationThrottlingModel) + + /** + * Sets the throttling timeout duration (time during which the user should not be allowed to + * attempt authentication). + */ + suspend fun setThrottleDuration(durationMs: Int) + + /** + * Checks the given [LockscreenCredential] to see if it's correct, returning an + * [AuthenticationResultModel] representing what happened. + */ + suspend fun checkCredential(credential: LockscreenCredential): AuthenticationResultModel } class AuthenticationRepositoryImpl @@ -83,8 +140,8 @@ constructor( private val getSecurityMode: Function<Int, KeyguardSecurityModel.SecurityMode>, @Background private val backgroundDispatcher: CoroutineDispatcher, private val userRepository: UserRepository, - private val lockPatternUtils: LockPatternUtils, keyguardRepository: KeyguardRepository, + private val lockPatternUtils: LockPatternUtils, ) : AuthenticationRepository { override val isUnlocked: StateFlow<Boolean> = @@ -94,54 +151,140 @@ constructor( initialValue = false, ) + override suspend fun isLockscreenEnabled(): Boolean { + return withContext(backgroundDispatcher) { + val selectedUserId = userRepository.selectedUserId + !lockPatternUtils.isLockPatternEnabled(selectedUserId) + } + } + private val _isBypassEnabled = MutableStateFlow(false) override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled.asStateFlow() - private val _failedAuthenticationAttempts = MutableStateFlow(0) - override val failedAuthenticationAttempts: StateFlow<Int> = - _failedAuthenticationAttempts.asStateFlow() + override val isAutoConfirmEnabled: StateFlow<Boolean> = + userRepository.selectedUserInfo + .map { it.id } + .flatMapLatest { userId -> + flow { emit(lockPatternUtils.isAutoPinConfirmEnabled(userId)) } + .flowOn(backgroundDispatcher) + } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = true, + ) + + override val hintedPinLength: Int = LockPatternUtils.MIN_AUTO_PIN_REQUIREMENT_LENGTH + + override val isPatternVisible: StateFlow<Boolean> = + userRepository.selectedUserInfo + .map { it.id } + .flatMapLatest { userId -> + flow { emit(lockPatternUtils.isVisiblePatternEnabled(userId)) } + .flowOn(backgroundDispatcher) + } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = true, + ) + + private val _throttling = MutableStateFlow(AuthenticationThrottlingModel()) + override val throttling: StateFlow<AuthenticationThrottlingModel> = _throttling.asStateFlow() + + private val UserRepository.selectedUserId: Int + get() = getSelectedUserInfo().id override suspend fun getAuthenticationMethod(): AuthenticationMethodModel { return withContext(backgroundDispatcher) { - val selectedUserId = userRepository.getSelectedUserInfo().id + val selectedUserId = userRepository.selectedUserId when (getSecurityMode.apply(selectedUserId)) { KeyguardSecurityModel.SecurityMode.PIN, - KeyguardSecurityModel.SecurityMode.SimPin -> - AuthenticationMethodModel.Pin( - code = listOf(1, 2, 3, 4), // TODO(b/280883900): remove this - autoConfirm = lockPatternUtils.isAutoPinConfirmEnabled(selectedUserId), - ) - KeyguardSecurityModel.SecurityMode.Password, - KeyguardSecurityModel.SecurityMode.SimPuk -> - AuthenticationMethodModel.Password( - password = "password", // TODO(b/280883900): remove this - ) - KeyguardSecurityModel.SecurityMode.Pattern -> - AuthenticationMethodModel.Pattern( - coordinates = - listOf( - AuthenticationMethodModel.Pattern.PatternCoordinate(2, 0), - AuthenticationMethodModel.Pattern.PatternCoordinate(2, 1), - AuthenticationMethodModel.Pattern.PatternCoordinate(2, 2), - AuthenticationMethodModel.Pattern.PatternCoordinate(1, 1), - AuthenticationMethodModel.Pattern.PatternCoordinate(0, 0), - AuthenticationMethodModel.Pattern.PatternCoordinate(0, 1), - AuthenticationMethodModel.Pattern.PatternCoordinate(0, 2), - ), // TODO(b/280883900): remove this - ) + KeyguardSecurityModel.SecurityMode.SimPin, + KeyguardSecurityModel.SecurityMode.SimPuk -> AuthenticationMethodModel.Pin + KeyguardSecurityModel.SecurityMode.Password -> AuthenticationMethodModel.Password + KeyguardSecurityModel.SecurityMode.Pattern -> AuthenticationMethodModel.Pattern KeyguardSecurityModel.SecurityMode.None -> AuthenticationMethodModel.None KeyguardSecurityModel.SecurityMode.Invalid -> error("Invalid security mode!") - null -> error("Invalid security is null!") } } } + override suspend fun getPinLength(): Int { + return withContext(backgroundDispatcher) { + val selectedUserId = userRepository.selectedUserId + lockPatternUtils.getPinLength(selectedUserId) + } + } + override fun setBypassEnabled(isBypassEnabled: Boolean) { _isBypassEnabled.value = isBypassEnabled } - override fun setFailedAuthenticationAttempts(failedAuthenticationAttempts: Int) { - _failedAuthenticationAttempts.value = failedAuthenticationAttempts + override suspend fun reportAuthenticationAttempt(isSuccessful: Boolean) { + val selectedUserId = userRepository.selectedUserId + withContext(backgroundDispatcher) { + if (isSuccessful) { + lockPatternUtils.reportSuccessfulPasswordAttempt(selectedUserId) + } else { + lockPatternUtils.reportFailedPasswordAttempt(selectedUserId) + } + } + } + + override suspend fun getFailedAuthenticationAttemptCount(): Int { + return withContext(backgroundDispatcher) { + val selectedUserId = userRepository.selectedUserId + lockPatternUtils.getCurrentFailedPasswordAttempts(selectedUserId) + } + } + + override suspend fun getThrottlingEndTimestamp(): Long { + return withContext(backgroundDispatcher) { + val selectedUserId = userRepository.selectedUserId + lockPatternUtils.getLockoutAttemptDeadline(selectedUserId) + } + } + + override fun setThrottling(throttlingModel: AuthenticationThrottlingModel) { + _throttling.value = throttlingModel + } + + override suspend fun setThrottleDuration(durationMs: Int) { + withContext(backgroundDispatcher) { + lockPatternUtils.setLockoutAttemptDeadline( + userRepository.selectedUserId, + durationMs, + ) + } + } + + override suspend fun checkCredential( + credential: LockscreenCredential + ): AuthenticationResultModel { + return suspendCoroutine { continuation -> + LockPatternChecker.checkCredential( + lockPatternUtils, + credential, + userRepository.selectedUserId, + object : LockPatternChecker.OnCheckCallback { + override fun onChecked(matched: Boolean, throttleTimeoutMs: Int) { + continuation.resume( + AuthenticationResultModel( + isSuccessful = matched, + throttleDurationMs = throttleTimeoutMs, + ) + ) + } + + override fun onCancelled() { + continuation.resume(AuthenticationResultModel(isSuccessful = false)) + } + + override fun onEarlyMatched() = Unit + } + ) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt index 15e579d6d72e..82674bff6433 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt @@ -16,25 +16,43 @@ package com.android.systemui.authentication.domain.interactor -import android.app.admin.DevicePolicyManager +import com.android.internal.widget.LockPatternView +import com.android.internal.widget.LockscreenCredential import com.android.systemui.authentication.data.repository.AuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.util.time.SystemClock import javax.inject.Inject +import kotlin.math.max +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** Hosts application business logic related to authentication. */ @SysUISingleton class AuthenticationInteractor @Inject constructor( - @Application applicationScope: CoroutineScope, + @Application private val applicationScope: CoroutineScope, private val repository: AuthenticationRepository, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val userRepository: UserRepository, + private val clock: SystemClock, ) { /** * Whether the device is unlocked. @@ -67,18 +85,66 @@ constructor( */ val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled + /** The current authentication throttling state, only meaningful if [isThrottled] is `true`. */ + val throttling: StateFlow<AuthenticationThrottlingModel> = repository.throttling + /** - * Number of consecutively failed authentication attempts. This resets to `0` when - * authentication succeeds. + * Whether currently throttled and the user has to wait before being able to try another + * authentication attempt. */ - val failedAuthenticationAttempts: StateFlow<Int> = repository.failedAuthenticationAttempts + val isThrottled: StateFlow<Boolean> = + throttling + .map { it.remainingMs > 0 } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = throttling.value.remainingMs > 0, + ) + + /** The length of the hinted PIN, or `null` if pin length hint should not be shown. */ + val hintedPinLength: StateFlow<Int?> = + flow { emit(repository.getPinLength()) } + .map { currentPinLength -> + // Hinting is enabled for 6-digit codes only + currentPinLength.takeIf { repository.hintedPinLength == it } + } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = null, + ) + + /** Whether the auto confirm feature is enabled for the currently-selected user. */ + val isAutoConfirmEnabled: StateFlow<Boolean> = repository.isAutoConfirmEnabled + + /** Whether the pattern should be visible for the currently-selected user. */ + val isPatternVisible: StateFlow<Boolean> = repository.isPatternVisible + + private var throttlingCountdownJob: Job? = null + + init { + applicationScope.launch { + userRepository.selectedUserInfo + .map { it.id } + .distinctUntilChanged() + .collect { onSelectedUserChanged() } + } + } /** * Returns the currently-configured authentication method. This determines how the * authentication challenge is completed in order to unlock an otherwise locked device. */ suspend fun getAuthenticationMethod(): AuthenticationMethodModel { - return repository.getAuthenticationMethod() + val authMethod = repository.getAuthenticationMethod() + return if ( + authMethod is AuthenticationMethodModel.None && repository.isLockscreenEnabled() + ) { + // We treat "None" as "Swipe" when the lockscreen is enabled. + AuthenticationMethodModel.Swipe + } else { + authMethod + } } /** @@ -104,39 +170,52 @@ constructor( * authentication failed, `null` if the check was not performed. */ suspend fun authenticate(input: List<Any>, tryAutoConfirm: Boolean = false): Boolean? { - val authMethod = getAuthenticationMethod() - if (tryAutoConfirm) { - if ((authMethod as? AuthenticationMethodModel.Pin)?.autoConfirm != true) { - // Do not attempt to authenticate unless the PIN lock is set to auto-confirm. - return null - } + if (input.isEmpty()) { + throw IllegalArgumentException("Input was empty!") + } - if (input.size < authMethod.code.size) { - // Do not attempt to authenticate if the PIN has not yet the required amount of - // digits. This intentionally only skip for shorter PINs; if the PIN is longer, the - // layer above might have throttled this check, and the PIN should be rejected via - // the auth code below. - return null + val skipCheck = + when { + // We're being throttled, the UI layer should not have called this; skip the + // attempt. + isThrottled.value -> true + // Auto-confirm attempt when the feature is not enabled; skip the attempt. + tryAutoConfirm && !isAutoConfirmEnabled.value -> true + // Auto-confirm should skip the attempt if the pin entered is too short. + tryAutoConfirm && input.size < repository.getPinLength() -> true + else -> false } + if (skipCheck) { + return null } - val isSuccessful = - when (authMethod) { - is AuthenticationMethodModel.Pin -> input.asCode() == authMethod.code - is AuthenticationMethodModel.Password -> input.asPassword() == authMethod.password - is AuthenticationMethodModel.Pattern -> input.asPattern() == authMethod.coordinates - else -> true - } + // Attempt to authenticate: + val authMethod = getAuthenticationMethod() + val credential = authMethod.createCredential(input) ?: return null + val authenticationResult = repository.checkCredential(credential) + credential.zeroize() - if (isSuccessful) { - repository.setFailedAuthenticationAttempts(0) - } else { - repository.setFailedAuthenticationAttempts( - repository.failedAuthenticationAttempts.value + 1 + if (authenticationResult.isSuccessful || !tryAutoConfirm) { + repository.reportAuthenticationAttempt( + isSuccessful = authenticationResult.isSuccessful, + ) + } + + // Check if we need to throttle and, if so, kick off the throttle countdown: + if (!authenticationResult.isSuccessful && authenticationResult.throttleDurationMs > 0) { + repository.setThrottleDuration( + durationMs = authenticationResult.throttleDurationMs, ) + startThrottlingCountdown() } - return isSuccessful + if (authenticationResult.isSuccessful) { + // Since authentication succeeded, we should refresh throttling to make sure that our + // state is completely reflecting the upstream source of truth. + refreshThrottling() + } + + return authenticationResult.isSuccessful } /** See [isBypassEnabled]. */ @@ -144,44 +223,67 @@ constructor( repository.setBypassEnabled(!repository.isBypassEnabled.value) } - companion object { - /** - * Returns a PIN code from the given list. It's assumed the given list elements are all - * [Int] in the range [0-9]. - */ - private fun List<Any>.asCode(): List<Int>? { - if (isEmpty() || size > DevicePolicyManager.MAX_PASSWORD_LENGTH) { - return null - } - - return map { - require(it is Int && it in 0..9) { - "Pin is required to be Int in range [0..9], but got $it" + /** Starts refreshing the throttling state every second. */ + private suspend fun startThrottlingCountdown() { + cancelCountdown() + throttlingCountdownJob = + applicationScope.launch { + while (refreshThrottling() > 0) { + delay(1.seconds.inWholeMilliseconds) } - it } + } + + /** Cancels any throttling state countdown started in [startThrottlingCountdown]. */ + private fun cancelCountdown() { + throttlingCountdownJob?.cancel() + throttlingCountdownJob = null + } + + /** Notifies that the currently-selected user has changed. */ + private suspend fun onSelectedUserChanged() { + cancelCountdown() + if (refreshThrottling() > 0) { + startThrottlingCountdown() } + } - /** - * Returns a password from the given list. It's assumed the given list elements are all - * [Char]. - */ - private fun List<Any>.asPassword(): String { - val anyList = this - return buildString { anyList.forEach { append(it as Char) } } + /** + * Refreshes the throttling state, hydrating the repository with the latest state. + * + * @return The remaining time for the current throttling countdown, in milliseconds or `0` if + * not being throttled. + */ + private suspend fun refreshThrottling(): Long { + return withContext(backgroundDispatcher) { + val failedAttemptCount = async { repository.getFailedAuthenticationAttemptCount() } + val deadline = async { repository.getThrottlingEndTimestamp() } + val remainingMs = max(0, deadline.await() - clock.elapsedRealtime()) + repository.setThrottling( + AuthenticationThrottlingModel( + failedAttemptCount = failedAttemptCount.await(), + remainingMs = remainingMs.toInt(), + ), + ) + remainingMs } + } - /** - * Returns a list of [AuthenticationMethodModel.Pattern.PatternCoordinate] from the given - * list. It's assumed the given list elements are all - * [AuthenticationMethodModel.Pattern.PatternCoordinate]. - */ - private fun List<Any>.asPattern(): - List<AuthenticationMethodModel.Pattern.PatternCoordinate> { - val anyList = this - return buildList { - anyList.forEach { add(it as AuthenticationMethodModel.Pattern.PatternCoordinate) } - } + private fun AuthenticationMethodModel.createCredential( + input: List<Any> + ): LockscreenCredential? { + return when (this) { + is AuthenticationMethodModel.Pin -> + LockscreenCredential.createPin(input.joinToString("")) + is AuthenticationMethodModel.Password -> + LockscreenCredential.createPassword(input.joinToString("")) + is AuthenticationMethodModel.Pattern -> + LockscreenCredential.createPattern( + input + .map { it as AuthenticationMethodModel.Pattern.PatternCoordinate } + .map { LockPatternView.Cell.of(it.y, it.x) } + ) + else -> null } } } diff --git a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt index 1016b6b9a99e..97c6697f10a5 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt @@ -16,8 +16,6 @@ package com.android.systemui.authentication.shared.model -import androidx.annotation.VisibleForTesting - /** Enumerates all known authentication methods. */ sealed class AuthenticationMethodModel( /** @@ -34,30 +32,11 @@ sealed class AuthenticationMethodModel( /** The most basic authentication method. The lock screen can be swiped away when displayed. */ object Swipe : AuthenticationMethodModel(isSecure = false) - /** - * Authentication method using a PIN. - * - * In practice, a pin is restricted to 16 decimal digits , see - * [android.app.admin.DevicePolicyManager.MAX_PASSWORD_LENGTH] - */ - data class Pin(val code: List<Int>, val autoConfirm: Boolean) : - AuthenticationMethodModel(isSecure = true) { - - /** Convenience constructor for tests only. */ - @VisibleForTesting - constructor( - code: Long, - autoConfirm: Boolean = false - ) : this(code.toString(10).map { it - '0' }, autoConfirm) {} - } - - data class Password(val password: String) : AuthenticationMethodModel(isSecure = true) + object Pin : AuthenticationMethodModel(isSecure = true) - data class Pattern( - val coordinates: List<PatternCoordinate>, - val isPatternVisible: Boolean = true, - ) : AuthenticationMethodModel(isSecure = true) { + object Password : AuthenticationMethodModel(isSecure = true) + object Pattern : AuthenticationMethodModel(isSecure = true) { data class PatternCoordinate( val x: Int, val y: Int, diff --git a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationResultModel.kt b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationResultModel.kt new file mode 100644 index 000000000000..f2a3e74700db --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationResultModel.kt @@ -0,0 +1,25 @@ +/* + * Copyright 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.authentication.shared.model + +/** Models the result of an authentication attempt. */ +data class AuthenticationResultModel( + /** Whether authentication was successful. */ + val isSuccessful: Boolean = false, + /** If [isSuccessful] is `false`, how long the user must wait before trying again. */ + val throttleDurationMs: Int = 0, +) diff --git a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationThrottlingModel.kt b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationThrottlingModel.kt new file mode 100644 index 000000000000..d0d398e31859 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationThrottlingModel.kt @@ -0,0 +1,32 @@ +/* + * Copyright 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.authentication.shared.model + +/** Models a state for throttling the next authentication attempt. */ +data class AuthenticationThrottlingModel( + + /** Number of failed authentication attempts so far. If not throttling this will be `0`. */ + val failedAttemptCount: Int = 0, + + /** + * Remaining amount of time, in milliseconds, before another authentication attempt can be done. + * If not throttling this will be `0`. + * + * This number is changed throughout the timeout. + */ + val remainingMs: Int = 0, +) diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt index ff896fa51350..943216ebd0d6 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt @@ -16,7 +16,6 @@ package com.android.systemui.bouncer.data.repository -import com.android.systemui.bouncer.shared.model.AuthenticationThrottledModel import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow @@ -30,15 +29,7 @@ class BouncerRepository @Inject constructor() { /** The user-facing message to show in the bouncer. */ val message: StateFlow<String?> = _message.asStateFlow() - private val _throttling = MutableStateFlow<AuthenticationThrottledModel?>(null) - /** The current authentication throttling state. If `null`, there's no throttling. */ - val throttling: StateFlow<AuthenticationThrottledModel?> = _throttling.asStateFlow() - fun setMessage(message: String?) { _message.value = message } - - fun setThrottling(throttling: AuthenticationThrottledModel?) { - _throttling.value = throttling - } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt index 5dd24b27d19c..2513c81c17f8 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt @@ -14,24 +14,27 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.bouncer.domain.interactor import android.content.Context -import androidx.annotation.VisibleForTesting import com.android.systemui.R import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel import com.android.systemui.bouncer.data.repository.BouncerRepository -import com.android.systemui.bouncer.shared.model.AuthenticationThrottledModel import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.util.kotlin.pairwise import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -57,9 +60,10 @@ constructor( val message: StateFlow<String?> = combine( repository.message, - repository.throttling, - ) { message, throttling -> - messageOrThrottlingMessage(message, throttling) + authenticationInteractor.isThrottled, + authenticationInteractor.throttling, + ) { message, isThrottled, throttling -> + messageOrThrottlingMessage(message, isThrottled, throttling) } .stateIn( scope = applicationScope, @@ -67,12 +71,28 @@ constructor( initialValue = messageOrThrottlingMessage( repository.message.value, - repository.throttling.value, + authenticationInteractor.isThrottled.value, + authenticationInteractor.throttling.value, ) ) - /** The current authentication throttling state. If `null`, there's no throttling. */ - val throttling: StateFlow<AuthenticationThrottledModel?> = repository.throttling + /** The current authentication throttling state, only meaningful if [isThrottled] is `true`. */ + val throttling: StateFlow<AuthenticationThrottlingModel> = authenticationInteractor.throttling + + /** + * Whether currently throttled and the user has to wait before being able to try another + * authentication attempt. + */ + val isThrottled: StateFlow<Boolean> = authenticationInteractor.isThrottled + + /** Whether the auto confirm feature is enabled for the currently-selected user. */ + val isAutoConfirmEnabled: StateFlow<Boolean> = authenticationInteractor.isAutoConfirmEnabled + + /** The length of the PIN for which we should show a hint. */ + val hintedPinLength: StateFlow<Int?> = authenticationInteractor.hintedPinLength + + /** Whether the pattern should be visible for the currently-selected user. */ + val isPatternVisible: StateFlow<Boolean> = authenticationInteractor.isPatternVisible init { // UNLOCKING SHOWS Gone. @@ -98,6 +118,15 @@ constructor( } } } + + // Clear the message if moved from throttling to no-longer throttling. + applicationScope.launch { + isThrottled.pairwise().collect { (wasThrottled, currentlyThrottled) -> + if (wasThrottled && !currentlyThrottled) { + clearMessage() + } + } + } } /** @@ -168,41 +197,16 @@ constructor( input: List<Any>, tryAutoConfirm: Boolean = false, ): Boolean? { - if (repository.throttling.value != null) { - return false - } - val isAuthenticated = authenticationInteractor.authenticate(input, tryAutoConfirm) ?: return null - val failedAttempts = authenticationInteractor.failedAuthenticationAttempts.value - when { - isAuthenticated -> { - repository.setThrottling(null) - sceneInteractor.setCurrentScene( - containerName = containerName, - scene = SceneModel(SceneKey.Gone), - ) - } - failedAttempts >= THROTTLE_AGGRESSIVELY_AFTER || failedAttempts % THROTTLE_EVERY == 0 -> - applicationScope.launch { - var remainingDurationSec = THROTTLE_DURATION_SEC - while (remainingDurationSec > 0) { - repository.setThrottling( - AuthenticationThrottledModel( - failedAttemptCount = failedAttempts, - totalDurationSec = THROTTLE_DURATION_SEC, - remainingDurationSec = remainingDurationSec, - ) - ) - remainingDurationSec-- - delay(1000) - } - - repository.setThrottling(null) - clearMessage() - } - else -> repository.setMessage(errorMessage(getAuthenticationMethod())) + if (isAuthenticated) { + sceneInteractor.setCurrentScene( + containerName = containerName, + scene = SceneModel(SceneKey.Gone), + ) + } else { + repository.setMessage(errorMessage(getAuthenticationMethod())) } return isAuthenticated @@ -233,13 +237,14 @@ constructor( private fun messageOrThrottlingMessage( message: String?, - throttling: AuthenticationThrottledModel?, + isThrottled: Boolean, + throttlingModel: AuthenticationThrottlingModel, ): String { return when { - throttling != null -> + isThrottled -> applicationContext.getString( com.android.internal.R.string.lockscreen_too_many_failed_attempts_countdown, - throttling.remainingDurationSec, + throttlingModel.remainingMs.milliseconds.inWholeSeconds, ) message != null -> message else -> "" @@ -252,10 +257,4 @@ constructor( containerName: String, ): BouncerInteractor } - - companion object { - @VisibleForTesting const val THROTTLE_DURATION_SEC = 30 - @VisibleForTesting const val THROTTLE_AGGRESSIVELY_AFTER = 15 - @VisibleForTesting const val THROTTLE_EVERY = 5 - } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/AuthenticationThrottledModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/AuthenticationThrottledModel.kt deleted file mode 100644 index cbea635f6b13..000000000000 --- a/packages/SystemUI/src/com/android/systemui/bouncer/shared/model/AuthenticationThrottledModel.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.bouncer.shared.model - -/** - * Models application state for when further authentication attempts are being throttled due to too - * many consecutive failed authentication attempts. - */ -data class AuthenticationThrottledModel( - /** Total number of failed attempts so far. */ - val failedAttemptCount: Int, - /** Total amount of time the user has to wait before attempting again. */ - val totalDurationSec: Int, - /** Remaining amount of time the user has to wait before attempting again. */ - val remainingDurationSec: Int, -) diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt index db6ca0bda0f6..5425022e11b7 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt @@ -14,19 +14,22 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.bouncer.ui.viewmodel import android.content.Context import com.android.systemui.R import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.BouncerInteractor -import com.android.systemui.bouncer.shared.model.AuthenticationThrottledModel import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.util.kotlin.pairwise import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlin.math.ceil import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -51,12 +54,12 @@ constructor( private val interactor: BouncerInteractor = interactorFactory.create(containerName) private val isInputEnabled: StateFlow<Boolean> = - interactor.throttling - .map { it == null } + interactor.isThrottled + .map { !it } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), - initialValue = interactor.throttling.value == null, + initialValue = !interactor.isThrottled.value, ) private val pin: PinBouncerViewModel by lazy { @@ -115,9 +118,9 @@ constructor( val message: StateFlow<MessageViewModel> = combine( interactor.message, - interactor.throttling, - ) { message, throttling -> - toMessageViewModel(message, throttling) + interactor.isThrottled, + ) { message, isThrottled -> + toMessageViewModel(message, isThrottled) } .stateIn( scope = applicationScope, @@ -125,7 +128,7 @@ constructor( initialValue = toMessageViewModel( message = interactor.message.value, - throttling = interactor.throttling.value, + isThrottled = interactor.isThrottled.value, ), ) @@ -143,9 +146,9 @@ constructor( init { applicationScope.launch { - interactor.throttling - .map { model -> - model?.let { + interactor.isThrottled + .map { isThrottled -> + if (isThrottled) { when (interactor.getAuthenticationMethod()) { is AuthenticationMethodModel.Pin -> R.string.kg_too_many_failed_pin_attempts_dialog_message @@ -157,10 +160,12 @@ constructor( }?.let { stringResourceId -> applicationContext.getString( stringResourceId, - model.failedAttemptCount, - model.totalDurationSec, + interactor.throttling.value.failedAttemptCount, + ceil(interactor.throttling.value.remainingMs / 1000f).toInt(), ) } + } else { + null } } .distinctUntilChanged() @@ -184,11 +189,11 @@ constructor( private fun toMessageViewModel( message: String?, - throttling: AuthenticationThrottledModel?, + isThrottled: Boolean, ): MessageViewModel { return MessageViewModel( text = message ?: "", - isUpdateAnimated = throttling == null, + isUpdateAnimated = !isThrottled, ) } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt index 5efa6f073f7a..4be539d9396d 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt @@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -70,19 +69,7 @@ class PatternBouncerViewModel( val dots: StateFlow<List<PatternDotViewModel>> = _dots.asStateFlow() /** Whether the pattern itself should be rendered visibly. */ - val isPatternVisible: StateFlow<Boolean> = - flow { - emit(null) - emit(interactor.getAuthenticationMethod()) - } - .map { authMethod -> - (authMethod as? AuthenticationMethodModel.Pattern)?.isPatternVisible ?: false - } - .stateIn( - scope = applicationScope, - started = SharingStarted.Eagerly, - initialValue = false, - ) + val isPatternVisible: StateFlow<Boolean> = interactor.isPatternVisible /** Notifies that the UI has been shown to the user. */ fun onShown() { diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt index 641e863fa303..1b14acc7fabc 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt @@ -18,13 +18,12 @@ package com.android.systemui.bouncer.ui.viewmodel import android.content.Context import com.android.keyguard.PinShapeAdapter -import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -45,26 +44,18 @@ class PinBouncerViewModel( private val mutablePinEntries = MutableStateFlow<List<EnteredKey>>(emptyList()) val pinEntries: StateFlow<List<EnteredKey>> = mutablePinEntries - /** The length of the hinted PIN, or `null` if pin length hint should not be shown. */ - val hintedPinLength: StateFlow<Int?> = - flow { emit(interactor.getAuthenticationMethod()) } - .map { authMethod -> - // Hinting is enabled for 6-digit codes only - autoConfirmPinLength(authMethod).takeIf { it == HINTING_PASSCODE_LENGTH } - } - .stateIn( - scope = applicationScope, - started = SharingStarted.Eagerly, - initialValue = null, - ) + /** The length of the PIN for which we should show a hint. */ + val hintedPinLength: StateFlow<Int?> = interactor.hintedPinLength /** Appearance of the backspace button. */ val backspaceButtonAppearance: StateFlow<ActionButtonAppearance> = - mutablePinEntries - .map { mutablePinEntries -> + combine( + mutablePinEntries, + interactor.isAutoConfirmEnabled, + ) { mutablePinEntries, isAutoConfirmEnabled -> computeBackspaceButtonAppearance( - interactor.getAuthenticationMethod(), - mutablePinEntries + enteredPin = mutablePinEntries, + isAutoConfirmEnabled = isAutoConfirmEnabled, ) } .stateIn( @@ -75,11 +66,14 @@ class PinBouncerViewModel( /** Appearance of the confirm button. */ val confirmButtonAppearance: StateFlow<ActionButtonAppearance> = - flow { - emit(null) - emit(interactor.getAuthenticationMethod()) + interactor.isAutoConfirmEnabled + .map { + if (it) { + ActionButtonAppearance.Hidden + } else { + ActionButtonAppearance.Shown + } } - .map { authMethod -> computeConfirmButtonAppearance(authMethod) } .stateIn( scope = applicationScope, started = SharingStarted.Eagerly, @@ -134,21 +128,10 @@ class PinBouncerViewModel( } } - private fun isAutoConfirmEnabled(authMethodModel: AuthenticationMethodModel?): Boolean { - return (authMethodModel as? AuthenticationMethodModel.Pin)?.autoConfirm == true - } - - private fun autoConfirmPinLength(authMethodModel: AuthenticationMethodModel?): Int? { - if (!isAutoConfirmEnabled(authMethodModel)) return null - - return (authMethodModel as? AuthenticationMethodModel.Pin)?.code?.size - } - private fun computeBackspaceButtonAppearance( - authMethodModel: AuthenticationMethodModel, - enteredPin: List<EnteredKey> + enteredPin: List<EnteredKey>, + isAutoConfirmEnabled: Boolean, ): ActionButtonAppearance { - val isAutoConfirmEnabled = isAutoConfirmEnabled(authMethodModel) val isEmpty = enteredPin.isEmpty() return when { @@ -157,15 +140,6 @@ class PinBouncerViewModel( else -> ActionButtonAppearance.Shown } } - private fun computeConfirmButtonAppearance( - authMethodModel: AuthenticationMethodModel? - ): ActionButtonAppearance { - return if (isAutoConfirmEnabled(authMethodModel)) { - ActionButtonAppearance.Hidden - } else { - ActionButtonAppearance.Shown - } - } } /** Appearance of pin-pad action buttons. */ @@ -178,9 +152,6 @@ enum class ActionButtonAppearance { Shown, } -/** Auto-confirm passcodes of exactly 6 digits show a length hint, see http://shortn/_IXlmSNbDh6 */ -private const val HINTING_PASSCODE_LENGTH = 6 - private var nextSequenceNumber = 1 /** diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt index 1ebeced5fae6..0a86d35b7b62 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt @@ -19,6 +19,7 @@ package com.android.systemui.scene.data.repository import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.SceneTransitionModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -45,6 +46,9 @@ constructor( containerConfigByName .map { (containerName, _) -> containerName to MutableStateFlow(1f) } .toMap() + private val sceneTransitionByContainerName: + Map<String, MutableStateFlow<SceneTransitionModel?>> = + containerConfigByName.keys.associateWith { MutableStateFlow(null) } /** * Returns the keys to all scenes in the container with the given name. @@ -70,11 +74,43 @@ constructor( currentSceneByContainerName.setValue(containerName, scene) } + /** Sets the scene transition in the container with the given name. */ + fun setSceneTransition(containerName: String, from: SceneKey, to: SceneKey) { + check(allSceneKeys(containerName).contains(from)) { + """ + Cannot set current scene key to "$from". The container "$containerName" does + not contain a scene with that key. + """ + .trimIndent() + } + check(allSceneKeys(containerName).contains(to)) { + """ + Cannot set current scene key to "$to". The container "$containerName" does + not contain a scene with that key. + """ + .trimIndent() + } + + sceneTransitionByContainerName.setValue( + containerName, + SceneTransitionModel(from = from, to = to) + ) + } + /** The current scene in the container with the given name. */ fun currentScene(containerName: String): StateFlow<SceneModel> { return currentSceneByContainerName.mutableOrError(containerName).asStateFlow() } + /** + * Scene transitions as pairs of keys. A new value is emitted exactly once, each time a scene + * transition occurs. The flow begins with a `null` value at first, because the initial scene is + * not something that we transition to from another scene. + */ + fun sceneTransitions(containerName: String): StateFlow<SceneTransitionModel?> { + return sceneTransitionByContainerName.mutableOrError(containerName).asStateFlow() + } + /** Sets whether the container with the given name is visible. */ fun setVisible(containerName: String, isVisible: Boolean) { containerVisibilityByName.setValue(containerName, isVisible) diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index 1e55975658a5..c704ea838130 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -20,6 +20,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.scene.data.repository.SceneContainerRepository import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.SceneTransitionModel import javax.inject.Inject import kotlinx.coroutines.flow.StateFlow @@ -43,7 +44,9 @@ constructor( /** Sets the scene in the container with the given name. */ fun setCurrentScene(containerName: String, scene: SceneModel) { + val currentSceneKey = repository.currentScene(containerName).value.key repository.setCurrentScene(containerName, scene) + repository.setSceneTransition(containerName, from = currentSceneKey, to = scene.key) } /** The current scene in the container with the given name. */ @@ -70,4 +73,13 @@ constructor( fun sceneTransitionProgress(containerName: String): StateFlow<Float> { return repository.sceneTransitionProgress(containerName) } + + /** + * Scene transitions as pairs of keys. A new value is emitted exactly once, each time a scene + * transition occurs. The flow begins with a `null` value at first, because the initial scene is + * not something that we transition to from another scene. + */ + fun sceneTransitions(containerName: String): StateFlow<SceneTransitionModel?> { + return repository.sceneTransitions(containerName) + } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneTransitionModel.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneTransitionModel.kt new file mode 100644 index 000000000000..c8f46a72d64f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneTransitionModel.kt @@ -0,0 +1,25 @@ +/* + * Copyright 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.scene.shared.model + +/** Models a transition between two scenes. */ +data class SceneTransitionModel( + /** The scene we transitioned away from. */ + val from: SceneKey, + /** The scene we transitioned into. */ + val to: SceneKey, +) diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt index ab4ead6749cb..4d506f0b5d5e 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt @@ -532,6 +532,12 @@ constructor( } } + /** Returns the ID of the currently-selected user. */ + @UserIdInt + fun getSelectedUserId(): Int { + return repository.getSelectedUserInfo().id + } + private fun showDialog(request: ShowDialogRequestModel) { _dialogShowRequests.value = request } @@ -774,17 +780,16 @@ constructor( } // TODO(b/246631653): cache the bitmaps to avoid the background work to fetch them. - val userIcon = withContext(backgroundDispatcher) { - manager.getUserIcon(userId) - ?.let { bitmap -> + val userIcon = + withContext(backgroundDispatcher) { + manager.getUserIcon(userId)?.let { bitmap -> val iconSize = - applicationContext - .resources - .getDimensionPixelSize(R.dimen.bouncer_user_switcher_icon_size) + applicationContext.resources.getDimensionPixelSize( + R.dimen.bouncer_user_switcher_icon_size + ) Icon.scaleDownIfNecessary(bitmap, iconSize, iconSize) } - } - + } if (userIcon != null) { return BitmapDrawable(userIcon) diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java deleted file mode 100644 index 58b1edcc5511..000000000000 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java +++ /dev/null @@ -1,732 +0,0 @@ -/* - * Copyright (C) 2020 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.keyguard; - -import static com.android.keyguard.KeyguardSecurityContainer.MODE_DEFAULT; -import static com.android.keyguard.KeyguardSecurityContainer.MODE_ONE_HANDED; -import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE; - -import static com.google.common.truth.Truth.assertThat; - -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertTrue; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.res.Configuration; -import android.content.res.Resources; -import android.hardware.biometrics.BiometricOverlayConstants; -import android.media.AudioManager; -import android.telephony.TelephonyManager; -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.testing.TestableResources; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.WindowInsetsController; -import android.widget.FrameLayout; - -import androidx.test.filters.SmallTest; - -import com.android.internal.logging.MetricsLogger; -import com.android.internal.logging.UiEventLogger; -import com.android.internal.widget.LockPatternUtils; -import com.android.keyguard.KeyguardSecurityModel.SecurityMode; -import com.android.systemui.R; -import com.android.systemui.SysuiTestCase; -import com.android.systemui.biometrics.SideFpsController; -import com.android.systemui.biometrics.SideFpsUiRequestSource; -import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor; -import com.android.systemui.classifier.FalsingA11yDelegate; -import com.android.systemui.classifier.FalsingCollector; -import com.android.systemui.flags.FakeFeatureFlags; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; -import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor; -import com.android.systemui.log.SessionTracker; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.statusbar.policy.ConfigurationController; -import com.android.systemui.statusbar.policy.KeyguardStateController; -import com.android.systemui.statusbar.policy.UserSwitcherController; -import com.android.systemui.util.settings.GlobalSettings; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatcher; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; - -import java.util.Optional; - -@SmallTest -@RunWith(AndroidTestingRunner.class) -@TestableLooper.RunWithLooper() -public class KeyguardSecurityContainerControllerTest extends SysuiTestCase { - private static final int TARGET_USER_ID = 100; - @Rule - public MockitoRule mRule = MockitoJUnit.rule(); - @Mock - private KeyguardSecurityContainer mView; - @Mock - private AdminSecondaryLockScreenController.Factory mAdminSecondaryLockScreenControllerFactory; - @Mock - private AdminSecondaryLockScreenController mAdminSecondaryLockScreenController; - @Mock - private LockPatternUtils mLockPatternUtils; - @Mock - private KeyguardUpdateMonitor mKeyguardUpdateMonitor; - @Mock - private KeyguardSecurityModel mKeyguardSecurityModel; - @Mock - private MetricsLogger mMetricsLogger; - @Mock - private UiEventLogger mUiEventLogger; - @Mock - private KeyguardStateController mKeyguardStateController; - @Mock - private KeyguardInputViewController mInputViewController; - @Mock - private WindowInsetsController mWindowInsetsController; - @Mock - private KeyguardSecurityViewFlipper mSecurityViewFlipper; - @Mock - private KeyguardSecurityViewFlipperController mKeyguardSecurityViewFlipperController; - @Mock - private KeyguardMessageAreaController.Factory mKeyguardMessageAreaControllerFactory; - @Mock - private KeyguardMessageAreaController mKeyguardMessageAreaController; - @Mock - private BouncerKeyguardMessageArea mKeyguardMessageArea; - @Mock - private ConfigurationController mConfigurationController; - @Mock - private EmergencyButtonController mEmergencyButtonController; - @Mock - private FalsingCollector mFalsingCollector; - @Mock - private FalsingManager mFalsingManager; - @Mock - private GlobalSettings mGlobalSettings; - @Mock - private FeatureFlags mFeatureFlags; - @Mock - private UserSwitcherController mUserSwitcherController; - @Mock - private SessionTracker mSessionTracker; - @Mock - private KeyguardViewController mKeyguardViewController; - @Mock - private SideFpsController mSideFpsController; - @Mock - private KeyguardPasswordViewController mKeyguardPasswordViewControllerMock; - @Mock - private FalsingA11yDelegate mFalsingA11yDelegate; - @Mock - private TelephonyManager mTelephonyManager; - @Mock - private ViewMediatorCallback mViewMediatorCallback; - @Mock - private AudioManager mAudioManager; - - @Captor - private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallback; - @Captor - private ArgumentCaptor<KeyguardSecurityContainer.SwipeListener> mSwipeListenerArgumentCaptor; - - @Captor - private ArgumentCaptor<KeyguardSecurityViewFlipperController.OnViewInflatedCallback> - mOnViewInflatedCallbackArgumentCaptor; - - private KeyguardSecurityContainerController mKeyguardSecurityContainerController; - private KeyguardPasswordViewController mKeyguardPasswordViewController; - private KeyguardPasswordView mKeyguardPasswordView; - private TestableResources mTestableResources; - - @Before - public void setup() { - mTestableResources = mContext.getOrCreateTestableResources(); - mTestableResources.getResources().getConfiguration().orientation = - Configuration.ORIENTATION_UNDEFINED; - - when(mView.getContext()).thenReturn(mContext); - when(mView.getResources()).thenReturn(mTestableResources.getResources()); - FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(/* width= */ 0, /* height= */ - 0); - lp.gravity = 0; - when(mView.getLayoutParams()).thenReturn(lp); - when(mAdminSecondaryLockScreenControllerFactory.create(any(KeyguardSecurityCallback.class))) - .thenReturn(mAdminSecondaryLockScreenController); - when(mSecurityViewFlipper.getWindowInsetsController()).thenReturn(mWindowInsetsController); - mKeyguardPasswordView = spy((KeyguardPasswordView) LayoutInflater.from(mContext).inflate( - R.layout.keyguard_password_view, null)); - when(mKeyguardPasswordView.getRootView()).thenReturn(mSecurityViewFlipper); - when(mKeyguardPasswordView.requireViewById(R.id.bouncer_message_area)) - .thenReturn(mKeyguardMessageArea); - when(mKeyguardMessageAreaControllerFactory.create(any(KeyguardMessageArea.class))) - .thenReturn(mKeyguardMessageAreaController); - when(mKeyguardPasswordView.getWindowInsetsController()).thenReturn(mWindowInsetsController); - when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(SecurityMode.PIN); - when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true); - FakeFeatureFlags featureFlags = new FakeFeatureFlags(); - featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true); - - mKeyguardPasswordViewController = new KeyguardPasswordViewController( - (KeyguardPasswordView) mKeyguardPasswordView, mKeyguardUpdateMonitor, - SecurityMode.Password, mLockPatternUtils, null, - mKeyguardMessageAreaControllerFactory, null, null, mEmergencyButtonController, - null, mock(Resources.class), null, mKeyguardViewController, - featureFlags); - - mKeyguardSecurityContainerController = new KeyguardSecurityContainerController( - mView, mAdminSecondaryLockScreenControllerFactory, mLockPatternUtils, - mKeyguardUpdateMonitor, mKeyguardSecurityModel, mMetricsLogger, mUiEventLogger, - mKeyguardStateController, mKeyguardSecurityViewFlipperController, - mConfigurationController, mFalsingCollector, mFalsingManager, - mUserSwitcherController, mFeatureFlags, mGlobalSettings, - mSessionTracker, Optional.of(mSideFpsController), mFalsingA11yDelegate, - mTelephonyManager, mViewMediatorCallback, mAudioManager, - mock(KeyguardFaceAuthInteractor.class), - mock(BouncerMessageInteractor.class)); - } - - @Test - public void onInitConfiguresViewMode() { - mKeyguardSecurityContainerController.onInit(); - verify(mView).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager), - eq(mUserSwitcherController), - any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class), - eq(mFalsingA11yDelegate)); - } - - @Test - public void showSecurityScreen_canInflateAllModes() { - SecurityMode[] modes = SecurityMode.values(); - for (SecurityMode mode : modes) { - when(mInputViewController.getSecurityMode()).thenReturn(mode); - mKeyguardSecurityContainerController.showSecurityScreen(mode); - if (mode == SecurityMode.Invalid) { - verify(mKeyguardSecurityViewFlipperController, never()).getSecurityView( - any(SecurityMode.class), any(KeyguardSecurityCallback.class), any( - KeyguardSecurityViewFlipperController.OnViewInflatedCallback.class) - ); - } else { - verify(mKeyguardSecurityViewFlipperController).getSecurityView( - eq(mode), any(KeyguardSecurityCallback.class), any( - KeyguardSecurityViewFlipperController.OnViewInflatedCallback.class) - ); - } - } - } - - @Test - public void onResourcesUpdate_callsThroughOnRotationChange() { - clearInvocations(mView); - - // Rotation is the same, shouldn't cause an update - mKeyguardSecurityContainerController.updateResources(); - verify(mView, never()).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager), - eq(mUserSwitcherController), - any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class), - eq(mFalsingA11yDelegate)); - - // Update rotation. Should trigger update - mTestableResources.getResources().getConfiguration().orientation = - Configuration.ORIENTATION_LANDSCAPE; - - mKeyguardSecurityContainerController.updateResources(); - verify(mView).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager), - eq(mUserSwitcherController), - any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class), - eq(mFalsingA11yDelegate)); - } - - private void touchDown() { - mKeyguardSecurityContainerController.mGlobalTouchListener.onTouchEvent( - MotionEvent.obtain( - /* downTime= */0, - /* eventTime= */0, - MotionEvent.ACTION_DOWN, - /* x= */0, - /* y= */0, - /* metaState= */0)); - } - - @Test - public void onInterceptTap_inhibitsFalsingInSidedSecurityMode() { - - when(mView.isTouchOnTheOtherSideOfSecurity(any())).thenReturn(false); - touchDown(); - verify(mFalsingCollector, never()).avoidGesture(); - - when(mView.isTouchOnTheOtherSideOfSecurity(any())).thenReturn(true); - touchDown(); - verify(mFalsingCollector).avoidGesture(); - } - - @Test - public void showSecurityScreen_oneHandedMode_flagDisabled_noOneHandedMode() { - mTestableResources.addOverride(R.bool.can_use_one_handed_bouncer, false); - setupGetSecurityView(SecurityMode.Pattern); - - mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.Pattern); - verify(mView).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager), - eq(mUserSwitcherController), - any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class), - eq(mFalsingA11yDelegate)); - } - - @Test - public void showSecurityScreen_oneHandedMode_flagEnabled_oneHandedMode() { - mTestableResources.addOverride(R.bool.can_use_one_handed_bouncer, true); - setupGetSecurityView(SecurityMode.Pattern); - verify(mView).initMode(eq(MODE_ONE_HANDED), eq(mGlobalSettings), eq(mFalsingManager), - eq(mUserSwitcherController), - any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class), - eq(mFalsingA11yDelegate)); - } - - @Test - public void showSecurityScreen_twoHandedMode_flagEnabled_noOneHandedMode() { - mTestableResources.addOverride(R.bool.can_use_one_handed_bouncer, true); - setupGetSecurityView(SecurityMode.Password); - - verify(mView).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager), - eq(mUserSwitcherController), - any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class), - eq(mFalsingA11yDelegate)); - } - - @Test - public void addUserSwitcherCallback() { - ArgumentCaptor<KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback> - captor = ArgumentCaptor.forClass( - KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class); - setupGetSecurityView(SecurityMode.Password); - - verify(mView).initMode(anyInt(), any(GlobalSettings.class), any(FalsingManager.class), - any(UserSwitcherController.class), - captor.capture(), - eq(mFalsingA11yDelegate)); - captor.getValue().showUnlockToContinueMessage(); - getViewControllerImmediately(); - verify(mKeyguardPasswordViewControllerMock).showMessage( - /* message= */ getContext().getString(R.string.keyguard_unlock_to_continue), - /* colorState= */ null, - /* animated= */ true); - } - - @Test - public void addUserSwitchCallback() { - mKeyguardSecurityContainerController.onViewAttached(); - verify(mUserSwitcherController) - .addUserSwitchCallback(any(UserSwitcherController.UserSwitchCallback.class)); - mKeyguardSecurityContainerController.onViewDetached(); - verify(mUserSwitcherController) - .removeUserSwitchCallback(any(UserSwitcherController.UserSwitchCallback.class)); - } - - @Test - public void onBouncerVisibilityChanged_resetsScale() { - mKeyguardSecurityContainerController.onBouncerVisibilityChanged(false); - verify(mView).resetScale(); - } - - @Test - public void showNextSecurityScreenOrFinish_setsSecurityScreenToPinAfterSimPinUnlock() { - // GIVEN the current security method is SimPin - when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(false); - when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(TARGET_USER_ID)).thenReturn(false); - mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.SimPin); - - // WHEN a request is made from the SimPin screens to show the next security method - when(mKeyguardSecurityModel.getSecurityMode(TARGET_USER_ID)).thenReturn(SecurityMode.PIN); - mKeyguardSecurityContainerController.showNextSecurityScreenOrFinish( - /* authenticated= */true, - TARGET_USER_ID, - /* bypassSecondaryLockScreen= */true, - SecurityMode.SimPin); - - // THEN the next security method of PIN is set, and the keyguard is not marked as done - - verify(mViewMediatorCallback, never()).keyguardDonePending(anyBoolean(), anyInt()); - verify(mViewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt()); - assertThat(mKeyguardSecurityContainerController.getCurrentSecurityMode()) - .isEqualTo(SecurityMode.PIN); - } - - @Test - public void showNextSecurityScreenOrFinish_DeviceNotSecure() { - // GIVEN the current security method is SimPin - when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(false); - when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(TARGET_USER_ID)).thenReturn(false); - mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.SimPin); - - // WHEN a request is made from the SimPin screens to show the next security method - when(mKeyguardSecurityModel.getSecurityMode(TARGET_USER_ID)).thenReturn(SecurityMode.None); - when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(true); - mKeyguardSecurityContainerController.showNextSecurityScreenOrFinish( - /* authenticated= */true, - TARGET_USER_ID, - /* bypassSecondaryLockScreen= */true, - SecurityMode.SimPin); - - // THEN the next security method of None will dismiss keyguard. - verify(mViewMediatorCallback).keyguardDone(anyBoolean(), anyInt()); - } - - @Test - public void showNextSecurityScreenOrFinish_ignoresCallWhenSecurityMethodHasChanged() { - //GIVEN current security mode has been set to PIN - mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.PIN); - - //WHEN a request comes from SimPin to dismiss the security screens - boolean keyguardDone = mKeyguardSecurityContainerController.showNextSecurityScreenOrFinish( - /* authenticated= */true, - TARGET_USER_ID, - /* bypassSecondaryLockScreen= */true, - SecurityMode.SimPin); - - //THEN no action has happened, which will not dismiss the security screens - assertThat(keyguardDone).isEqualTo(false); - verify(mKeyguardUpdateMonitor, never()).getUserHasTrust(anyInt()); - } - - @Test - public void showNextSecurityScreenOrFinish_SimPin_Swipe() { - // GIVEN the current security method is SimPin - when(mKeyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(false); - when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(TARGET_USER_ID)).thenReturn(false); - mKeyguardSecurityContainerController.showSecurityScreen(SecurityMode.SimPin); - - // WHEN a request is made from the SimPin screens to show the next security method - when(mKeyguardSecurityModel.getSecurityMode(TARGET_USER_ID)).thenReturn(SecurityMode.None); - // WHEN security method is SWIPE - when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false); - mKeyguardSecurityContainerController.showNextSecurityScreenOrFinish( - /* authenticated= */true, - TARGET_USER_ID, - /* bypassSecondaryLockScreen= */true, - SecurityMode.SimPin); - - // THEN the next security method of None will dismiss keyguard. - verify(mViewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt()); - } - - - @Test - public void onSwipeUp_whenFaceDetectionIsNotRunning_initiatesFaceAuth() { - KeyguardSecurityContainer.SwipeListener registeredSwipeListener = - getRegisteredSwipeListener(); - when(mKeyguardUpdateMonitor.isFaceDetectionRunning()).thenReturn(false); - setupGetSecurityView(SecurityMode.Password); - - registeredSwipeListener.onSwipeUp(); - - verify(mKeyguardUpdateMonitor).requestFaceAuth( - FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER); - } - - @Test - public void onSwipeUp_whenFaceDetectionIsRunning_doesNotInitiateFaceAuth() { - KeyguardSecurityContainer.SwipeListener registeredSwipeListener = - getRegisteredSwipeListener(); - when(mKeyguardUpdateMonitor.isFaceDetectionRunning()).thenReturn(true); - - registeredSwipeListener.onSwipeUp(); - - verify(mKeyguardUpdateMonitor, never()) - .requestFaceAuth(FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER); - } - - @Test - public void onSwipeUp_whenFaceDetectionIsTriggered_hidesBouncerMessage() { - KeyguardSecurityContainer.SwipeListener registeredSwipeListener = - getRegisteredSwipeListener(); - when(mKeyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER)) - .thenReturn(true); - setupGetSecurityView(SecurityMode.Password); - - clearInvocations(mKeyguardSecurityViewFlipperController); - registeredSwipeListener.onSwipeUp(); - getViewControllerImmediately(); - - verify(mKeyguardPasswordViewControllerMock).showMessage(/* message= */ - null, /* colorState= */ null, /* animated= */ true); - } - - @Test - public void onSwipeUp_whenFaceDetectionIsNotTriggered_retainsBouncerMessage() { - KeyguardSecurityContainer.SwipeListener registeredSwipeListener = - getRegisteredSwipeListener(); - when(mKeyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER)) - .thenReturn(false); - setupGetSecurityView(SecurityMode.Password); - - registeredSwipeListener.onSwipeUp(); - - verify(mKeyguardPasswordViewControllerMock, never()).showMessage(/* message= */ - null, /* colorState= */ null, /* animated= */ true); - } - - @Test - public void onDensityOrFontScaleChanged() { - ArgumentCaptor<ConfigurationController.ConfigurationListener> - configurationListenerArgumentCaptor = ArgumentCaptor.forClass( - ConfigurationController.ConfigurationListener.class); - mKeyguardSecurityContainerController.onViewAttached(); - verify(mConfigurationController).addCallback(configurationListenerArgumentCaptor.capture()); - clearInvocations(mKeyguardSecurityViewFlipperController); - - configurationListenerArgumentCaptor.getValue().onDensityOrFontScaleChanged(); - - verify(mKeyguardSecurityViewFlipperController).clearViews(); - verify(mKeyguardSecurityViewFlipperController).asynchronouslyInflateView( - eq(SecurityMode.PIN), - any(KeyguardSecurityCallback.class), - mOnViewInflatedCallbackArgumentCaptor.capture()); - - mOnViewInflatedCallbackArgumentCaptor.getValue().onViewInflated(mInputViewController); - - verify(mView).onDensityOrFontScaleChanged(); - } - - @Test - public void onThemeChanged() { - ArgumentCaptor<ConfigurationController.ConfigurationListener> - configurationListenerArgumentCaptor = ArgumentCaptor.forClass( - ConfigurationController.ConfigurationListener.class); - mKeyguardSecurityContainerController.onViewAttached(); - verify(mConfigurationController).addCallback(configurationListenerArgumentCaptor.capture()); - clearInvocations(mKeyguardSecurityViewFlipperController); - - configurationListenerArgumentCaptor.getValue().onThemeChanged(); - - verify(mKeyguardSecurityViewFlipperController).clearViews(); - verify(mKeyguardSecurityViewFlipperController).asynchronouslyInflateView( - eq(SecurityMode.PIN), - any(KeyguardSecurityCallback.class), - mOnViewInflatedCallbackArgumentCaptor.capture()); - - mOnViewInflatedCallbackArgumentCaptor.getValue().onViewInflated(mInputViewController); - - verify(mView).reset(); - verify(mKeyguardSecurityViewFlipperController).reset(); - verify(mView).reloadColors(); - } - - @Test - public void onUiModeChanged() { - ArgumentCaptor<ConfigurationController.ConfigurationListener> - configurationListenerArgumentCaptor = ArgumentCaptor.forClass( - ConfigurationController.ConfigurationListener.class); - mKeyguardSecurityContainerController.onViewAttached(); - verify(mConfigurationController).addCallback(configurationListenerArgumentCaptor.capture()); - clearInvocations(mKeyguardSecurityViewFlipperController); - - configurationListenerArgumentCaptor.getValue().onUiModeChanged(); - - verify(mKeyguardSecurityViewFlipperController).clearViews(); - verify(mKeyguardSecurityViewFlipperController).asynchronouslyInflateView( - eq(SecurityMode.PIN), - any(KeyguardSecurityCallback.class), - mOnViewInflatedCallbackArgumentCaptor.capture()); - - mOnViewInflatedCallbackArgumentCaptor.getValue().onViewInflated(mInputViewController); - - verify(mView).reloadColors(); - } - - @Test - public void testHasDismissActions() { - assertFalse("Action not set yet", mKeyguardSecurityContainerController.hasDismissActions()); - mKeyguardSecurityContainerController.setOnDismissAction(mock( - ActivityStarter.OnDismissAction.class), - null /* cancelAction */); - assertTrue("Action should exist", mKeyguardSecurityContainerController.hasDismissActions()); - } - - @Test - public void testWillRunDismissFromKeyguardIsTrue() { - ActivityStarter.OnDismissAction action = mock(ActivityStarter.OnDismissAction.class); - when(action.willRunAnimationOnKeyguard()).thenReturn(true); - mKeyguardSecurityContainerController.setOnDismissAction(action, null /* cancelAction */); - - mKeyguardSecurityContainerController.finish(false /* strongAuth */, 0 /* currentUser */); - - assertThat(mKeyguardSecurityContainerController.willRunDismissFromKeyguard()).isTrue(); - } - - @Test - public void testWillRunDismissFromKeyguardIsFalse() { - ActivityStarter.OnDismissAction action = mock(ActivityStarter.OnDismissAction.class); - when(action.willRunAnimationOnKeyguard()).thenReturn(false); - mKeyguardSecurityContainerController.setOnDismissAction(action, null /* cancelAction */); - - mKeyguardSecurityContainerController.finish(false /* strongAuth */, 0 /* currentUser */); - - assertThat(mKeyguardSecurityContainerController.willRunDismissFromKeyguard()).isFalse(); - } - - @Test - public void testWillRunDismissFromKeyguardIsFalseWhenNoDismissActionSet() { - mKeyguardSecurityContainerController.setOnDismissAction(null /* action */, - null /* cancelAction */); - - mKeyguardSecurityContainerController.finish(false /* strongAuth */, 0 /* currentUser */); - - assertThat(mKeyguardSecurityContainerController.willRunDismissFromKeyguard()).isFalse(); - } - - @Test - public void testOnStartingToHide() { - mKeyguardSecurityContainerController.onStartingToHide(); - verify(mKeyguardSecurityViewFlipperController).getSecurityView(any(SecurityMode.class), - any(KeyguardSecurityCallback.class), - mOnViewInflatedCallbackArgumentCaptor.capture()); - - mOnViewInflatedCallbackArgumentCaptor.getValue().onViewInflated(mInputViewController); - verify(mInputViewController).onStartingToHide(); - } - - @Test - public void testGravityReappliedOnConfigurationChange() { - // Set initial gravity - mTestableResources.addOverride(R.integer.keyguard_host_view_gravity, - Gravity.CENTER); - mTestableResources.addOverride( - R.bool.can_use_one_handed_bouncer, false); - - // Kick off the initial pass... - mKeyguardSecurityContainerController.onInit(); - verify(mView).setLayoutParams(any()); - clearInvocations(mView); - - // Now simulate a config change - mTestableResources.addOverride(R.integer.keyguard_host_view_gravity, - Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); - - mKeyguardSecurityContainerController.updateResources(); - verify(mView).setLayoutParams(any()); - } - - @Test - public void testGravityUsesOneHandGravityWhenApplicable() { - mTestableResources.addOverride( - R.integer.keyguard_host_view_gravity, - Gravity.CENTER); - mTestableResources.addOverride( - R.integer.keyguard_host_view_one_handed_gravity, - Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); - - // Start disabled. - mTestableResources.addOverride( - R.bool.can_use_one_handed_bouncer, false); - - mKeyguardSecurityContainerController.onInit(); - verify(mView).setLayoutParams(argThat( - (ArgumentMatcher<FrameLayout.LayoutParams>) argument -> - argument.gravity == Gravity.CENTER)); - clearInvocations(mView); - - // And enable - mTestableResources.addOverride( - R.bool.can_use_one_handed_bouncer, true); - - mKeyguardSecurityContainerController.updateResources(); - verify(mView).setLayoutParams(argThat( - (ArgumentMatcher<FrameLayout.LayoutParams>) argument -> - argument.gravity == (Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM))); - } - - @Test - public void testUpdateKeyguardPositionDelegatesToSecurityContainer() { - mKeyguardSecurityContainerController.updateKeyguardPosition(1.0f); - verify(mView).updatePositionByTouchX(1.0f); - } - - @Test - public void testReinflateViewFlipper() { - KeyguardSecurityViewFlipperController.OnViewInflatedCallback onViewInflatedCallback = - controller -> { - }; - mKeyguardSecurityContainerController.reinflateViewFlipper(onViewInflatedCallback); - verify(mKeyguardSecurityViewFlipperController).clearViews(); - verify(mKeyguardSecurityViewFlipperController).asynchronouslyInflateView( - any(SecurityMode.class), - any(KeyguardSecurityCallback.class), eq(onViewInflatedCallback)); - } - - @Test - public void testSideFpsControllerShow() { - mKeyguardSecurityContainerController.updateSideFpsVisibility(/* isVisible= */ true); - verify(mSideFpsController).show( - SideFpsUiRequestSource.PRIMARY_BOUNCER, - BiometricOverlayConstants.REASON_AUTH_KEYGUARD); - } - - @Test - public void testSideFpsControllerHide() { - mKeyguardSecurityContainerController.updateSideFpsVisibility(/* isVisible= */ false); - verify(mSideFpsController).hide(SideFpsUiRequestSource.PRIMARY_BOUNCER); - } - - @Test - public void setExpansion_setsAlpha() { - mKeyguardSecurityContainerController.setExpansion(EXPANSION_VISIBLE); - - verify(mView).setAlpha(1f); - verify(mView).setTranslationY(0f); - } - - private KeyguardSecurityContainer.SwipeListener getRegisteredSwipeListener() { - mKeyguardSecurityContainerController.onViewAttached(); - verify(mView).setSwipeListener(mSwipeListenerArgumentCaptor.capture()); - return mSwipeListenerArgumentCaptor.getValue(); - } - - private void setupGetSecurityView(SecurityMode securityMode) { - mKeyguardSecurityContainerController.showSecurityScreen(securityMode); - getViewControllerImmediately(); - } - - private void getViewControllerImmediately() { - verify(mKeyguardSecurityViewFlipperController, atLeastOnce()).getSecurityView( - any(SecurityMode.class), any(), - mOnViewInflatedCallbackArgumentCaptor.capture()); - mOnViewInflatedCallbackArgumentCaptor.getValue().onViewInflated( - (KeyguardInputViewController) mKeyguardPasswordViewControllerMock); - - } -} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt new file mode 100644 index 000000000000..d44717420bdf --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt @@ -0,0 +1,810 @@ +/* + * 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. + */ +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.keyguard + +import android.content.res.Configuration +import android.hardware.biometrics.BiometricOverlayConstants +import android.media.AudioManager +import android.telephony.TelephonyManager +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.testing.TestableResources +import android.view.Gravity +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.WindowInsetsController +import android.widget.FrameLayout +import androidx.test.filters.SmallTest +import com.android.internal.logging.MetricsLogger +import com.android.internal.logging.UiEventLogger +import com.android.internal.widget.LockPatternUtils +import com.android.keyguard.KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback +import com.android.keyguard.KeyguardSecurityModel.SecurityMode +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.SideFpsController +import com.android.systemui.biometrics.SideFpsUiRequestSource +import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants +import com.android.systemui.classifier.FalsingA11yDelegate +import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.log.SessionTracker +import com.android.systemui.plugins.ActivityStarter.OnDismissAction +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.user.domain.interactor.UserInteractor +import com.android.systemui.util.kotlin.JavaAdapter +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argThat +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.settings.GlobalSettings +import com.google.common.truth.Truth +import java.util.Optional +import junit.framework.Assert +import kotlinx.coroutines.ExperimentalCoroutinesApi +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.ArgumentMatcher +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.atLeastOnce +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class KeyguardSecurityContainerControllerTest : SysuiTestCase() { + + @Mock private lateinit var view: KeyguardSecurityContainer + @Mock + private lateinit var adminSecondaryLockScreenControllerFactory: + AdminSecondaryLockScreenController.Factory + @Mock + private lateinit var adminSecondaryLockScreenController: AdminSecondaryLockScreenController + @Mock private lateinit var lockPatternUtils: LockPatternUtils + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + @Mock private lateinit var keyguardSecurityModel: KeyguardSecurityModel + @Mock private lateinit var metricsLogger: MetricsLogger + @Mock private lateinit var uiEventLogger: UiEventLogger + @Mock private lateinit var keyguardStateController: KeyguardStateController + @Mock private lateinit var inputViewController: KeyguardInputViewController<KeyguardInputView> + @Mock private lateinit var windowInsetsController: WindowInsetsController + @Mock private lateinit var securityViewFlipper: KeyguardSecurityViewFlipper + @Mock private lateinit var viewFlipperController: KeyguardSecurityViewFlipperController + @Mock private lateinit var messageAreaControllerFactory: KeyguardMessageAreaController.Factory + @Mock private lateinit var keyguardMessageAreaController: KeyguardMessageAreaController<*> + @Mock private lateinit var keyguardMessageArea: BouncerKeyguardMessageArea + @Mock private lateinit var configurationController: ConfigurationController + @Mock private lateinit var emergencyButtonController: EmergencyButtonController + @Mock private lateinit var falsingCollector: FalsingCollector + @Mock private lateinit var falsingManager: FalsingManager + @Mock private lateinit var globalSettings: GlobalSettings + @Mock private lateinit var userSwitcherController: UserSwitcherController + @Mock private lateinit var sessionTracker: SessionTracker + @Mock private lateinit var keyguardViewController: KeyguardViewController + @Mock private lateinit var sideFpsController: SideFpsController + @Mock private lateinit var keyguardPasswordViewControllerMock: KeyguardPasswordViewController + @Mock private lateinit var falsingA11yDelegate: FalsingA11yDelegate + @Mock private lateinit var telephonyManager: TelephonyManager + @Mock private lateinit var viewMediatorCallback: ViewMediatorCallback + @Mock private lateinit var audioManager: AudioManager + @Mock private lateinit var userInteractor: UserInteractor + + @Captor + private lateinit var swipeListenerArgumentCaptor: + ArgumentCaptor<KeyguardSecurityContainer.SwipeListener> + @Captor + private lateinit var onViewInflatedCallbackArgumentCaptor: + ArgumentCaptor<KeyguardSecurityViewFlipperController.OnViewInflatedCallback> + + private lateinit var featureFlags: FakeFeatureFlags + private lateinit var keyguardPasswordViewController: KeyguardPasswordViewController + private lateinit var keyguardPasswordView: KeyguardPasswordView + private lateinit var testableResources: TestableResources + private lateinit var sceneTestUtils: SceneTestUtils + private lateinit var sceneInteractor: SceneInteractor + + private lateinit var underTest: KeyguardSecurityContainerController + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + testableResources = mContext.getOrCreateTestableResources() + testableResources.resources.configuration.orientation = Configuration.ORIENTATION_UNDEFINED + whenever(view.context).thenReturn(mContext) + whenever(view.resources).thenReturn(testableResources.resources) + + val lp = FrameLayout.LayoutParams(/* width= */ 0, /* height= */ 0) + lp.gravity = 0 + whenever(view.layoutParams).thenReturn(lp) + + whenever(adminSecondaryLockScreenControllerFactory.create(any())) + .thenReturn(adminSecondaryLockScreenController) + whenever(securityViewFlipper.windowInsetsController).thenReturn(windowInsetsController) + keyguardPasswordView = + spy( + LayoutInflater.from(mContext).inflate(R.layout.keyguard_password_view, null) + as KeyguardPasswordView + ) + whenever(keyguardPasswordView.rootView).thenReturn(securityViewFlipper) + whenever<Any?>(keyguardPasswordView.requireViewById(R.id.bouncer_message_area)) + .thenReturn(keyguardMessageArea) + whenever(messageAreaControllerFactory.create(any())) + .thenReturn(keyguardMessageAreaController) + whenever(keyguardPasswordView.windowInsetsController).thenReturn(windowInsetsController) + whenever(keyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(SecurityMode.PIN) + whenever(keyguardStateController.canDismissLockScreen()).thenReturn(true) + + featureFlags = FakeFeatureFlags() + featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) + featureFlags.set(Flags.SCENE_CONTAINER, false) + featureFlags.set(Flags.BOUNCER_USER_SWITCHER, false) + + keyguardPasswordViewController = + KeyguardPasswordViewController( + keyguardPasswordView, + keyguardUpdateMonitor, + SecurityMode.Password, + lockPatternUtils, + null, + messageAreaControllerFactory, + null, + null, + emergencyButtonController, + null, + mock(), + null, + keyguardViewController, + featureFlags + ) + + whenever(userInteractor.getSelectedUserId()).thenReturn(TARGET_USER_ID) + sceneTestUtils = SceneTestUtils(this) + sceneInteractor = sceneTestUtils.sceneInteractor() + + underTest = + KeyguardSecurityContainerController( + view, + adminSecondaryLockScreenControllerFactory, + lockPatternUtils, + keyguardUpdateMonitor, + keyguardSecurityModel, + metricsLogger, + uiEventLogger, + keyguardStateController, + viewFlipperController, + configurationController, + falsingCollector, + falsingManager, + userSwitcherController, + featureFlags, + globalSettings, + sessionTracker, + Optional.of(sideFpsController), + falsingA11yDelegate, + telephonyManager, + viewMediatorCallback, + audioManager, + mock(), + mock(), + { JavaAdapter(sceneTestUtils.testScope.backgroundScope) }, + userInteractor, + ) { + sceneInteractor + } + } + + @Test + fun onInitConfiguresViewMode() { + underTest.onInit() + verify(view) + .initMode( + eq(KeyguardSecurityContainer.MODE_DEFAULT), + eq(globalSettings), + eq(falsingManager), + eq(userSwitcherController), + any(), + eq(falsingA11yDelegate) + ) + } + + @Test + fun showSecurityScreen_canInflateAllModes() { + val modes = SecurityMode.values() + for (mode in modes) { + whenever(inputViewController.securityMode).thenReturn(mode) + underTest.showSecurityScreen(mode) + if (mode == SecurityMode.Invalid) { + verify(viewFlipperController, never()).getSecurityView(any(), any(), any()) + } else { + verify(viewFlipperController).getSecurityView(eq(mode), any(), any()) + } + } + } + + @Test + fun onResourcesUpdate_callsThroughOnRotationChange() { + clearInvocations(view) + + // Rotation is the same, shouldn't cause an update + underTest.updateResources() + verify(view, never()) + .initMode( + eq(KeyguardSecurityContainer.MODE_DEFAULT), + eq(globalSettings), + eq(falsingManager), + eq(userSwitcherController), + any(), + eq(falsingA11yDelegate) + ) + + // Update rotation. Should trigger update + testableResources.resources.configuration.orientation = Configuration.ORIENTATION_LANDSCAPE + underTest.updateResources() + verify(view) + .initMode( + eq(KeyguardSecurityContainer.MODE_DEFAULT), + eq(globalSettings), + eq(falsingManager), + eq(userSwitcherController), + any(), + eq(falsingA11yDelegate) + ) + } + + private fun touchDown() { + underTest.mGlobalTouchListener.onTouchEvent( + MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 0, + MotionEvent.ACTION_DOWN, + /* x= */ 0f, + /* y= */ 0f, + /* metaState= */ 0 + ) + ) + } + + @Test + fun onInterceptTap_inhibitsFalsingInSidedSecurityMode() { + whenever(view.isTouchOnTheOtherSideOfSecurity(any())).thenReturn(false) + touchDown() + verify(falsingCollector, never()).avoidGesture() + whenever(view.isTouchOnTheOtherSideOfSecurity(any())).thenReturn(true) + touchDown() + verify(falsingCollector).avoidGesture() + } + + @Test + fun showSecurityScreen_oneHandedMode_flagDisabled_noOneHandedMode() { + testableResources.addOverride(R.bool.can_use_one_handed_bouncer, false) + setupGetSecurityView(SecurityMode.Pattern) + underTest.showSecurityScreen(SecurityMode.Pattern) + verify(view) + .initMode( + eq(KeyguardSecurityContainer.MODE_DEFAULT), + eq(globalSettings), + eq(falsingManager), + eq(userSwitcherController), + any(), + eq(falsingA11yDelegate) + ) + } + + @Test + fun showSecurityScreen_oneHandedMode_flagEnabled_oneHandedMode() { + testableResources.addOverride(R.bool.can_use_one_handed_bouncer, true) + setupGetSecurityView(SecurityMode.Pattern) + verify(view) + .initMode( + eq(KeyguardSecurityContainer.MODE_ONE_HANDED), + eq(globalSettings), + eq(falsingManager), + eq(userSwitcherController), + any(), + eq(falsingA11yDelegate) + ) + } + + @Test + fun showSecurityScreen_twoHandedMode_flagEnabled_noOneHandedMode() { + testableResources.addOverride(R.bool.can_use_one_handed_bouncer, true) + setupGetSecurityView(SecurityMode.Password) + verify(view) + .initMode( + eq(KeyguardSecurityContainer.MODE_DEFAULT), + eq(globalSettings), + eq(falsingManager), + eq(userSwitcherController), + any(), + eq(falsingA11yDelegate) + ) + } + + @Test + fun addUserSwitcherCallback() { + val captor = ArgumentCaptor.forClass(UserSwitcherCallback::class.java) + setupGetSecurityView(SecurityMode.Password) + verify(view) + .initMode(anyInt(), any(), any(), any(), captor.capture(), eq(falsingA11yDelegate)) + captor.value.showUnlockToContinueMessage() + viewControllerImmediately + verify(keyguardPasswordViewControllerMock) + .showMessage( + /* message= */ context.getString(R.string.keyguard_unlock_to_continue), + /* colorState= */ null, + /* animated= */ true + ) + } + + @Test + fun addUserSwitchCallback() { + underTest.onViewAttached() + verify(userSwitcherController).addUserSwitchCallback(any()) + underTest.onViewDetached() + verify(userSwitcherController).removeUserSwitchCallback(any()) + } + + @Test + fun onBouncerVisibilityChanged_resetsScale() { + underTest.onBouncerVisibilityChanged(false) + verify(view).resetScale() + } + + @Test + fun showNextSecurityScreenOrFinish_setsSecurityScreenToPinAfterSimPinUnlock() { + // GIVEN the current security method is SimPin + whenever(keyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(false) + whenever(keyguardUpdateMonitor.getUserUnlockedWithBiometric(TARGET_USER_ID)) + .thenReturn(false) + underTest.showSecurityScreen(SecurityMode.SimPin) + + // WHEN a request is made from the SimPin screens to show the next security method + whenever(keyguardSecurityModel.getSecurityMode(TARGET_USER_ID)).thenReturn(SecurityMode.PIN) + underTest.showNextSecurityScreenOrFinish( + /* authenticated= */ true, + TARGET_USER_ID, + /* bypassSecondaryLockScreen= */ true, + SecurityMode.SimPin + ) + + // THEN the next security method of PIN is set, and the keyguard is not marked as done + verify(viewMediatorCallback, never()).keyguardDonePending(anyBoolean(), anyInt()) + verify(viewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt()) + Truth.assertThat(underTest.currentSecurityMode).isEqualTo(SecurityMode.PIN) + } + + @Test + fun showNextSecurityScreenOrFinish_DeviceNotSecure() { + // GIVEN the current security method is SimPin + whenever(keyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(false) + whenever(keyguardUpdateMonitor.getUserUnlockedWithBiometric(TARGET_USER_ID)) + .thenReturn(false) + underTest.showSecurityScreen(SecurityMode.SimPin) + + // WHEN a request is made from the SimPin screens to show the next security method + whenever(keyguardSecurityModel.getSecurityMode(TARGET_USER_ID)) + .thenReturn(SecurityMode.None) + whenever(lockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(true) + underTest.showNextSecurityScreenOrFinish( + /* authenticated= */ true, + TARGET_USER_ID, + /* bypassSecondaryLockScreen= */ true, + SecurityMode.SimPin + ) + + // THEN the next security method of None will dismiss keyguard. + verify(viewMediatorCallback).keyguardDone(anyBoolean(), anyInt()) + } + + @Test + fun showNextSecurityScreenOrFinish_ignoresCallWhenSecurityMethodHasChanged() { + // GIVEN current security mode has been set to PIN + underTest.showSecurityScreen(SecurityMode.PIN) + + // WHEN a request comes from SimPin to dismiss the security screens + val keyguardDone = + underTest.showNextSecurityScreenOrFinish( + /* authenticated= */ true, + TARGET_USER_ID, + /* bypassSecondaryLockScreen= */ true, + SecurityMode.SimPin + ) + + // THEN no action has happened, which will not dismiss the security screens + Truth.assertThat(keyguardDone).isEqualTo(false) + verify(keyguardUpdateMonitor, never()).getUserHasTrust(anyInt()) + } + + @Test + fun showNextSecurityScreenOrFinish_SimPin_Swipe() { + // GIVEN the current security method is SimPin + whenever(keyguardUpdateMonitor.getUserHasTrust(anyInt())).thenReturn(false) + whenever(keyguardUpdateMonitor.getUserUnlockedWithBiometric(TARGET_USER_ID)) + .thenReturn(false) + underTest.showSecurityScreen(SecurityMode.SimPin) + + // WHEN a request is made from the SimPin screens to show the next security method + whenever(keyguardSecurityModel.getSecurityMode(TARGET_USER_ID)) + .thenReturn(SecurityMode.None) + // WHEN security method is SWIPE + whenever(lockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false) + underTest.showNextSecurityScreenOrFinish( + /* authenticated= */ true, + TARGET_USER_ID, + /* bypassSecondaryLockScreen= */ true, + SecurityMode.SimPin + ) + + // THEN the next security method of None will dismiss keyguard. + verify(viewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt()) + } + + @Test + fun onSwipeUp_whenFaceDetectionIsNotRunning_initiatesFaceAuth() { + val registeredSwipeListener = registeredSwipeListener + whenever(keyguardUpdateMonitor.isFaceDetectionRunning).thenReturn(false) + setupGetSecurityView(SecurityMode.Password) + registeredSwipeListener.onSwipeUp() + verify(keyguardUpdateMonitor).requestFaceAuth(FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER) + } + + @Test + fun onSwipeUp_whenFaceDetectionIsRunning_doesNotInitiateFaceAuth() { + val registeredSwipeListener = registeredSwipeListener + whenever(keyguardUpdateMonitor.isFaceDetectionRunning).thenReturn(true) + registeredSwipeListener.onSwipeUp() + verify(keyguardUpdateMonitor, never()) + .requestFaceAuth(FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER) + } + + @Test + fun onSwipeUp_whenFaceDetectionIsTriggered_hidesBouncerMessage() { + val registeredSwipeListener = registeredSwipeListener + whenever( + keyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER) + ) + .thenReturn(true) + setupGetSecurityView(SecurityMode.Password) + clearInvocations(viewFlipperController) + registeredSwipeListener.onSwipeUp() + viewControllerImmediately + verify(keyguardPasswordViewControllerMock) + .showMessage(/* message= */ null, /* colorState= */ null, /* animated= */ true) + } + + @Test + fun onSwipeUp_whenFaceDetectionIsNotTriggered_retainsBouncerMessage() { + val registeredSwipeListener = registeredSwipeListener + whenever( + keyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.SWIPE_UP_ON_BOUNCER) + ) + .thenReturn(false) + setupGetSecurityView(SecurityMode.Password) + registeredSwipeListener.onSwipeUp() + verify(keyguardPasswordViewControllerMock, never()) + .showMessage(/* message= */ null, /* colorState= */ null, /* animated= */ true) + } + + @Test + fun onDensityOrFontScaleChanged() { + val configurationListenerArgumentCaptor = + ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java) + underTest.onViewAttached() + verify(configurationController).addCallback(configurationListenerArgumentCaptor.capture()) + clearInvocations(viewFlipperController) + configurationListenerArgumentCaptor.value.onDensityOrFontScaleChanged() + verify(viewFlipperController).clearViews() + verify(viewFlipperController) + .asynchronouslyInflateView( + eq(SecurityMode.PIN), + any(), + onViewInflatedCallbackArgumentCaptor.capture() + ) + onViewInflatedCallbackArgumentCaptor.value.onViewInflated(inputViewController) + verify(view).onDensityOrFontScaleChanged() + } + + @Test + fun onThemeChanged() { + val configurationListenerArgumentCaptor = + ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java) + underTest.onViewAttached() + verify(configurationController).addCallback(configurationListenerArgumentCaptor.capture()) + clearInvocations(viewFlipperController) + configurationListenerArgumentCaptor.value.onThemeChanged() + verify(viewFlipperController).clearViews() + verify(viewFlipperController) + .asynchronouslyInflateView( + eq(SecurityMode.PIN), + any(), + onViewInflatedCallbackArgumentCaptor.capture() + ) + onViewInflatedCallbackArgumentCaptor.value.onViewInflated(inputViewController) + verify(view).reset() + verify(viewFlipperController).reset() + verify(view).reloadColors() + } + + @Test + fun onUiModeChanged() { + val configurationListenerArgumentCaptor = + ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java) + underTest.onViewAttached() + verify(configurationController).addCallback(configurationListenerArgumentCaptor.capture()) + clearInvocations(viewFlipperController) + configurationListenerArgumentCaptor.value.onUiModeChanged() + verify(viewFlipperController).clearViews() + verify(viewFlipperController) + .asynchronouslyInflateView( + eq(SecurityMode.PIN), + any(), + onViewInflatedCallbackArgumentCaptor.capture() + ) + onViewInflatedCallbackArgumentCaptor.value.onViewInflated(inputViewController) + verify(view).reloadColors() + } + + @Test + fun hasDismissActions() { + Assert.assertFalse("Action not set yet", underTest.hasDismissActions()) + underTest.setOnDismissAction(mock(), null /* cancelAction */) + Assert.assertTrue("Action should exist", underTest.hasDismissActions()) + } + + @Test + fun willRunDismissFromKeyguardIsTrue() { + val action: OnDismissAction = mock() + whenever(action.willRunAnimationOnKeyguard()).thenReturn(true) + underTest.setOnDismissAction(action, null /* cancelAction */) + underTest.finish(false /* strongAuth */, 0 /* currentUser */) + Truth.assertThat(underTest.willRunDismissFromKeyguard()).isTrue() + } + + @Test + fun willRunDismissFromKeyguardIsFalse() { + val action: OnDismissAction = mock() + whenever(action.willRunAnimationOnKeyguard()).thenReturn(false) + underTest.setOnDismissAction(action, null /* cancelAction */) + underTest.finish(false /* strongAuth */, 0 /* currentUser */) + Truth.assertThat(underTest.willRunDismissFromKeyguard()).isFalse() + } + + @Test + fun willRunDismissFromKeyguardIsFalseWhenNoDismissActionSet() { + underTest.setOnDismissAction(null /* action */, null /* cancelAction */) + underTest.finish(false /* strongAuth */, 0 /* currentUser */) + Truth.assertThat(underTest.willRunDismissFromKeyguard()).isFalse() + } + + @Test + fun onStartingToHide() { + underTest.onStartingToHide() + verify(viewFlipperController) + .getSecurityView(any(), any(), onViewInflatedCallbackArgumentCaptor.capture()) + onViewInflatedCallbackArgumentCaptor.value.onViewInflated(inputViewController) + verify(inputViewController).onStartingToHide() + } + + @Test + fun gravityReappliedOnConfigurationChange() { + // Set initial gravity + testableResources.addOverride(R.integer.keyguard_host_view_gravity, Gravity.CENTER) + testableResources.addOverride(R.bool.can_use_one_handed_bouncer, false) + + // Kick off the initial pass... + underTest.onInit() + verify(view).layoutParams = any() + clearInvocations(view) + + // Now simulate a config change + testableResources.addOverride( + R.integer.keyguard_host_view_gravity, + Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM + ) + underTest.updateResources() + verify(view).layoutParams = any() + } + + @Test + fun gravityUsesOneHandGravityWhenApplicable() { + testableResources.addOverride(R.integer.keyguard_host_view_gravity, Gravity.CENTER) + testableResources.addOverride( + R.integer.keyguard_host_view_one_handed_gravity, + Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM + ) + + // Start disabled. + testableResources.addOverride(R.bool.can_use_one_handed_bouncer, false) + underTest.onInit() + verify(view).layoutParams = + argThat( + ArgumentMatcher { argument: FrameLayout.LayoutParams -> + argument.gravity == Gravity.CENTER + } + as ArgumentMatcher<FrameLayout.LayoutParams> + ) + clearInvocations(view) + + // And enable + testableResources.addOverride(R.bool.can_use_one_handed_bouncer, true) + underTest.updateResources() + verify(view).layoutParams = + argThat( + ArgumentMatcher { argument: FrameLayout.LayoutParams -> + argument.gravity == Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM + } + as ArgumentMatcher<FrameLayout.LayoutParams> + ) + } + + @Test + fun updateKeyguardPositionDelegatesToSecurityContainer() { + underTest.updateKeyguardPosition(1.0f) + verify(view).updatePositionByTouchX(1.0f) + } + + @Test + fun reinflateViewFlipper() { + val onViewInflatedCallback = KeyguardSecurityViewFlipperController.OnViewInflatedCallback {} + underTest.reinflateViewFlipper(onViewInflatedCallback) + verify(viewFlipperController).clearViews() + verify(viewFlipperController) + .asynchronouslyInflateView(any(), any(), eq(onViewInflatedCallback)) + } + + @Test + fun sideFpsControllerShow() { + underTest.updateSideFpsVisibility(/* isVisible= */ true) + verify(sideFpsController) + .show( + SideFpsUiRequestSource.PRIMARY_BOUNCER, + BiometricOverlayConstants.REASON_AUTH_KEYGUARD + ) + } + + @Test + fun sideFpsControllerHide() { + underTest.updateSideFpsVisibility(/* isVisible= */ false) + verify(sideFpsController).hide(SideFpsUiRequestSource.PRIMARY_BOUNCER) + } + + @Test + fun setExpansion_setsAlpha() { + underTest.setExpansion(KeyguardBouncerConstants.EXPANSION_VISIBLE) + verify(view).alpha = 1f + verify(view).translationY = 0f + } + + @Test + fun dismissesKeyguard_whenSceneChangesFromBouncerToGone() = + sceneTestUtils.testScope.runTest { + featureFlags.set(Flags.SCENE_CONTAINER, true) + + // Upon init, we have never dismisses the keyguard. + underTest.onInit() + runCurrent() + verify(viewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt()) + + // Once the view is attached, we start listening but simply going to the bouncer scene + // is + // not enough to trigger a dismissal of the keyguard. + underTest.onViewAttached() + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Bouncer, null) + ) + runCurrent() + verify(viewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt()) + + // While listening, going from the bouncer scene to the gone scene, does dismiss the + // keyguard. + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Gone, null) + ) + runCurrent() + verify(viewMediatorCallback).keyguardDone(anyBoolean(), anyInt()) + + // While listening, moving back to the bouncer scene does not dismiss the keyguard + // again. + clearInvocations(viewMediatorCallback) + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Bouncer, null) + ) + runCurrent() + verify(viewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt()) + + // Detaching the view stops listening, so moving from the bouncer scene to the gone + // scene + // does not dismiss the keyguard while we're not listening. + underTest.onViewDetached() + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Gone, null) + ) + runCurrent() + verify(viewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt()) + + // While not listening, moving back to the bouncer does not dismiss the keyguard. + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Bouncer, null) + ) + runCurrent() + verify(viewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt()) + + // Reattaching the view starts listening again so moving from the bouncer scene to the + // gone + // scene now does dismiss the keyguard again. + underTest.onViewAttached() + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Gone, null) + ) + runCurrent() + verify(viewMediatorCallback).keyguardDone(anyBoolean(), anyInt()) + } + + private val registeredSwipeListener: KeyguardSecurityContainer.SwipeListener + get() { + underTest.onViewAttached() + verify(view).setSwipeListener(swipeListenerArgumentCaptor.capture()) + return swipeListenerArgumentCaptor.value + } + + private fun setupGetSecurityView(securityMode: SecurityMode) { + underTest.showSecurityScreen(securityMode) + viewControllerImmediately + } + + private val viewControllerImmediately: Unit + get() { + verify(viewFlipperController, atLeastOnce()) + .getSecurityView(any(), any(), onViewInflatedCallbackArgumentCaptor.capture()) + @Suppress("UNCHECKED_CAST") + onViewInflatedCallbackArgumentCaptor.value.onViewInflated( + keyguardPasswordViewControllerMock as KeyguardInputViewController<KeyguardInputView> + ) + } + + companion object { + private const val TARGET_USER_ID = 100 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt index ea3289cc3836..c0c690892958 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt @@ -20,11 +20,16 @@ import android.app.admin.DevicePolicyManager import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.AuthenticationRepository +import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.scene.SceneTestUtils import com.google.common.truth.Truth.assertThat +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test @@ -47,25 +52,57 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun getAuthenticationMethod() = testScope.runTest { - assertThat(underTest.getAuthenticationMethod()) - .isEqualTo(AuthenticationMethodModel.Pin(1234)) + assertThat(underTest.getAuthenticationMethod()).isEqualTo(AuthenticationMethodModel.Pin) utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Password("password") + AuthenticationMethodModel.Password ) + + assertThat(underTest.getAuthenticationMethod()) + .isEqualTo(AuthenticationMethodModel.Password) + } + + @Test + fun getAuthenticationMethod_noneTreatedAsSwipe_whenLockscreenEnabled() = + testScope.runTest { + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) + utils.authenticationRepository.setLockscreenEnabled(true) + + assertThat(underTest.getAuthenticationMethod()) + .isEqualTo(AuthenticationMethodModel.Swipe) + } + + @Test + fun getAuthenticationMethod_none_whenLockscreenDisabled() = + testScope.runTest { + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) + utils.authenticationRepository.setLockscreenEnabled(false) + assertThat(underTest.getAuthenticationMethod()) - .isEqualTo(AuthenticationMethodModel.Password("password")) + .isEqualTo(AuthenticationMethodModel.None) } @Test - fun isUnlocked_whenAuthMethodIsNone_isTrue() = + fun isUnlocked_whenAuthMethodIsNoneAndLockscreenDisabled_isTrue() = testScope.runTest { utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) + utils.authenticationRepository.setLockscreenEnabled(false) + val isUnlocked by collectLastValue(underTest.isUnlocked) assertThat(isUnlocked).isTrue() } @Test + fun isUnlocked_whenAuthMethodIsNoneAndLockscreenEnabled_isFalse() = + testScope.runTest { + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) + utils.authenticationRepository.setLockscreenEnabled(true) + + val isUnlocked by collectLastValue(underTest.isUnlocked) + assertThat(isUnlocked).isFalse() + } + + @Test fun toggleBypassEnabled() = testScope.runTest { val isBypassEnabled by collectLastValue(underTest.isBypassEnabled) @@ -84,7 +121,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { utils.authenticationRepository.setUnlocked(false) runCurrent() utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Password("password") + AuthenticationMethodModel.Password ) assertThat(underTest.isAuthenticationRequired()).isTrue() @@ -106,7 +143,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { utils.authenticationRepository.setUnlocked(true) runCurrent() utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Password("password") + AuthenticationMethodModel.Password ) assertThat(underTest.isAuthenticationRequired()).isFalse() @@ -125,49 +162,33 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun authenticate_withCorrectPin_returnsTrue() = testScope.runTest { - val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) - + val isThrottled by collectLastValue(underTest.isThrottled) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isTrue() - assertThat(failedAttemptCount).isEqualTo(0) + assertThat(isThrottled).isFalse() } @Test fun authenticate_withIncorrectPin_returnsFalse() = testScope.runTest { - val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) - + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) assertThat(underTest.authenticate(listOf(9, 8, 7))).isFalse() - assertThat(failedAttemptCount).isEqualTo(1) } - @Test - fun authenticate_withEmptyPin_returnsFalse() = + @Test(expected = IllegalArgumentException::class) + fun authenticate_withEmptyPin_throwsException() = testScope.runTest { - val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) - - assertThat(underTest.authenticate(listOf())).isFalse() - assertThat(failedAttemptCount).isEqualTo(1) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + underTest.authenticate(listOf()) } @Test fun authenticate_withCorrectMaxLengthPin_returnsTrue() = testScope.runTest { - val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(9999999999999999) - ) - - assertThat(underTest.authenticate(List(16) { 9 })).isTrue() - assertThat(failedAttemptCount).isEqualTo(0) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + val pin = List(16) { 9 } + utils.authenticationRepository.overrideCredential(pin) + assertThat(underTest.authenticate(pin)).isTrue() } @Test @@ -179,105 +200,47 @@ class AuthenticationInteractorTest : SysuiTestCase() { // If the policy changes, there is work to do in SysUI. assertThat(DevicePolicyManager.MAX_PASSWORD_LENGTH).isLessThan(17) - val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(99999999999999999) - ) - + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) assertThat(underTest.authenticate(List(17) { 9 })).isFalse() - assertThat(failedAttemptCount).isEqualTo(1) } @Test fun authenticate_withCorrectPassword_returnsTrue() = testScope.runTest { - val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts) + val isThrottled by collectLastValue(underTest.isThrottled) utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Password("password") + AuthenticationMethodModel.Password ) assertThat(underTest.authenticate("password".toList())).isTrue() - assertThat(failedAttemptCount).isEqualTo(0) + assertThat(isThrottled).isFalse() } @Test fun authenticate_withIncorrectPassword_returnsFalse() = testScope.runTest { - val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts) utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Password("password") + AuthenticationMethodModel.Password ) assertThat(underTest.authenticate("alohomora".toList())).isFalse() - assertThat(failedAttemptCount).isEqualTo(1) } @Test fun authenticate_withCorrectPattern_returnsTrue() = testScope.runTest { - val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts) utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pattern( - listOf( - AuthenticationMethodModel.Pattern.PatternCoordinate( - x = 0, - y = 0, - ), - AuthenticationMethodModel.Pattern.PatternCoordinate( - x = 0, - y = 1, - ), - AuthenticationMethodModel.Pattern.PatternCoordinate( - x = 0, - y = 2, - ), - ) - ) + AuthenticationMethodModel.Pattern ) - assertThat( - underTest.authenticate( - listOf( - AuthenticationMethodModel.Pattern.PatternCoordinate( - x = 0, - y = 0, - ), - AuthenticationMethodModel.Pattern.PatternCoordinate( - x = 0, - y = 1, - ), - AuthenticationMethodModel.Pattern.PatternCoordinate( - x = 0, - y = 2, - ), - ) - ) - ) - .isTrue() - assertThat(failedAttemptCount).isEqualTo(0) + assertThat(underTest.authenticate(FakeAuthenticationRepository.PATTERN)).isTrue() } @Test fun authenticate_withIncorrectPattern_returnsFalse() = testScope.runTest { - val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts) utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pattern( - listOf( - AuthenticationMethodModel.Pattern.PatternCoordinate( - x = 0, - y = 0, - ), - AuthenticationMethodModel.Pattern.PatternCoordinate( - x = 0, - y = 1, - ), - AuthenticationMethodModel.Pattern.PatternCoordinate( - x = 0, - y = 2, - ), - ) - ) + AuthenticationMethodModel.Pattern ) assertThat( @@ -299,91 +262,159 @@ class AuthenticationInteractorTest : SysuiTestCase() { ) ) .isFalse() - assertThat(failedAttemptCount).isEqualTo(1) - } - - @Test - fun tryAutoConfirm_withAutoConfirmPinAndEmptyInput_returnsNull() = - testScope.runTest { - val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234, autoConfirm = true) - ) - - assertThat(underTest.authenticate(listOf(), tryAutoConfirm = true)).isNull() - assertThat(failedAttemptCount).isEqualTo(0) } @Test fun tryAutoConfirm_withAutoConfirmPinAndShorterPin_returnsNullAndHasNoEffect() = testScope.runTest { - val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234, autoConfirm = true) - ) - + val isThrottled by collectLastValue(underTest.isThrottled) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + utils.authenticationRepository.setAutoConfirmEnabled(true) assertThat(underTest.authenticate(listOf(1, 2, 3), tryAutoConfirm = true)).isNull() - assertThat(failedAttemptCount).isEqualTo(0) + assertThat(isThrottled).isFalse() } @Test fun tryAutoConfirm_withAutoConfirmWrongPinCorrectLength_returnsFalseAndDoesNotUnlockDevice() = testScope.runTest { - val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234, autoConfirm = true) - ) - + val isUnlocked by collectLastValue(underTest.isUnlocked) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + utils.authenticationRepository.setAutoConfirmEnabled(true) assertThat(underTest.authenticate(listOf(1, 2, 4, 4), tryAutoConfirm = true)).isFalse() - assertThat(failedAttemptCount).isEqualTo(1) + assertThat(isUnlocked).isFalse() } @Test fun tryAutoConfirm_withAutoConfirmLongerPin_returnsFalseAndDoesNotUnlockDevice() = testScope.runTest { - val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234, autoConfirm = true) - ) - + val isUnlocked by collectLastValue(underTest.isUnlocked) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + utils.authenticationRepository.setAutoConfirmEnabled(true) assertThat(underTest.authenticate(listOf(1, 2, 3, 4, 5), tryAutoConfirm = true)) .isFalse() - assertThat(failedAttemptCount).isEqualTo(1) + assertThat(isUnlocked).isFalse() } @Test fun tryAutoConfirm_withAutoConfirmCorrectPin_returnsTrueAndUnlocksDevice() = testScope.runTest { - val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234, autoConfirm = true) - ) - - assertThat(underTest.authenticate(listOf(1, 2, 4, 4), tryAutoConfirm = true)).isFalse() - assertThat(failedAttemptCount).isEqualTo(1) + val isUnlocked by collectLastValue(underTest.isUnlocked) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + utils.authenticationRepository.setAutoConfirmEnabled(true) + assertThat(underTest.authenticate(listOf(1, 2, 3, 4), tryAutoConfirm = true)).isTrue() + assertThat(isUnlocked).isTrue() } @Test fun tryAutoConfirm_withoutAutoConfirmButCorrectPin_returnsNullAndHasNoEffects() = testScope.runTest { - val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234, autoConfirm = false) - ) - + val isUnlocked by collectLastValue(underTest.isUnlocked) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + utils.authenticationRepository.setAutoConfirmEnabled(false) assertThat(underTest.authenticate(listOf(1, 2, 3, 4), tryAutoConfirm = true)).isNull() - assertThat(failedAttemptCount).isEqualTo(0) + assertThat(isUnlocked).isFalse() } @Test fun tryAutoConfirm_withoutCorrectPassword_returnsNullAndHasNoEffects() = testScope.runTest { - val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts) + val isUnlocked by collectLastValue(underTest.isUnlocked) utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Password("password") + AuthenticationMethodModel.Password ) assertThat(underTest.authenticate("password".toList(), tryAutoConfirm = true)).isNull() - assertThat(failedAttemptCount).isEqualTo(0) + assertThat(isUnlocked).isFalse() + } + + @Test + fun throttling() = + testScope.runTest { + val isUnlocked by collectLastValue(underTest.isUnlocked) + val throttling by collectLastValue(underTest.throttling) + val isThrottled by collectLastValue(underTest.isThrottled) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + underTest.authenticate(listOf(1, 2, 3, 4)) + assertThat(isUnlocked).isTrue() + assertThat(isThrottled).isFalse() + assertThat(throttling).isEqualTo(AuthenticationThrottlingModel()) + + utils.authenticationRepository.setUnlocked(false) + assertThat(isUnlocked).isFalse() + assertThat(isThrottled).isFalse() + assertThat(throttling).isEqualTo(AuthenticationThrottlingModel()) + + // Make many wrong attempts, but just shy of what's needed to get throttled: + repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING - 1) { + underTest.authenticate(listOf(5, 6, 7)) // Wrong PIN + assertThat(isUnlocked).isFalse() + assertThat(isThrottled).isFalse() + assertThat(throttling).isEqualTo(AuthenticationThrottlingModel()) + } + + // Make one more wrong attempt, leading to throttling: + underTest.authenticate(listOf(5, 6, 7)) // Wrong PIN + assertThat(isUnlocked).isFalse() + assertThat(isThrottled).isTrue() + assertThat(throttling) + .isEqualTo( + AuthenticationThrottlingModel( + failedAttemptCount = + FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING, + remainingMs = FakeAuthenticationRepository.THROTTLE_DURATION_MS, + ) + ) + + // Correct PIN, but throttled, so doesn't attempt it: + assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isNull() + assertThat(isUnlocked).isFalse() + assertThat(isThrottled).isTrue() + assertThat(throttling) + .isEqualTo( + AuthenticationThrottlingModel( + failedAttemptCount = + FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING, + remainingMs = FakeAuthenticationRepository.THROTTLE_DURATION_MS, + ) + ) + + // Move the clock forward to ALMOST skip the throttling, leaving one second to go: + val throttleTimeoutSec = + FakeAuthenticationRepository.THROTTLE_DURATION_MS.milliseconds.inWholeSeconds + .toInt() + repeat(throttleTimeoutSec - 1) { time -> + advanceTimeBy(1000) + assertThat(isThrottled).isTrue() + assertThat(throttling) + .isEqualTo( + AuthenticationThrottlingModel( + failedAttemptCount = + FakeAuthenticationRepository + .MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING, + remainingMs = + ((throttleTimeoutSec - (time + 1)).seconds.inWholeMilliseconds) + .toInt(), + ) + ) + } + + // Move the clock forward one more second, to completely finish the throttling period: + advanceTimeBy(1000) + assertThat(isUnlocked).isFalse() + assertThat(isThrottled).isFalse() + assertThat(throttling) + .isEqualTo( + AuthenticationThrottlingModel( + failedAttemptCount = + FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING, + remainingMs = 0, + ) + ) + + // Correct PIN and no longer throttled so unlocks successfully: + assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isTrue() + assertThat(isUnlocked).isTrue() + assertThat(isThrottled).isFalse() + assertThat(throttling).isEqualTo(AuthenticationThrottlingModel()) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt index d09353b40e66..c2219a4d82eb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt @@ -19,12 +19,16 @@ package com.android.systemui.bouncer.domain.interactor import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat +import kotlin.math.ceil +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent @@ -65,14 +69,13 @@ class BouncerInteractorTest : SysuiTestCase() { @Test fun pinAuthMethod() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene("container1")) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(underTest.message) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) - underTest.showOrUnlockDevice("container1") + underTest.showOrUnlockDevice(SceneTestUtils.CONTAINER_1) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN) @@ -98,14 +101,14 @@ class BouncerInteractorTest : SysuiTestCase() { @Test fun pinAuthMethod_tryAutoConfirm_withAutoConfirmPin() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene("container1")) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(underTest.message) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234, autoConfirm = true) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + utils.authenticationRepository.setAutoConfirmEnabled(true) utils.authenticationRepository.setUnlocked(false) - underTest.showOrUnlockDevice("container1") + underTest.showOrUnlockDevice(SceneTestUtils.CONTAINER_1) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN) underTest.clearMessage() @@ -128,14 +131,13 @@ class BouncerInteractorTest : SysuiTestCase() { @Test fun pinAuthMethod_tryAutoConfirm_withoutAutoConfirmPin() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene("container1")) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(underTest.message) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234, autoConfirm = false) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) - underTest.showOrUnlockDevice("container1") + underTest.showOrUnlockDevice(SceneTestUtils.CONTAINER_1) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.clearMessage() @@ -153,13 +155,14 @@ class BouncerInteractorTest : SysuiTestCase() { @Test fun passwordAuthMethod() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene("container1")) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(underTest.message) utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Password("password") + AuthenticationMethodModel.Password ) utils.authenticationRepository.setUnlocked(false) - underTest.showOrUnlockDevice("container1") + underTest.showOrUnlockDevice(SceneTestUtils.CONTAINER_1) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD) @@ -185,13 +188,14 @@ class BouncerInteractorTest : SysuiTestCase() { @Test fun patternAuthMethod() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene("container1")) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(underTest.message) utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pattern(emptyList()) + AuthenticationMethodModel.Pattern ) utils.authenticationRepository.setUnlocked(false) - underTest.showOrUnlockDevice("container1") + underTest.showOrUnlockDevice(SceneTestUtils.CONTAINER_1) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN) @@ -204,7 +208,7 @@ class BouncerInteractorTest : SysuiTestCase() { // Wrong input. assertThat( underTest.authenticate( - listOf(AuthenticationMethodModel.Pattern.PatternCoordinate(3, 4)) + listOf(AuthenticationMethodModel.Pattern.PatternCoordinate(1, 2)) ) ) .isFalse() @@ -215,21 +219,20 @@ class BouncerInteractorTest : SysuiTestCase() { assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN) // Correct input. - assertThat(underTest.authenticate(emptyList())).isTrue() + assertThat(underTest.authenticate(FakeAuthenticationRepository.PATTERN)).isTrue() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) } @Test fun showOrUnlockDevice_notLocked_switchesToGoneScene() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene("container1")) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(true) runCurrent() - underTest.showOrUnlockDevice("container1") + underTest.showOrUnlockDevice(SceneTestUtils.CONTAINER_1) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) } @@ -237,11 +240,12 @@ class BouncerInteractorTest : SysuiTestCase() { @Test fun showOrUnlockDevice_authMethodNotSecure_switchesToGoneScene() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene("container1")) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Swipe) utils.authenticationRepository.setUnlocked(false) - underTest.showOrUnlockDevice("container1") + underTest.showOrUnlockDevice(SceneTestUtils.CONTAINER_1) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone)) } @@ -249,15 +253,16 @@ class BouncerInteractorTest : SysuiTestCase() { @Test fun showOrUnlockDevice_customMessageShown() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene("container1")) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(underTest.message) utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Password("password") + AuthenticationMethodModel.Password ) utils.authenticationRepository.setUnlocked(false) val customMessage = "Hello there!" - underTest.showOrUnlockDevice("container1", customMessage) + underTest.showOrUnlockDevice(SceneTestUtils.CONTAINER_1, customMessage) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) assertThat(message).isEqualTo(customMessage) @@ -266,50 +271,78 @@ class BouncerInteractorTest : SysuiTestCase() { @Test fun throttling() = testScope.runTest { + val isThrottled by collectLastValue(underTest.isThrottled) val throttling by collectLastValue(underTest.throttling) val message by collectLastValue(underTest.message) val currentScene by collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) runCurrent() underTest.showOrUnlockDevice(SceneTestUtils.CONTAINER_1) runCurrent() assertThat(currentScene?.key).isEqualTo(SceneKey.Bouncer) - assertThat(throttling).isNull() + assertThat(isThrottled).isFalse() + assertThat(throttling).isEqualTo(AuthenticationThrottlingModel()) assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN) - repeat(BouncerInteractor.THROTTLE_EVERY) { times -> + repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING) { times -> // Wrong PIN. assertThat(underTest.authenticate(listOf(6, 7, 8, 9))).isFalse() - if (times < BouncerInteractor.THROTTLE_EVERY - 1) { + if ( + times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING - 1 + ) { assertThat(message).isEqualTo(MESSAGE_WRONG_PIN) } } - assertThat(throttling).isNotNull() - assertTryAgainMessage(message, BouncerInteractor.THROTTLE_DURATION_SEC) + assertThat(isThrottled).isTrue() + assertThat(throttling) + .isEqualTo( + AuthenticationThrottlingModel( + failedAttemptCount = + FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING, + remainingMs = FakeAuthenticationRepository.THROTTLE_DURATION_MS, + ) + ) + assertTryAgainMessage( + message, + FakeAuthenticationRepository.THROTTLE_DURATION_MS.milliseconds.inWholeSeconds + .toInt() + ) // Correct PIN, but throttled, so doesn't change away from the bouncer scene: - assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isFalse() + assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isNull() assertThat(currentScene?.key).isEqualTo(SceneKey.Bouncer) - assertTryAgainMessage(message, BouncerInteractor.THROTTLE_DURATION_SEC) + assertTryAgainMessage( + message, + FakeAuthenticationRepository.THROTTLE_DURATION_MS.milliseconds.inWholeSeconds + .toInt() + ) - throttling?.totalDurationSec?.let { seconds -> + throttling?.remainingMs?.let { remainingMs -> + val seconds = ceil(remainingMs / 1000f).toInt() repeat(seconds) { time -> advanceTimeBy(1000) - val remainingTime = seconds - time - 1 - if (remainingTime > 0) { - assertTryAgainMessage(message, remainingTime) + val remainingTimeSec = seconds - time - 1 + if (remainingTimeSec > 0) { + assertTryAgainMessage(message, remainingTimeSec) } } } assertThat(message).isEqualTo("") - assertThat(throttling).isNull() + assertThat(isThrottled).isFalse() + assertThat(throttling) + .isEqualTo( + AuthenticationThrottlingModel( + failedAttemptCount = + FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING, + ) + ) assertThat(currentScene?.key).isEqualTo(SceneKey.Bouncer) // Correct PIN and no longer throttled so changes to the Gone scene: assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isTrue() assertThat(currentScene?.key).isEqualTo(SceneKey.Gone) + assertThat(isThrottled).isFalse() + assertThat(throttling).isEqualTo(AuthenticationThrottlingModel()) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt index f811ce05e314..0867558545bd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt @@ -55,10 +55,8 @@ class AuthMethodBouncerViewModelTest : SysuiTestCase() { @Test fun animateFailure() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) val animateFailure by collectLastValue(underTest.animateFailure) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) assertThat(animateFailure).isFalse() // Wrong PIN: diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt index 5ffc47119e17..0356036fa78f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt @@ -18,8 +18,8 @@ package com.android.systemui.bouncer.ui.viewmodel import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel -import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.scene.SceneTestUtils import com.google.common.truth.Truth.assertThat @@ -110,18 +110,16 @@ class BouncerViewModelTest : SysuiTestCase() { testScope.runTest { val message by collectLastValue(underTest.message) val throttling by collectLastValue(bouncerInteractor.throttling) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) assertThat(message?.isUpdateAnimated).isTrue() - repeat(BouncerInteractor.THROTTLE_EVERY) { + repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING) { // Wrong PIN. bouncerInteractor.authenticate(listOf(3, 4, 5, 6)) } assertThat(message?.isUpdateAnimated).isFalse() - throttling?.totalDurationSec?.let { seconds -> advanceTimeBy(seconds * 1000L) } + throttling?.remainingMs?.let { remainingMs -> advanceTimeBy(remainingMs.toLong()) } assertThat(message?.isUpdateAnimated).isTrue() } @@ -135,18 +133,16 @@ class BouncerViewModelTest : SysuiTestCase() { } ) val throttling by collectLastValue(bouncerInteractor.throttling) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) assertThat(isInputEnabled).isTrue() - repeat(BouncerInteractor.THROTTLE_EVERY) { + repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING) { // Wrong PIN. bouncerInteractor.authenticate(listOf(3, 4, 5, 6)) } assertThat(isInputEnabled).isFalse() - throttling?.totalDurationSec?.let { seconds -> advanceTimeBy(seconds * 1000L) } + throttling?.remainingMs?.let { milliseconds -> advanceTimeBy(milliseconds.toLong()) } assertThat(isInputEnabled).isTrue() } @@ -154,11 +150,9 @@ class BouncerViewModelTest : SysuiTestCase() { fun throttlingDialogMessage() = testScope.runTest { val throttlingDialogMessage by collectLastValue(underTest.throttlingDialogMessage) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - repeat(BouncerInteractor.THROTTLE_EVERY) { + repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING) { // Wrong PIN. assertThat(throttlingDialogMessage).isNull() bouncerInteractor.authenticate(listOf(3, 4, 5, 6)) @@ -173,11 +167,9 @@ class BouncerViewModelTest : SysuiTestCase() { return listOf( AuthenticationMethodModel.None, AuthenticationMethodModel.Swipe, - AuthenticationMethodModel.Pin(1234), - AuthenticationMethodModel.Password("password"), - AuthenticationMethodModel.Pattern( - listOf(AuthenticationMethodModel.Pattern.PatternCoordinate(1, 1)) - ), + AuthenticationMethodModel.Pin, + AuthenticationMethodModel.Password, + AuthenticationMethodModel.Pattern, ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt index 699571b1537c..b1533fecbc5e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt @@ -72,14 +72,18 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { @Test fun onShown() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Password("password") + AuthenticationMethodModel.Password ) utils.authenticationRepository.setUnlocked(false) - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Bouncer) + ) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() @@ -92,14 +96,18 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { @Test fun onPasswordInputChanged() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Password("password") + AuthenticationMethodModel.Password ) utils.authenticationRepository.setUnlocked(false) - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Bouncer) + ) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() runCurrent() @@ -114,12 +122,16 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { @Test fun onAuthenticateKeyPressed_whenCorrect() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Password("password") + AuthenticationMethodModel.Password ) utils.authenticationRepository.setUnlocked(false) - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Bouncer) + ) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() underTest.onPasswordInputChanged("password") @@ -132,14 +144,18 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { @Test fun onAuthenticateKeyPressed_whenWrong() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Password("password") + AuthenticationMethodModel.Password ) utils.authenticationRepository.setUnlocked(false) - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Bouncer) + ) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() underTest.onPasswordInputChanged("wrong") @@ -154,14 +170,18 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { @Test fun onAuthenticateKeyPressed_correctAfterWrong() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Password("password") + AuthenticationMethodModel.Password ) utils.authenticationRepository.setUnlocked(false) - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Bouncer) + ) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() underTest.onPasswordInputChanged("wrong") @@ -180,7 +200,6 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { } companion object { - private const val CONTAINER_NAME = "container1" private const val ENTER_YOUR_PASSWORD = "Enter your password" private const val WRONG_PASSWORD = "Wrong password" } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt index 9a1f584bced9..f69cbb8fd004 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.bouncer.ui.viewmodel import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.scene.SceneTestUtils @@ -74,15 +75,19 @@ class PatternBouncerViewModelTest : SysuiTestCase() { @Test fun onShown() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(bouncerViewModel.message) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pattern(CORRECT_PATTERN) + AuthenticationMethodModel.Pattern ) utils.authenticationRepository.setUnlocked(false) - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Bouncer) + ) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() @@ -96,15 +101,19 @@ class PatternBouncerViewModelTest : SysuiTestCase() { @Test fun onDragStart() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(bouncerViewModel.message) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pattern(CORRECT_PATTERN) + AuthenticationMethodModel.Pattern ) utils.authenticationRepository.setUnlocked(false) - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Bouncer) + ) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() runCurrent() @@ -120,14 +129,18 @@ class PatternBouncerViewModelTest : SysuiTestCase() { @Test fun onDragEnd_whenCorrect() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pattern(CORRECT_PATTERN) + AuthenticationMethodModel.Pattern ) utils.authenticationRepository.setUnlocked(false) - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Bouncer) + ) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() underTest.onDragStart() @@ -167,15 +180,19 @@ class PatternBouncerViewModelTest : SysuiTestCase() { @Test fun onDragEnd_whenWrong() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(bouncerViewModel.message) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pattern(CORRECT_PATTERN) + AuthenticationMethodModel.Pattern ) utils.authenticationRepository.setUnlocked(false) - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Bouncer) + ) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() underTest.onDragStart() @@ -199,15 +216,19 @@ class PatternBouncerViewModelTest : SysuiTestCase() { @Test fun onDragEnd_correctAfterWrong() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(bouncerViewModel.message) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pattern(CORRECT_PATTERN) + AuthenticationMethodModel.Pattern ) utils.authenticationRepository.setUnlocked(false) - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Bouncer) + ) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() underTest.onDragStart() @@ -241,20 +262,8 @@ class PatternBouncerViewModelTest : SysuiTestCase() { } companion object { - private const val CONTAINER_NAME = "container1" private const val ENTER_YOUR_PATTERN = "Enter your pattern" private const val WRONG_PATTERN = "Wrong pattern" - private val CORRECT_PATTERN = - listOf( - AuthenticationMethodModel.Pattern.PatternCoordinate(x = 1, y = 1), - AuthenticationMethodModel.Pattern.PatternCoordinate(x = 0, y = 1), - AuthenticationMethodModel.Pattern.PatternCoordinate(x = 0, y = 0), - AuthenticationMethodModel.Pattern.PatternCoordinate(x = 1, y = 0), - AuthenticationMethodModel.Pattern.PatternCoordinate(x = 2, y = 0), - AuthenticationMethodModel.Pattern.PatternCoordinate(x = 2, y = 1), - AuthenticationMethodModel.Pattern.PatternCoordinate(x = 2, y = 2), - AuthenticationMethodModel.Pattern.PatternCoordinate(x = 1, y = 2), - AuthenticationMethodModel.Pattern.PatternCoordinate(x = 0, y = 2), - ) + private val CORRECT_PATTERN = FakeAuthenticationRepository.PATTERN } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt index 608a187a1bc9..7d6c4a1a1455 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt @@ -63,7 +63,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { return bouncerInteractor } }, - containerName = CONTAINER_NAME, + containerName = SceneTestUtils.CONTAINER_1, ) private val underTest = PinBouncerViewModel( @@ -82,11 +82,15 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onShown() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(bouncerViewModel.message) val entries by collectLastValue(underTest.pinEntries) utils.authenticationRepository.setUnlocked(false) - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Bouncer) + ) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() @@ -99,14 +103,16 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onPinButtonClicked() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(bouncerViewModel.message) val entries by collectLastValue(underTest.pinEntries) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Bouncer) + ) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() runCurrent() @@ -122,14 +128,16 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onBackspaceButtonClicked() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(bouncerViewModel.message) val entries by collectLastValue(underTest.pinEntries) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Bouncer) + ) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() runCurrent() @@ -146,13 +154,15 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onPinEdit() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val entries by collectLastValue(underTest.pinEntries) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Bouncer) + ) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() @@ -172,14 +182,16 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onBackspaceButtonLongPressed() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(bouncerViewModel.message) val entries by collectLastValue(underTest.pinEntries) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Bouncer) + ) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() runCurrent() @@ -198,12 +210,14 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onAuthenticateButtonClicked_whenCorrect() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Bouncer) + ) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() underTest.onPinButtonClicked(1) @@ -219,14 +233,16 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onAuthenticateButtonClicked_whenWrong() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(bouncerViewModel.message) val entries by collectLastValue(underTest.pinEntries) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Bouncer) + ) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() underTest.onPinButtonClicked(1) @@ -245,14 +261,16 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onAuthenticateButtonClicked_correctAfterWrong() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(bouncerViewModel.message) val entries by collectLastValue(underTest.pinEntries) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Bouncer) + ) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() underTest.onPinButtonClicked(1) @@ -280,12 +298,15 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onAutoConfirm_whenCorrect() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234, autoConfirm = true) - ) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + utils.authenticationRepository.setAutoConfirmEnabled(true) + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Bouncer) + ) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() underTest.onPinButtonClicked(1) @@ -299,14 +320,17 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onAutoConfirm_whenWrong() = testScope.runTest { - val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME)) + val currentScene by + collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(bouncerViewModel.message) val entries by collectLastValue(underTest.pinEntries) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234, autoConfirm = true) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) - sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Bouncer)) + utils.authenticationRepository.setAutoConfirmEnabled(true) + sceneInteractor.setCurrentScene( + SceneTestUtils.CONTAINER_1, + SceneModel(SceneKey.Bouncer) + ) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) underTest.onShown() underTest.onPinButtonClicked(1) @@ -324,9 +348,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { testScope.runTest { val backspaceButtonAppearance by collectLastValue(underTest.backspaceButtonAppearance) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234, autoConfirm = false) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) assertThat(backspaceButtonAppearance).isEqualTo(ActionButtonAppearance.Shown) } @@ -335,9 +357,8 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun backspaceButtonAppearance_withAutoConfirmButNoInput_isHidden() = testScope.runTest { val backspaceButtonAppearance by collectLastValue(underTest.backspaceButtonAppearance) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234, autoConfirm = true) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + utils.authenticationRepository.setAutoConfirmEnabled(true) assertThat(backspaceButtonAppearance).isEqualTo(ActionButtonAppearance.Hidden) } @@ -346,9 +367,8 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun backspaceButtonAppearance_withAutoConfirmAndInput_isShownQuiet() = testScope.runTest { val backspaceButtonAppearance by collectLastValue(underTest.backspaceButtonAppearance) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234, autoConfirm = true) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + utils.authenticationRepository.setAutoConfirmEnabled(true) underTest.onPinButtonClicked(1) @@ -360,9 +380,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { testScope.runTest { val confirmButtonAppearance by collectLastValue(underTest.confirmButtonAppearance) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234, autoConfirm = false) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) assertThat(confirmButtonAppearance).isEqualTo(ActionButtonAppearance.Shown) } @@ -371,9 +389,8 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun confirmButtonAppearance_withAutoConfirm_isHidden() = testScope.runTest { val confirmButtonAppearance by collectLastValue(underTest.confirmButtonAppearance) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234, autoConfirm = true) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + utils.authenticationRepository.setAutoConfirmEnabled(true) assertThat(confirmButtonAppearance).isEqualTo(ActionButtonAppearance.Hidden) } @@ -382,9 +399,8 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun hintedPinLength_withoutAutoConfirm_isNull() = testScope.runTest { val hintedPinLength by collectLastValue(underTest.hintedPinLength) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234, autoConfirm = false) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + utils.authenticationRepository.setAutoConfirmEnabled(false) assertThat(hintedPinLength).isNull() } @@ -393,9 +409,8 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun hintedPinLength_withAutoConfirmPinLessThanSixDigits_isNull() = testScope.runTest { val hintedPinLength by collectLastValue(underTest.hintedPinLength) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(12345, autoConfirm = true) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + utils.authenticationRepository.setAutoConfirmEnabled(true) assertThat(hintedPinLength).isNull() } @@ -404,9 +419,9 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun hintedPinLength_withAutoConfirmPinExactlySixDigits_isSix() = testScope.runTest { val hintedPinLength by collectLastValue(underTest.hintedPinLength) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(123456, autoConfirm = true) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + utils.authenticationRepository.setAutoConfirmEnabled(true) + utils.authenticationRepository.overrideCredential(listOf(1, 2, 3, 4, 5, 6)) assertThat(hintedPinLength).isEqualTo(6) } @@ -415,15 +430,13 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun hintedPinLength_withAutoConfirmPinMoreThanSixDigits_isNull() = testScope.runTest { val hintedPinLength by collectLastValue(underTest.hintedPinLength) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234567, autoConfirm = true) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + utils.authenticationRepository.setAutoConfirmEnabled(true) assertThat(hintedPinLength).isNull() } companion object { - private const val CONTAINER_NAME = "container1" private const val ENTER_YOUR_PIN = "Enter your pin" private const val WRONG_PIN = "Wrong pin" } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractorTest.kt index abbdc3d55ac6..8b36284b8620 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractorTest.kt @@ -94,9 +94,7 @@ class LockscreenSceneInteractorTest : SysuiTestCase() { testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) utils.authenticationRepository.setUnlocked(false) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) underTest.dismissLockscreen() @@ -109,9 +107,7 @@ class LockscreenSceneInteractorTest : SysuiTestCase() { testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) utils.authenticationRepository.setUnlocked(true) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) underTest.dismissLockscreen() @@ -153,9 +149,7 @@ class LockscreenSceneInteractorTest : SysuiTestCase() { testScope.runTest { val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked) sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Lockscreen)) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) assertThat(isUnlocked).isFalse() sceneInteractor.setCurrentScene(CONTAINER_1, SceneModel(SceneKey.Gone)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt index ff4ec4b738bd..161dd660671e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt @@ -73,7 +73,7 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { testScope.runTest { val lockButtonIcon by collectLastValue(underTest.lockButtonIcon) utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Password("password") + AuthenticationMethodModel.Password ) utils.authenticationRepository.setUnlocked(false) @@ -86,7 +86,7 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { testScope.runTest { val lockButtonIcon by collectLastValue(underTest.lockButtonIcon) utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Password("password") + AuthenticationMethodModel.Password ) utils.authenticationRepository.setUnlocked(true) @@ -108,9 +108,7 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { fun upTransitionSceneKey_swipeToUnlockedNotEnabled_bouncer() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Bouncer) @@ -120,9 +118,7 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { fun onLockButtonClicked_deviceLockedSecurely_switchesToBouncer() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) runCurrent() @@ -135,9 +131,7 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { fun onContentClicked_deviceUnlocked_switchesToGone() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(true) runCurrent() @@ -150,9 +144,7 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { fun onContentClicked_deviceLockedSecurely_switchesToBouncer() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) runCurrent() @@ -165,9 +157,7 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { fun onLockButtonClicked_deviceUnlocked_switchesToGone() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(true) runCurrent() diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt index c85c8baf7ccb..4eedc99839c6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt @@ -69,9 +69,7 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { fun onContentClicked_deviceUnlocked_switchesToGone() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(true) runCurrent() @@ -84,9 +82,7 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { fun onContentClicked_deviceLockedSecurely_switchesToBouncer() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) runCurrent() diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt index de15c7711bb3..9ce378dd079f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt @@ -24,6 +24,7 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.SceneTransitionModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -40,7 +41,7 @@ class SceneContainerRepositoryTest : SysuiTestCase() { @Test fun allSceneKeys() { val underTest = utils.fakeSceneContainerRepository() - assertThat(underTest.allSceneKeys("container1")) + assertThat(underTest.allSceneKeys(SceneTestUtils.CONTAINER_1)) .isEqualTo( listOf( SceneKey.QuickSettings, @@ -61,10 +62,10 @@ class SceneContainerRepositoryTest : SysuiTestCase() { @Test fun currentScene() = runTest { val underTest = utils.fakeSceneContainerRepository() - val currentScene by collectLastValue(underTest.currentScene("container1")) + val currentScene by collectLastValue(underTest.currentScene(SceneTestUtils.CONTAINER_1)) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) - underTest.setCurrentScene("container1", SceneModel(SceneKey.Shade)) + underTest.setCurrentScene(SceneTestUtils.CONTAINER_1, SceneModel(SceneKey.Shade)) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Shade)) } @@ -85,26 +86,26 @@ class SceneContainerRepositoryTest : SysuiTestCase() { val underTest = utils.fakeSceneContainerRepository( setOf( - utils.fakeSceneContainerConfig("container1"), + utils.fakeSceneContainerConfig(SceneTestUtils.CONTAINER_1), utils.fakeSceneContainerConfig( - "container2", + SceneTestUtils.CONTAINER_2, listOf(SceneKey.QuickSettings, SceneKey.Lockscreen) ), ) ) - underTest.setCurrentScene("container2", SceneModel(SceneKey.Shade)) + underTest.setCurrentScene(SceneTestUtils.CONTAINER_2, SceneModel(SceneKey.Shade)) } @Test fun isVisible() = runTest { val underTest = utils.fakeSceneContainerRepository() - val isVisible by collectLastValue(underTest.isVisible("container1")) + val isVisible by collectLastValue(underTest.isVisible(SceneTestUtils.CONTAINER_1)) assertThat(isVisible).isTrue() - underTest.setVisible("container1", false) + underTest.setVisible(SceneTestUtils.CONTAINER_1, false) assertThat(isVisible).isFalse() - underTest.setVisible("container1", true) + underTest.setVisible(SceneTestUtils.CONTAINER_1, true) assertThat(isVisible).isTrue() } @@ -124,13 +125,13 @@ class SceneContainerRepositoryTest : SysuiTestCase() { fun sceneTransitionProgress() = runTest { val underTest = utils.fakeSceneContainerRepository() val sceneTransitionProgress by - collectLastValue(underTest.sceneTransitionProgress("container1")) + collectLastValue(underTest.sceneTransitionProgress(SceneTestUtils.CONTAINER_1)) assertThat(sceneTransitionProgress).isEqualTo(1f) - underTest.setSceneTransitionProgress("container1", 0.1f) + underTest.setSceneTransitionProgress(SceneTestUtils.CONTAINER_1, 0.1f) assertThat(sceneTransitionProgress).isEqualTo(0.1f) - underTest.setSceneTransitionProgress("container1", 0.9f) + underTest.setSceneTransitionProgress(SceneTestUtils.CONTAINER_1, 0.9f) assertThat(sceneTransitionProgress).isEqualTo(0.9f) } @@ -139,4 +140,75 @@ class SceneContainerRepositoryTest : SysuiTestCase() { val underTest = utils.fakeSceneContainerRepository() underTest.sceneTransitionProgress("nonExistingContainer") } + + @Test + fun setSceneTransition() = runTest { + val underTest = + utils.fakeSceneContainerRepository( + setOf( + utils.fakeSceneContainerConfig(SceneTestUtils.CONTAINER_1), + utils.fakeSceneContainerConfig( + SceneTestUtils.CONTAINER_2, + listOf(SceneKey.QuickSettings, SceneKey.Lockscreen) + ), + ) + ) + val sceneTransition by + collectLastValue(underTest.sceneTransitions(SceneTestUtils.CONTAINER_2)) + assertThat(sceneTransition).isNull() + + underTest.setSceneTransition( + SceneTestUtils.CONTAINER_2, + SceneKey.Lockscreen, + SceneKey.QuickSettings + ) + assertThat(sceneTransition) + .isEqualTo( + SceneTransitionModel(from = SceneKey.Lockscreen, to = SceneKey.QuickSettings) + ) + } + + @Test(expected = IllegalStateException::class) + fun setSceneTransition_noSuchContainer_throws() { + val underTest = utils.fakeSceneContainerRepository() + underTest.setSceneTransition("nonExistingContainer", SceneKey.Lockscreen, SceneKey.Shade) + } + + @Test(expected = IllegalStateException::class) + fun setSceneTransition_noFromSceneInContainer_throws() { + val underTest = + utils.fakeSceneContainerRepository( + setOf( + utils.fakeSceneContainerConfig(SceneTestUtils.CONTAINER_1), + utils.fakeSceneContainerConfig( + SceneTestUtils.CONTAINER_2, + listOf(SceneKey.QuickSettings, SceneKey.Lockscreen) + ), + ) + ) + underTest.setSceneTransition( + SceneTestUtils.CONTAINER_2, + SceneKey.Shade, + SceneKey.Lockscreen + ) + } + + @Test(expected = IllegalStateException::class) + fun setSceneTransition_noToSceneInContainer_throws() { + val underTest = + utils.fakeSceneContainerRepository( + setOf( + utils.fakeSceneContainerConfig(SceneTestUtils.CONTAINER_1), + utils.fakeSceneContainerConfig( + SceneTestUtils.CONTAINER_2, + listOf(SceneKey.QuickSettings, SceneKey.Lockscreen) + ), + ) + ) + underTest.setSceneTransition( + SceneTestUtils.CONTAINER_2, + SceneKey.Shade, + SceneKey.Lockscreen + ) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index ee4f6c23ca8a..3050c4edd24f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -24,6 +24,7 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.scene.shared.model.SceneTransitionModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -40,36 +41,63 @@ class SceneInteractorTest : SysuiTestCase() { @Test fun allSceneKeys() { - assertThat(underTest.allSceneKeys("container1")).isEqualTo(utils.fakeSceneKeys()) + assertThat(underTest.allSceneKeys(SceneTestUtils.CONTAINER_1)) + .isEqualTo(utils.fakeSceneKeys()) } @Test - fun sceneTransitions() = runTest { - val currentScene by collectLastValue(underTest.currentScene("container1")) + fun currentScene() = runTest { + val currentScene by collectLastValue(underTest.currentScene(SceneTestUtils.CONTAINER_1)) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) - underTest.setCurrentScene("container1", SceneModel(SceneKey.Shade)) + underTest.setCurrentScene(SceneTestUtils.CONTAINER_1, SceneModel(SceneKey.Shade)) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Shade)) } @Test fun sceneTransitionProgress() = runTest { - val progress by collectLastValue(underTest.sceneTransitionProgress("container1")) + val progress by + collectLastValue(underTest.sceneTransitionProgress(SceneTestUtils.CONTAINER_1)) assertThat(progress).isEqualTo(1f) - underTest.setSceneTransitionProgress("container1", 0.55f) + underTest.setSceneTransitionProgress(SceneTestUtils.CONTAINER_1, 0.55f) assertThat(progress).isEqualTo(0.55f) } @Test fun isVisible() = runTest { - val isVisible by collectLastValue(underTest.isVisible("container1")) + val isVisible by collectLastValue(underTest.isVisible(SceneTestUtils.CONTAINER_1)) assertThat(isVisible).isTrue() - underTest.setVisible("container1", false) + underTest.setVisible(SceneTestUtils.CONTAINER_1, false) assertThat(isVisible).isFalse() - underTest.setVisible("container1", true) + underTest.setVisible(SceneTestUtils.CONTAINER_1, true) assertThat(isVisible).isTrue() } + + @Test + fun sceneTransitions() = runTest { + val transitions by collectLastValue(underTest.sceneTransitions(SceneTestUtils.CONTAINER_1)) + assertThat(transitions).isNull() + + val initialSceneKey = underTest.currentScene(SceneTestUtils.CONTAINER_1).value.key + underTest.setCurrentScene(SceneTestUtils.CONTAINER_1, SceneModel(SceneKey.Shade)) + assertThat(transitions) + .isEqualTo( + SceneTransitionModel( + from = initialSceneKey, + to = SceneKey.Shade, + ) + ) + + underTest.setCurrentScene(SceneTestUtils.CONTAINER_1, SceneModel(SceneKey.QuickSettings)) + assertThat(transitions) + .isEqualTo( + SceneTransitionModel( + from = SceneKey.Shade, + to = SceneKey.QuickSettings, + ) + ) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt index cd2f5af592cf..6882be7fe184 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt @@ -40,7 +40,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { private val underTest = SceneContainerViewModel( interactor = interactor, - containerName = "container1", + containerName = SceneTestUtils.CONTAINER_1, ) @Test @@ -48,10 +48,10 @@ class SceneContainerViewModelTest : SysuiTestCase() { val isVisible by collectLastValue(underTest.isVisible) assertThat(isVisible).isTrue() - interactor.setVisible("container1", false) + interactor.setVisible(SceneTestUtils.CONTAINER_1, false) assertThat(isVisible).isFalse() - interactor.setVisible("container1", true) + interactor.setVisible(SceneTestUtils.CONTAINER_1, true) assertThat(isVisible).isTrue() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt index 5d2d192bb61a..309ab058fb64 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt @@ -70,9 +70,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() { fun upTransitionSceneKey_deviceLocked_lockScreen() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Lockscreen) @@ -82,9 +80,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() { fun upTransitionSceneKey_deviceUnlocked_gone() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(true) assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone) @@ -94,9 +90,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() { fun onContentClicked_deviceUnlocked_switchesToGone() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(true) runCurrent() @@ -109,9 +103,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() { fun onContentClicked_deviceLockedSecurely_switchesToBouncer() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_1)) - utils.authenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin(1234) - ) + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) runCurrent() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt index a718f70b235d..bebdd4004c3d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt @@ -16,40 +16,154 @@ package com.android.systemui.authentication.data.repository +import com.android.internal.widget.LockPatternUtils +import com.android.internal.widget.LockPatternView +import com.android.internal.widget.LockscreenCredential import com.android.keyguard.KeyguardSecurityModel.SecurityMode import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.authentication.shared.model.AuthenticationResultModel +import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow class FakeAuthenticationRepository( - private val delegate: AuthenticationRepository, - private val onSecurityModeChanged: (SecurityMode) -> Unit, -) : AuthenticationRepository by delegate { + private val currentTime: () -> Long, +) : AuthenticationRepository { + + private val _isBypassEnabled = MutableStateFlow(false) + override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled + + private val _isAutoConfirmEnabled = MutableStateFlow(false) + override val isAutoConfirmEnabled: StateFlow<Boolean> = _isAutoConfirmEnabled.asStateFlow() private val _isUnlocked = MutableStateFlow(false) override val isUnlocked: StateFlow<Boolean> = _isUnlocked.asStateFlow() - private var authenticationMethod: AuthenticationMethodModel = DEFAULT_AUTHENTICATION_METHOD + override val hintedPinLength: Int = 6 + + private val _isPatternVisible = MutableStateFlow(true) + override val isPatternVisible: StateFlow<Boolean> = _isPatternVisible.asStateFlow() + + private val _throttling = MutableStateFlow(AuthenticationThrottlingModel()) + override val throttling: StateFlow<AuthenticationThrottlingModel> = _throttling.asStateFlow() + + private val _authenticationMethod = + MutableStateFlow<AuthenticationMethodModel>(DEFAULT_AUTHENTICATION_METHOD) + val authenticationMethod: StateFlow<AuthenticationMethodModel> = + _authenticationMethod.asStateFlow() + + private var isLockscreenEnabled = true + private var failedAttemptCount = 0 + private var throttlingEndTimestamp = 0L + private var credentialOverride: List<Any>? = null + private var securityMode: SecurityMode = DEFAULT_AUTHENTICATION_METHOD.toSecurityMode() override suspend fun getAuthenticationMethod(): AuthenticationMethodModel { - return authenticationMethod + return authenticationMethod.value } fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) { - this.authenticationMethod = authenticationMethod - onSecurityModeChanged(authenticationMethod.toSecurityMode()) + _authenticationMethod.value = authenticationMethod + securityMode = authenticationMethod.toSecurityMode() + } + + fun overrideCredential(pin: List<Int>) { + credentialOverride = pin + } + + override suspend fun isLockscreenEnabled(): Boolean { + return isLockscreenEnabled + } + + override suspend fun reportAuthenticationAttempt(isSuccessful: Boolean) { + failedAttemptCount = if (isSuccessful) 0 else failedAttemptCount + 1 + _isUnlocked.value = isSuccessful + } + + override suspend fun getPinLength(): Int { + return (credentialOverride ?: listOf(1, 2, 3, 4)).size + } + + override fun setBypassEnabled(isBypassEnabled: Boolean) { + _isBypassEnabled.value = isBypassEnabled + } + + override suspend fun getFailedAuthenticationAttemptCount(): Int { + return failedAttemptCount + } + + override suspend fun getThrottlingEndTimestamp(): Long { + return throttlingEndTimestamp + } + + override fun setThrottling(throttlingModel: AuthenticationThrottlingModel) { + _throttling.value = throttlingModel } fun setUnlocked(isUnlocked: Boolean) { _isUnlocked.value = isUnlocked } + fun setAutoConfirmEnabled(isEnabled: Boolean) { + _isAutoConfirmEnabled.value = isEnabled + } + + fun setLockscreenEnabled(isLockscreenEnabled: Boolean) { + this.isLockscreenEnabled = isLockscreenEnabled + } + + override suspend fun setThrottleDuration(durationMs: Int) { + throttlingEndTimestamp = if (durationMs > 0) currentTime() + durationMs else 0 + } + + override suspend fun checkCredential( + credential: LockscreenCredential + ): AuthenticationResultModel { + val expectedCredential = credentialOverride ?: getExpectedCredential(securityMode) + val isSuccessful = + when { + credential.type != getCurrentCredentialType(securityMode) -> false + credential.type == LockPatternUtils.CREDENTIAL_TYPE_PIN -> + credential.isPin && credential.matches(expectedCredential) + credential.type == LockPatternUtils.CREDENTIAL_TYPE_PASSWORD -> + credential.isPassword && credential.matches(expectedCredential) + credential.type == LockPatternUtils.CREDENTIAL_TYPE_PATTERN -> + credential.isPattern && credential.matches(expectedCredential) + else -> error("Unexpected credential type ${credential.type}!") + } + + return if ( + isSuccessful || failedAttemptCount < MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING - 1 + ) { + AuthenticationResultModel( + isSuccessful = isSuccessful, + throttleDurationMs = 0, + ) + } else { + AuthenticationResultModel( + isSuccessful = false, + throttleDurationMs = THROTTLE_DURATION_MS, + ) + } + } + companion object { - val DEFAULT_AUTHENTICATION_METHOD = - AuthenticationMethodModel.Pin(listOf(1, 2, 3, 4), autoConfirm = false) + val DEFAULT_AUTHENTICATION_METHOD = AuthenticationMethodModel.Pin + val PATTERN = + listOf( + AuthenticationMethodModel.Pattern.PatternCoordinate(2, 0), + AuthenticationMethodModel.Pattern.PatternCoordinate(2, 1), + AuthenticationMethodModel.Pattern.PatternCoordinate(2, 2), + AuthenticationMethodModel.Pattern.PatternCoordinate(1, 1), + AuthenticationMethodModel.Pattern.PatternCoordinate(0, 0), + AuthenticationMethodModel.Pattern.PatternCoordinate(0, 1), + AuthenticationMethodModel.Pattern.PatternCoordinate(0, 2), + ) + const val MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING = 5 + const val THROTTLE_DURATION_MS = 30000 - fun AuthenticationMethodModel.toSecurityMode(): SecurityMode { + private fun AuthenticationMethodModel.toSecurityMode(): SecurityMode { return when (this) { is AuthenticationMethodModel.Pin -> SecurityMode.PIN is AuthenticationMethodModel.Password -> SecurityMode.Password @@ -58,5 +172,50 @@ class FakeAuthenticationRepository( is AuthenticationMethodModel.None -> SecurityMode.None } } + + @LockPatternUtils.CredentialType + private fun getCurrentCredentialType( + securityMode: SecurityMode, + ): Int { + return when (securityMode) { + SecurityMode.PIN, + SecurityMode.SimPin, + SecurityMode.SimPuk -> LockPatternUtils.CREDENTIAL_TYPE_PIN + SecurityMode.Password -> LockPatternUtils.CREDENTIAL_TYPE_PASSWORD + SecurityMode.Pattern -> LockPatternUtils.CREDENTIAL_TYPE_PATTERN + SecurityMode.None -> LockPatternUtils.CREDENTIAL_TYPE_NONE + else -> error("Unsupported SecurityMode $securityMode!") + } + } + + private fun getExpectedCredential(securityMode: SecurityMode): List<Any> { + return when (val credentialType = getCurrentCredentialType(securityMode)) { + LockPatternUtils.CREDENTIAL_TYPE_PIN -> listOf(1, 2, 3, 4) + LockPatternUtils.CREDENTIAL_TYPE_PASSWORD -> "password".toList() + LockPatternUtils.CREDENTIAL_TYPE_PATTERN -> PATTERN.toCells() + else -> error("Unsupported credential type $credentialType!") + } + } + + private fun LockscreenCredential.matches(expectedCredential: List<Any>): Boolean { + @Suppress("UNCHECKED_CAST") + return when { + isPin -> + credential.map { byte -> byte.toInt().toChar() - '0' } == expectedCredential + isPassword -> credential.map { byte -> byte.toInt().toChar() } == expectedCredential + isPattern -> + credential.contentEquals( + LockPatternUtils.patternToByteArray( + expectedCredential as List<LockPatternView.Cell> + ) + ) + else -> error("Unsupported credential type $type!") + } + } + + private fun List<AuthenticationMethodModel.Pattern.PatternCoordinate>.toCells(): + List<LockPatternView.Cell> { + return map { coordinate -> LockPatternView.Cell.of(coordinate.y, coordinate.x) } + } } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index eb2f71a9b951..60a4951735c5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -194,6 +194,10 @@ class FakeKeyguardRepository : KeyguardRepository { _statusBarState.value = state } + fun setKeyguardUnlocked(isUnlocked: Boolean) { + _isKeyguardUnlocked.value = isUnlocked + } + override fun isUdfpsSupported(): Boolean { return _isUdfpsSupported.value } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt index 0b6e2a2a4e51..fec1187d8d11 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt @@ -16,29 +16,30 @@ package com.android.systemui.scene -import com.android.keyguard.KeyguardSecurityModel.SecurityMode +import android.content.pm.UserInfo import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.AuthenticationRepository -import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository -import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository.Companion.toSecurityMode import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.bouncer.data.repository.BouncerRepository import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.LockscreenSceneInteractor import com.android.systemui.scene.data.repository.SceneContainerRepository import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.SceneContainerConfig +import com.android.systemui.scene.shared.model.SceneContainerNames import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.currentTime /** * Utilities for creating scene container framework related repositories, interactors, and @@ -48,22 +49,19 @@ import kotlinx.coroutines.test.TestScope class SceneTestUtils( test: SysuiTestCase, ) { - val testDispatcher: TestDispatcher by lazy { StandardTestDispatcher() } - val testScope: TestScope by lazy { TestScope(testDispatcher) } - private var securityMode: SecurityMode = - FakeAuthenticationRepository.DEFAULT_AUTHENTICATION_METHOD.toSecurityMode() + val testDispatcher = StandardTestDispatcher() + val testScope = TestScope(testDispatcher) + private val userRepository: UserRepository by lazy { + FakeUserRepository().apply { + val users = listOf(UserInfo(/* id= */ 0, "name", /* flags= */ 0)) + setUserInfos(users) + runBlocking { setSelectedUserInfo(users.first()) } + } + } + val authenticationRepository: FakeAuthenticationRepository by lazy { FakeAuthenticationRepository( - delegate = - AuthenticationRepositoryImpl( - applicationScope = applicationScope(), - getSecurityMode = { securityMode }, - backgroundDispatcher = testDispatcher, - userRepository = FakeUserRepository(), - lockPatternUtils = mock(), - keyguardRepository = FakeKeyguardRepository(), - ), - onSecurityModeChanged = { securityMode = it }, + currentTime = { testScope.currentTime }, ) } private val context = test.context @@ -105,7 +103,7 @@ class SceneTestUtils( ) } - fun authenticationRepository(): AuthenticationRepository { + fun authenticationRepository(): FakeAuthenticationRepository { return authenticationRepository } @@ -115,6 +113,9 @@ class SceneTestUtils( return AuthenticationInteractor( applicationScope = applicationScope(), repository = repository, + backgroundDispatcher = testDispatcher, + userRepository = userRepository, + clock = mock { whenever(elapsedRealtime()).thenAnswer { testScope.currentTime } } ) } @@ -172,7 +173,7 @@ class SceneTestUtils( } companion object { - const val CONTAINER_1 = "container1" + const val CONTAINER_1 = SceneContainerNames.SYSTEM_UI_DEFAULT const val CONTAINER_2 = "container2" } } |