[flexiglass] Hooks up real app state and logic in keyguard (3)

- Connects the unlock logic to Flexiglass to let it unlock the device.

Bug: 280883900
Test: Unit tests still pass.
Test: manually verified that unlocking the device through the Flexiglass
bouncer actually unlocks the actual device.
Test: manually verified that locking the device using the power button (with "instantly lock device" setting both
on and off) properly locks the device and moves back to the Lockscreen scene that shows the
lock icon as locked.
Test: verified that above with no lock screen, swipe method, PIN,
pattern, and password methods.

Change-Id: I1cfb71ab31cf2ddda54c8009656a58b3f64e6fa6
This commit is contained in:
Alejandro Nijamkin
2023-06-30 17:47:43 -07:00
parent d3813b1887
commit db7568e3c4
35 changed files with 2205 additions and 1473 deletions

View File

@@ -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;
}
}
/** */

View File

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

View File

@@ -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.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
}
if (input.isEmpty()) {
throw IllegalArgumentException("Input was empty!")
}
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
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
}
if (isSuccessful) {
repository.setFailedAuthenticationAttempts(0)
} else {
repository.setFailedAuthenticationAttempts(
repository.failedAuthenticationAttempts.value + 1
// Attempt to authenticate:
val authMethod = getAuthenticationMethod()
val credential = authMethod.createCredential(input) ?: return null
val authenticationResult = repository.checkCredential(credential)
credential.zeroize()
if (authenticationResult.isSuccessful || !tryAutoConfirm) {
repository.reportAuthenticationAttempt(
isSuccessful = authenticationResult.isSuccessful,
)
}
return 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()
}
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
}
}
}
/**
* 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) } }
}
/** Cancels any throttling state countdown started in [startThrottlingCountdown]. */
private fun cancelCountdown() {
throttlingCountdownJob?.cancel()
throttlingCountdownJob = null
}
/**
* 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) }
}
/** Notifies that the currently-selected user has changed. */
private suspend fun onSelectedUserChanged() {
cancelCountdown()
if (refreshThrottling() > 0) {
startThrottlingCountdown()
}
}
/**
* 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
}
}
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
}
}
}

View File

@@ -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) {
object Pin : 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)
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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -530,6 +530,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
}
@@ -772,17 +778,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)

View File

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

View File

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

View File

@@ -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,24 +52,56 @@ 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("password"))
.isEqualTo(AuthenticationMethodModel.Password)
}
@Test
fun isUnlocked_whenAuthMethodIsNone_isTrue() =
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.None)
}
@Test
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 {
@@ -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())
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
}
companion object {
val DEFAULT_AUTHENTICATION_METHOD =
AuthenticationMethodModel.Pin(listOf(1, 2, 3, 4), autoConfirm = false)
fun setAutoConfirmEnabled(isEnabled: Boolean) {
_isAutoConfirmEnabled.value = isEnabled
}
fun AuthenticationMethodModel.toSecurityMode(): SecurityMode {
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
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
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) }
}
}
}

View File

@@ -187,6 +187,10 @@ class FakeKeyguardRepository : KeyguardRepository {
_statusBarState.value = state
}
fun setKeyguardUnlocked(isUnlocked: Boolean) {
_isKeyguardUnlocked.value = isUnlocked
}
override fun isUdfpsSupported(): Boolean {
return _isUdfpsSupported.value
}

View File

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