[flexiglass] Adds authmethod flow.

Now that I realized that we _can_ listen for changes to the
authentication method (using a broadcast), we can introduce this very
useful flow to our system. This allows downstream collectors who
previously ignored auth method changes to pick up on these.

Bug: 292502433
Test: manually verified that authentication method changes are picked up
by Flexiglass.
Test: unit tests updated.

Change-Id: Ifbbf5be25ed6bdb49f6c8793f5b5093fb14679bf
This commit is contained in:
Alejandro Nijamkin
2023-08-02 11:08:49 -07:00
parent 484b00fd26
commit 1cf0f476a3
27 changed files with 540 additions and 520 deletions

View File

@@ -63,7 +63,7 @@ constructor(
.stateIn(
scope = applicationScope,
started = SharingStarted.Eagerly,
initialValue = destinationScenes(up = viewModel.upDestinationSceneKey.value)
initialValue = destinationScenes(up = null)
)
@Composable
@@ -77,12 +77,12 @@ constructor(
}
private fun destinationScenes(
up: SceneKey,
up: SceneKey?,
): Map<UserAction, SceneModel> {
return mapOf(
UserAction.Swipe(Direction.UP) to SceneModel(up),
UserAction.Swipe(Direction.DOWN) to SceneModel(SceneKey.Shade)
)
return buildMap {
up?.let { this[UserAction.Swipe(Direction.UP)] = SceneModel(up) }
this[UserAction.Swipe(Direction.DOWN)] = SceneModel(SceneKey.Shade)
}
}
}

View File

@@ -0,0 +1,37 @@
/*
* 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.data.model
/** Enumerates all known authentication methods. */
sealed class AuthenticationMethodModel(
/**
* Whether the authentication method is considered to be "secure".
*
* "Secure" authentication methods require authentication to unlock the device. Non-secure auth
* methods simply require user dismissal.
*/
open val isSecure: Boolean,
) {
/** There is no authentication method on the device. We shouldn't even show the lock screen. */
object None : AuthenticationMethodModel(isSecure = false)
object Pin : AuthenticationMethodModel(isSecure = true)
object Password : AuthenticationMethodModel(isSecure = true)
object Pattern : AuthenticationMethodModel(isSecure = true)
}

View File

@@ -14,15 +14,21 @@
* limitations under the License.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package com.android.systemui.authentication.data.repository
import android.app.admin.DevicePolicyManager
import android.content.IntentFilter
import android.os.UserHandle
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.data.model.AuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationResultModel
import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyguard.data.repository.KeyguardRepository
@@ -37,13 +43,17 @@ import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -54,9 +64,10 @@ interface AuthenticationRepository {
* Whether the device is unlocked.
*
* A device that is not yet unlocked requires unlocking by completing an authentication
* challenge according to the current authentication method.
*
* Note that this state has no real bearing on whether the lockscreen is showing or dismissed.
* challenge according to the current authentication method, unless in cases when the current
* authentication method is not "secure" (for example, None); in such cases, the value of this
* flow will always be `true`, even if the lockscreen is showing and still needs to be dismissed
* by the user to proceed.
*/
val isUnlocked: StateFlow<Boolean>
@@ -85,9 +96,30 @@ interface AuthenticationRepository {
/** The current throttling state, as cached via [setThrottling]. */
val throttling: StateFlow<AuthenticationThrottlingModel>
/**
* The currently-configured authentication method. This determines how the authentication
* challenge needs to be completed in order to unlock an otherwise locked device.
*
* Note: there may be other ways to unlock the device that "bypass" the need for this
* authentication challenge (notably, biometrics like fingerprint or face unlock).
*
* Note: by design, this is a [Flow] and not a [StateFlow]; a consumer who wishes to get a
* snapshot of the current authentication method without establishing a collector of the flow
* can do so by invoking [getAuthenticationMethod].
*/
val authenticationMethod: Flow<AuthenticationMethodModel>
/**
* Returns the currently-configured authentication method. This determines how the
* authentication challenge is completed in order to unlock an otherwise locked device.
* authentication challenge needs to be completed in order to unlock an otherwise locked device.
*
* Note: there may be other ways to unlock the device that "bypass" the need for this
* authentication challenge (notably, biometrics like fingerprint or face unlock).
*
* Note: by design, this is offered as a convenience method alongside [authenticationMethod].
* The flow should be used for code that wishes to stay up-to-date its logic as the
* authentication changes over time and this method should be used for simple code that only
* needs to check the current value.
*/
suspend fun getAuthenticationMethod(): AuthenticationMethodModel
@@ -141,6 +173,7 @@ constructor(
private val userRepository: UserRepository,
keyguardRepository: KeyguardRepository,
private val lockPatternUtils: LockPatternUtils,
broadcastDispatcher: BroadcastDispatcher,
) : AuthenticationRepository {
override val isUnlocked = keyguardRepository.isKeyguardUnlocked
@@ -148,7 +181,7 @@ constructor(
override suspend fun isLockscreenEnabled(): Boolean {
return withContext(backgroundDispatcher) {
val selectedUserId = userRepository.selectedUserId
!lockPatternUtils.isLockPatternEnabled(selectedUserId)
!lockPatternUtils.isLockScreenDisabled(selectedUserId)
}
}
@@ -172,18 +205,31 @@ constructor(
private val UserRepository.selectedUserId: Int
get() = getSelectedUserInfo().id
override val authenticationMethod: Flow<AuthenticationMethodModel> =
userRepository.selectedUserInfo
.map { it.id }
.distinctUntilChanged()
.flatMapLatest { selectedUserId ->
broadcastDispatcher
.broadcastFlow(
filter =
IntentFilter(
DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED
),
user = UserHandle.of(selectedUserId),
)
.onStart { emit(Unit) }
.map { selectedUserId }
}
.map { selectedUserId ->
withContext(backgroundDispatcher) {
blockingAuthenticationMethodInternal(selectedUserId)
}
}
override suspend fun getAuthenticationMethod(): AuthenticationMethodModel {
return withContext(backgroundDispatcher) {
val selectedUserId = userRepository.selectedUserId
when (getSecurityMode.apply(selectedUserId)) {
KeyguardSecurityModel.SecurityMode.PIN,
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!")
}
blockingAuthenticationMethodInternal(userRepository.selectedUserId)
}
}
@@ -301,6 +347,27 @@ constructor(
return flow.asStateFlow()
}
/**
* Returns the authentication method for the given user ID.
*
* WARNING: this is actually a blocking IPC/"binder" call that's expensive to do on the main
* thread. We keep it not marked as `suspend` because we want to be able to run this without a
* `runBlocking` which has a ton of performance/blocking problems.
*/
private fun blockingAuthenticationMethodInternal(
userId: Int,
): AuthenticationMethodModel {
return when (getSecurityMode.apply(userId)) {
KeyguardSecurityModel.SecurityMode.PIN,
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!")
}
}
}
@Module

View File

@@ -18,8 +18,10 @@ package com.android.systemui.authentication.domain.interactor
import com.android.internal.widget.LockPatternView
import com.android.internal.widget.LockscreenCredential
import com.android.systemui.authentication.data.model.AuthenticationMethodModel as DataLayerAuthenticationMethodModel
import com.android.systemui.authentication.data.repository.AuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainLayerAuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -35,8 +37,10 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
@@ -55,22 +59,41 @@ constructor(
private val keyguardRepository: KeyguardRepository,
private val clock: SystemClock,
) {
/**
* The currently-configured authentication method. This determines how the authentication
* challenge needs to be completed in order to unlock an otherwise locked device.
*
* Note: there may be other ways to unlock the device that "bypass" the need for this
* authentication challenge (notably, biometrics like fingerprint or face unlock).
*
* Note: by design, this is a [Flow] and not a [StateFlow]; a consumer who wishes to get a
* snapshot of the current authentication method without establishing a collector of the flow
* can do so by invoking [getAuthenticationMethod].
*
* Note: this layer adds the synthetic authentication method of "swipe" which is special. When
* the current authentication method is "swipe", the user does not need to complete any
* authentication challenge to unlock the device; they just need to dismiss the lockscreen to
* get past it. This also means that the value of [isUnlocked] remains `false` even when the
* lockscreen is showing and still needs to be dismissed by the user to proceed.
*/
val authenticationMethod: Flow<DomainLayerAuthenticationMethodModel> =
repository.authenticationMethod.map { rawModel -> rawModel.toDomainLayer() }
/**
* Whether the device is unlocked.
*
* A device that is not yet unlocked requires unlocking by completing an authentication
* challenge according to the current authentication method.
*
* Note that this state has no real bearing on whether the lock screen is showing or dismissed.
* challenge according to the current authentication method, unless in cases when the current
* authentication method is not "secure" (for example, None and Swipe); in such cases, the value
* of this flow will always be `true`, even if the lockscreen is showing and still needs to be
* dismissed by the user to proceed.
*/
val isUnlocked: StateFlow<Boolean> =
repository.isUnlocked
.map { isUnlocked ->
if (getAuthenticationMethod() is AuthenticationMethodModel.None) {
true
} else {
isUnlocked
}
combine(
repository.isUnlocked,
authenticationMethod,
) { isUnlocked, authenticationMethod ->
authenticationMethod is DomainLayerAuthenticationMethodModel.None || isUnlocked
}
.stateIn(
scope = applicationScope,
@@ -129,18 +152,24 @@ constructor(
/**
* Returns the currently-configured authentication method. This determines how the
* authentication challenge is completed in order to unlock an otherwise locked device.
* authentication challenge needs to be completed in order to unlock an otherwise locked device.
*
* Note: there may be other ways to unlock the device that "bypass" the need for this
* authentication challenge (notably, biometrics like fingerprint or face unlock).
*
* Note: by design, this is offered as a convenience method alongside [authenticationMethod].
* The flow should be used for code that wishes to stay up-to-date its logic as the
* authentication changes over time and this method should be used for simple code that only
* needs to check the current value.
*
* Note: this layer adds the synthetic authentication method of "swipe" which is special. When
* the current authentication method is "swipe", the user does not need to complete any
* authentication challenge to unlock the device; they just need to dismiss the lockscreen to
* get past it. This also means that the value of [isUnlocked] remains `false` even when the
* lockscreen is showing and still needs to be dismissed by the user to proceed.
*/
suspend fun getAuthenticationMethod(): AuthenticationMethodModel {
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
}
suspend fun getAuthenticationMethod(): DomainLayerAuthenticationMethodModel {
return repository.getAuthenticationMethod().toDomainLayer()
}
/**
@@ -270,21 +299,38 @@ constructor(
}
}
private fun AuthenticationMethodModel.createCredential(
private fun DomainLayerAuthenticationMethodModel.createCredential(
input: List<Any>
): LockscreenCredential? {
return when (this) {
is AuthenticationMethodModel.Pin ->
is DomainLayerAuthenticationMethodModel.Pin ->
LockscreenCredential.createPin(input.joinToString(""))
is AuthenticationMethodModel.Password ->
is DomainLayerAuthenticationMethodModel.Password ->
LockscreenCredential.createPassword(input.joinToString(""))
is AuthenticationMethodModel.Pattern ->
is DomainLayerAuthenticationMethodModel.Pattern ->
LockscreenCredential.createPattern(
input
.map { it as AuthenticationMethodModel.Pattern.PatternCoordinate }
.map { it as AuthenticationPatternCoordinate }
.map { LockPatternView.Cell.of(it.y, it.x) }
)
else -> null
}
}
private suspend fun DataLayerAuthenticationMethodModel.toDomainLayer():
DomainLayerAuthenticationMethodModel {
return when (this) {
is DataLayerAuthenticationMethodModel.None ->
if (repository.isLockscreenEnabled()) {
DomainLayerAuthenticationMethodModel.Swipe
} else {
DomainLayerAuthenticationMethodModel.None
}
is DataLayerAuthenticationMethodModel.Pin -> DomainLayerAuthenticationMethodModel.Pin
is DataLayerAuthenticationMethodModel.Password ->
DomainLayerAuthenticationMethodModel.Password
is DataLayerAuthenticationMethodModel.Pattern ->
DomainLayerAuthenticationMethodModel.Pattern
}
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.systemui.authentication.shared.model
package com.android.systemui.authentication.domain.model
/** Enumerates all known authentication methods. */
sealed class AuthenticationMethodModel(
@@ -36,10 +36,5 @@ sealed class AuthenticationMethodModel(
object Password : AuthenticationMethodModel(isSecure = true)
object Pattern : AuthenticationMethodModel(isSecure = true) {
data class PatternCoordinate(
val x: Int,
val y: Int,
)
}
object Pattern : AuthenticationMethodModel(isSecure = true)
}

View File

@@ -0,0 +1,22 @@
/*
* 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
data class AuthenticationPatternCoordinate(
val x: Int,
val y: Int,
)

View File

@@ -14,14 +14,12 @@
* limitations under the License.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package com.android.systemui.bouncer.domain.interactor
import android.content.Context
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.domain.model.AuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel
import com.android.systemui.bouncer.data.repository.BouncerRepository
import com.android.systemui.dagger.SysUISingleton
@@ -35,7 +33,6 @@ import com.android.systemui.util.kotlin.pairwise
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
@@ -106,14 +103,6 @@ constructor(
}
}
/**
* 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 authenticationInteractor.getAuthenticationMethod()
}
/**
* Either shows the bouncer or unlocks the device, if the bouncer doesn't need to be shown.
*
@@ -124,7 +113,9 @@ constructor(
) {
applicationScope.launch {
if (authenticationInteractor.isAuthenticationRequired()) {
repository.setMessage(message ?: promptMessage(getAuthenticationMethod()))
repository.setMessage(
message ?: promptMessage(authenticationInteractor.getAuthenticationMethod())
)
sceneInteractor.setCurrentScene(
scene = SceneModel(SceneKey.Bouncer),
loggingReason = "request to unlock device while authentication required",
@@ -143,7 +134,9 @@ constructor(
* method.
*/
fun resetMessage() {
applicationScope.launch { repository.setMessage(promptMessage(getAuthenticationMethod())) }
applicationScope.launch {
repository.setMessage(promptMessage(authenticationInteractor.getAuthenticationMethod()))
}
}
/** Removes the user-facing message. */
@@ -181,7 +174,7 @@ constructor(
loggingReason = "successful authentication",
)
} else {
repository.setMessage(errorMessage(getAuthenticationMethod()))
repository.setMessage(errorMessage(authenticationInteractor.getAuthenticationMethod()))
}
return isAuthenticated

View File

@@ -14,25 +14,20 @@
* 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.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.util.kotlin.pairwise
import javax.inject.Inject
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
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -50,23 +45,24 @@ class BouncerViewModel
constructor(
@Application private val applicationContext: Context,
@Application private val applicationScope: CoroutineScope,
private val interactor: BouncerInteractor,
private val bouncerInteractor: BouncerInteractor,
private val authenticationInteractor: AuthenticationInteractor,
featureFlags: FeatureFlags,
) {
private val isInputEnabled: StateFlow<Boolean> =
interactor.isThrottled
bouncerInteractor.isThrottled
.map { !it }
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
initialValue = !interactor.isThrottled.value,
initialValue = !bouncerInteractor.isThrottled.value,
)
private val pin: PinBouncerViewModel by lazy {
PinBouncerViewModel(
applicationContext = applicationContext,
applicationScope = applicationScope,
interactor = interactor,
interactor = bouncerInteractor,
isInputEnabled = isInputEnabled,
)
}
@@ -74,7 +70,7 @@ constructor(
private val password: PasswordBouncerViewModel by lazy {
PasswordBouncerViewModel(
applicationScope = applicationScope,
interactor = interactor,
interactor = bouncerInteractor,
isInputEnabled = isInputEnabled,
)
}
@@ -83,31 +79,35 @@ constructor(
PatternBouncerViewModel(
applicationContext = applicationContext,
applicationScope = applicationScope,
interactor = interactor,
interactor = bouncerInteractor,
isInputEnabled = isInputEnabled,
)
}
/** View-model for the current UI, based on the current authentication method. */
private val _authMethod =
MutableSharedFlow<AuthMethodBouncerViewModel?>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
val authMethod: StateFlow<AuthMethodBouncerViewModel?> =
_authMethod.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
initialValue = null,
)
authenticationInteractor.authenticationMethod
.map { authenticationMethod ->
when (authenticationMethod) {
is AuthenticationMethodModel.Pin -> pin
is AuthenticationMethodModel.Password -> password
is AuthenticationMethodModel.Pattern -> pattern
else -> null
}
}
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
initialValue = null,
)
init {
if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
applicationScope.launch {
interactor.isThrottled
bouncerInteractor.isThrottled
.map { isThrottled ->
if (isThrottled) {
when (interactor.getAuthenticationMethod()) {
when (authenticationInteractor.getAuthenticationMethod()) {
is AuthenticationMethodModel.Pin ->
R.string.kg_too_many_failed_pin_attempts_dialog_message
is AuthenticationMethodModel.Password ->
@@ -118,8 +118,9 @@ constructor(
}?.let { stringResourceId ->
applicationContext.getString(
stringResourceId,
interactor.throttling.value.failedAttemptCount,
ceil(interactor.throttling.value.remainingMs / 1000f).toInt(),
bouncerInteractor.throttling.value.failedAttemptCount,
ceil(bouncerInteractor.throttling.value.remainingMs / 1000f)
.toInt(),
)
}
} else {
@@ -133,25 +134,14 @@ constructor(
}
}
}
applicationScope.launch {
_authMethod.subscriptionCount
.pairwise()
.map { (previousCount, currentCount) -> currentCount > previousCount }
.collect { subscriberAdded ->
if (subscriberAdded) {
reloadAuthMethod()
}
}
}
}
}
/** The user-facing message to show in the bouncer. */
val message: StateFlow<MessageViewModel> =
combine(
interactor.message,
interactor.isThrottled,
bouncerInteractor.message,
bouncerInteractor.isThrottled,
) { message, isThrottled ->
toMessageViewModel(message, isThrottled)
}
@@ -160,8 +150,8 @@ constructor(
started = SharingStarted.WhileSubscribed(),
initialValue =
toMessageViewModel(
message = interactor.message.value,
isThrottled = interactor.isThrottled.value,
message = bouncerInteractor.message.value,
isThrottled = bouncerInteractor.isThrottled.value,
),
)
@@ -197,17 +187,6 @@ constructor(
)
}
private suspend fun reloadAuthMethod() {
_authMethod.tryEmit(
when (interactor.getAuthenticationMethod()) {
is AuthenticationMethodModel.Pin -> pin
is AuthenticationMethodModel.Password -> password
is AuthenticationMethodModel.Pattern -> pattern
else -> null
}
)
}
data class MessageViewModel(
val text: String,

View File

@@ -18,7 +18,7 @@ package com.android.systemui.bouncer.ui.viewmodel
import android.content.Context
import android.util.TypedValue
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import kotlin.math.max
import kotlin.math.min
@@ -193,8 +193,8 @@ data class PatternDotViewModel(
val x: Int,
val y: Int,
) {
fun toCoordinate(): AuthenticationMethodModel.Pattern.PatternCoordinate {
return AuthenticationMethodModel.Pattern.PatternCoordinate(
fun toCoordinate(): AuthenticationPatternCoordinate {
return AuthenticationPatternCoordinate(
x = x,
y = y,
)

View File

@@ -1,68 +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.keyguard.domain.interactor
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
/** Hosts business and application state accessing logic for the lockscreen scene. */
@SysUISingleton
class LockscreenSceneInteractor
@Inject
constructor(
@Application applicationScope: CoroutineScope,
private val authenticationInteractor: AuthenticationInteractor,
private val bouncerInteractor: BouncerInteractor,
) {
/** Whether the device is currently locked. */
val isDeviceLocked: StateFlow<Boolean> =
authenticationInteractor.isUnlocked
.map { !it }
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
initialValue = !authenticationInteractor.isUnlocked.value,
)
/** Whether it's currently possible to swipe up to dismiss the lockscreen. */
val isSwipeToDismissEnabled: StateFlow<Boolean> =
authenticationInteractor.isUnlocked
.map { isUnlocked ->
!isUnlocked &&
authenticationInteractor.getAuthenticationMethod() is
AuthenticationMethodModel.Swipe
}
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
initialValue = false,
)
/** Attempts to dismiss the lockscreen. This will cause the bouncer to show, if needed. */
fun dismissLockscreen() {
bouncerInteractor.showOrUnlockDevice()
}
}

View File

@@ -17,14 +17,17 @@
package com.android.systemui.keyguard.ui.viewmodel
import com.android.systemui.R
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.authentication.domain.model.AuthenticationMethodModel
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.domain.interactor.LockscreenSceneInteractor
import com.android.systemui.scene.shared.model.SceneKey
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
@@ -36,36 +39,37 @@ class LockscreenSceneViewModel
@Inject
constructor(
@Application applicationScope: CoroutineScope,
private val interactor: LockscreenSceneInteractor,
authenticationInteractor: AuthenticationInteractor,
private val bouncerInteractor: BouncerInteractor,
) {
/** The icon for the "lock" button on the lockscreen. */
val lockButtonIcon: StateFlow<Icon> =
interactor.isDeviceLocked
.map { isLocked -> lockIcon(isLocked = isLocked) }
authenticationInteractor.isUnlocked
.map { isUnlocked -> lockIcon(isUnlocked = isUnlocked) }
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
initialValue = lockIcon(isLocked = interactor.isDeviceLocked.value),
initialValue = lockIcon(isUnlocked = authenticationInteractor.isUnlocked.value),
)
/** The key of the scene we should switch to when swiping up. */
val upDestinationSceneKey: StateFlow<SceneKey> =
interactor.isSwipeToDismissEnabled
.map { isSwipeToUnlockEnabled -> upDestinationSceneKey(isSwipeToUnlockEnabled) }
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
initialValue = upDestinationSceneKey(interactor.isSwipeToDismissEnabled.value),
)
val upDestinationSceneKey: Flow<SceneKey> =
authenticationInteractor.authenticationMethod.map { authenticationMethod ->
if (authenticationMethod is AuthenticationMethodModel.Swipe) {
SceneKey.Gone
} else {
SceneKey.Bouncer
}
}
/** Notifies that the lock button on the lock screen was clicked. */
fun onLockButtonClicked() {
interactor.dismissLockscreen()
bouncerInteractor.showOrUnlockDevice()
}
/** Notifies that some content on the lock screen was clicked. */
fun onContentClicked() {
interactor.dismissLockscreen()
bouncerInteractor.showOrUnlockDevice()
}
private fun upDestinationSceneKey(
@@ -75,22 +79,22 @@ constructor(
}
private fun lockIcon(
isLocked: Boolean,
isUnlocked: Boolean,
): Icon {
return Icon.Resource(
res =
if (isLocked) {
R.drawable.ic_device_lock_on
} else {
if (isUnlocked) {
R.drawable.ic_device_lock_off
} else {
R.drawable.ic_device_lock_on
},
contentDescription =
ContentDescription.Resource(
res =
if (isLocked) {
R.string.accessibility_lock_icon
} else {
if (isUnlocked) {
R.string.accessibility_unlock_button
} else {
R.string.accessibility_lock_icon
}
)
)

View File

@@ -16,19 +16,17 @@
package com.android.systemui.qs.ui.viewmodel
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.LockscreenSceneInteractor
import javax.inject.Inject
/** Models UI state and handles user input for the quick settings scene. */
@SysUISingleton
class QuickSettingsSceneViewModel
@Inject
constructor(
private val lockscreenSceneInteractor: LockscreenSceneInteractor,
) {
constructor(private val bouncerInteractor: BouncerInteractor) {
/** Notifies that some content in quick settings was clicked. */
fun onContentClicked() {
lockscreenSceneInteractor.dismissLockscreen()
bouncerInteractor.showOrUnlockDevice()
}
}

View File

@@ -16,9 +16,10 @@
package com.android.systemui.shade.ui.viewmodel
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.domain.interactor.LockscreenSceneInteractor
import com.android.systemui.scene.shared.model.SceneKey
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -33,29 +34,30 @@ class ShadeSceneViewModel
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
private val lockscreenSceneInteractor: LockscreenSceneInteractor,
authenticationInteractor: AuthenticationInteractor,
private val bouncerInteractor: BouncerInteractor,
) {
/** The key of the scene we should switch to when swiping up. */
val upDestinationSceneKey: StateFlow<SceneKey> =
lockscreenSceneInteractor.isDeviceLocked
.map { isLocked -> upDestinationSceneKey(isLocked = isLocked) }
authenticationInteractor.isUnlocked
.map { isUnlocked -> upDestinationSceneKey(isUnlocked = isUnlocked) }
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
initialValue =
upDestinationSceneKey(
isLocked = lockscreenSceneInteractor.isDeviceLocked.value,
isUnlocked = authenticationInteractor.isUnlocked.value,
),
)
/** Notifies that some content in the shade was clicked. */
fun onContentClicked() {
lockscreenSceneInteractor.dismissLockscreen()
bouncerInteractor.showOrUnlockDevice()
}
private fun upDestinationSceneKey(
isLocked: Boolean,
isUnlocked: Boolean,
): SceneKey {
return if (isLocked) SceneKey.Lockscreen else SceneKey.Gone
return if (isUnlocked) SceneKey.Gone else SceneKey.Lockscreen
}
}

View File

@@ -18,23 +18,30 @@
package com.android.systemui.authentication.data.repository
import android.app.admin.DevicePolicyManager
import android.content.Intent
import android.content.pm.UserInfo
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils
import com.android.keyguard.KeyguardSecurityModel
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.model.AuthenticationMethodModel
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.scene.SceneTestUtils
import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import java.util.function.Function
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
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.junit.runners.JUnit4
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.MockitoAnnotations
@@ -43,6 +50,7 @@ import org.mockito.MockitoAnnotations
class AuthenticationRepositoryTest : SysuiTestCase() {
@Mock private lateinit var lockPatternUtils: LockPatternUtils
@Mock private lateinit var getSecurityMode: Function<Int, KeyguardSecurityModel.SecurityMode>
private val testUtils = SceneTestUtils(this)
private val testScope = testUtils.testScope
@@ -50,23 +58,48 @@ class AuthenticationRepositoryTest : SysuiTestCase() {
private lateinit var underTest: AuthenticationRepository
private var currentSecurityMode: KeyguardSecurityModel.SecurityMode =
KeyguardSecurityModel.SecurityMode.PIN
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
userRepository.setUserInfos(USER_INFOS)
runBlocking { userRepository.setSelectedUserInfo(USER_INFOS[0]) }
whenever(getSecurityMode.apply(anyInt())).thenAnswer { currentSecurityMode }
underTest =
AuthenticationRepositoryImpl(
applicationScope = testScope.backgroundScope,
getSecurityMode = { KeyguardSecurityModel.SecurityMode.PIN },
getSecurityMode = getSecurityMode,
backgroundDispatcher = testUtils.testDispatcher,
userRepository = userRepository,
keyguardRepository = testUtils.keyguardRepository,
lockPatternUtils = lockPatternUtils,
broadcastDispatcher = fakeBroadcastDispatcher,
)
}
@Test
fun authenticationMethod() =
testScope.runTest {
val authMethod by collectLastValue(underTest.authenticationMethod)
runCurrent()
dispatchBroadcast()
assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Pin)
assertThat(underTest.getAuthenticationMethod()).isEqualTo(AuthenticationMethodModel.Pin)
setSecurityModeAndDispatchBroadcast(KeyguardSecurityModel.SecurityMode.Pattern)
assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Pattern)
assertThat(underTest.getAuthenticationMethod())
.isEqualTo(AuthenticationMethodModel.Pattern)
setSecurityModeAndDispatchBroadcast(KeyguardSecurityModel.SecurityMode.None)
assertThat(authMethod).isEqualTo(AuthenticationMethodModel.None)
assertThat(underTest.getAuthenticationMethod())
.isEqualTo(AuthenticationMethodModel.None)
}
@Test
fun isAutoConfirmEnabled() =
testScope.runTest {
@@ -95,6 +128,20 @@ class AuthenticationRepositoryTest : SysuiTestCase() {
assertThat(values.last()).isTrue()
}
private fun setSecurityModeAndDispatchBroadcast(
securityMode: KeyguardSecurityModel.SecurityMode,
) {
currentSecurityMode = securityMode
dispatchBroadcast()
}
private fun dispatchBroadcast() {
fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
context,
Intent(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED)
)
}
companion object {
private val USER_INFOS =
listOf(

View File

@@ -19,9 +19,11 @@ package com.android.systemui.authentication.domain.interactor
import android.app.admin.DevicePolicyManager
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.model.AuthenticationMethodModel as DataLayerAuthenticationMethodModel
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.domain.model.AuthenticationMethodModel as DomainLayerAuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.scene.SceneTestUtils
@@ -50,42 +52,61 @@ class AuthenticationInteractorTest : SysuiTestCase() {
)
@Test
fun getAuthenticationMethod() =
fun authenticationMethod() =
testScope.runTest {
assertThat(underTest.getAuthenticationMethod()).isEqualTo(AuthenticationMethodModel.Pin)
val authMethod by collectLastValue(underTest.authenticationMethod)
runCurrent()
assertThat(authMethod).isEqualTo(DomainLayerAuthenticationMethodModel.Pin)
assertThat(underTest.getAuthenticationMethod())
.isEqualTo(DomainLayerAuthenticationMethodModel.Pin)
utils.authenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.Password
DataLayerAuthenticationMethodModel.Password
)
assertThat(authMethod).isEqualTo(DomainLayerAuthenticationMethodModel.Password)
assertThat(underTest.getAuthenticationMethod())
.isEqualTo(AuthenticationMethodModel.Password)
.isEqualTo(DomainLayerAuthenticationMethodModel.Password)
}
@Test
fun getAuthenticationMethod_noneTreatedAsSwipe_whenLockscreenEnabled() =
fun authenticationMethod_noneTreatedAsSwipe_whenLockscreenEnabled() =
testScope.runTest {
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
val authMethod by collectLastValue(underTest.authenticationMethod)
runCurrent()
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.None
)
utils.authenticationRepository.setLockscreenEnabled(true)
assertThat(authMethod).isEqualTo(DomainLayerAuthenticationMethodModel.Swipe)
assertThat(underTest.getAuthenticationMethod())
.isEqualTo(AuthenticationMethodModel.Swipe)
.isEqualTo(DomainLayerAuthenticationMethodModel.Swipe)
}
@Test
fun getAuthenticationMethod_none_whenLockscreenDisabled() =
fun authenticationMethod_none_whenLockscreenDisabled() =
testScope.runTest {
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
val authMethod by collectLastValue(underTest.authenticationMethod)
runCurrent()
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.None
)
utils.authenticationRepository.setLockscreenEnabled(false)
assertThat(authMethod).isEqualTo(DomainLayerAuthenticationMethodModel.None)
assertThat(underTest.getAuthenticationMethod())
.isEqualTo(AuthenticationMethodModel.None)
.isEqualTo(DomainLayerAuthenticationMethodModel.None)
}
@Test
fun isUnlocked_whenAuthMethodIsNoneAndLockscreenDisabled_isTrue() =
testScope.runTest {
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.None
)
utils.authenticationRepository.setLockscreenEnabled(false)
val isUnlocked by collectLastValue(underTest.isUnlocked)
@@ -111,7 +132,9 @@ class AuthenticationInteractorTest : SysuiTestCase() {
@Test
fun isUnlocked_whenAuthMethodIsNoneAndLockscreenEnabled_isFalse() =
testScope.runTest {
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.None
)
utils.authenticationRepository.setLockscreenEnabled(true)
val isUnlocked by collectLastValue(underTest.isUnlocked)
@@ -124,7 +147,7 @@ class AuthenticationInteractorTest : SysuiTestCase() {
utils.authenticationRepository.setUnlocked(false)
runCurrent()
utils.authenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.Password
DataLayerAuthenticationMethodModel.Password
)
assertThat(underTest.isAuthenticationRequired()).isTrue()
@@ -135,7 +158,9 @@ class AuthenticationInteractorTest : SysuiTestCase() {
testScope.runTest {
utils.authenticationRepository.setUnlocked(false)
runCurrent()
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.None
)
assertThat(underTest.isAuthenticationRequired()).isFalse()
}
@@ -146,7 +171,7 @@ class AuthenticationInteractorTest : SysuiTestCase() {
utils.authenticationRepository.setUnlocked(true)
runCurrent()
utils.authenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.Password
DataLayerAuthenticationMethodModel.Password
)
assertThat(underTest.isAuthenticationRequired()).isFalse()
@@ -157,7 +182,9 @@ class AuthenticationInteractorTest : SysuiTestCase() {
testScope.runTest {
utils.authenticationRepository.setUnlocked(true)
runCurrent()
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.None
)
assertThat(underTest.isAuthenticationRequired()).isFalse()
}
@@ -166,7 +193,9 @@ class AuthenticationInteractorTest : SysuiTestCase() {
fun authenticate_withCorrectPin_returnsTrue() =
testScope.runTest {
val isThrottled by collectLastValue(underTest.isThrottled)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.Pin
)
assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)).isTrue()
assertThat(isThrottled).isFalse()
}
@@ -174,21 +203,27 @@ class AuthenticationInteractorTest : SysuiTestCase() {
@Test
fun authenticate_withIncorrectPin_returnsFalse() =
testScope.runTest {
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.Pin
)
assertThat(underTest.authenticate(listOf(9, 8, 7, 6, 5, 4))).isFalse()
}
@Test(expected = IllegalArgumentException::class)
fun authenticate_withEmptyPin_throwsException() =
testScope.runTest {
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.Pin
)
underTest.authenticate(listOf())
}
@Test
fun authenticate_withCorrectMaxLengthPin_returnsTrue() =
testScope.runTest {
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.Pin
)
val pin = List(16) { 9 }
utils.authenticationRepository.overrideCredential(pin)
assertThat(underTest.authenticate(pin)).isTrue()
@@ -203,7 +238,9 @@ class AuthenticationInteractorTest : SysuiTestCase() {
// If the policy changes, there is work to do in SysUI.
assertThat(DevicePolicyManager.MAX_PASSWORD_LENGTH).isLessThan(17)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.Pin
)
assertThat(underTest.authenticate(List(17) { 9 })).isFalse()
}
@@ -212,7 +249,7 @@ class AuthenticationInteractorTest : SysuiTestCase() {
testScope.runTest {
val isThrottled by collectLastValue(underTest.isThrottled)
utils.authenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.Password
DataLayerAuthenticationMethodModel.Password
)
assertThat(underTest.authenticate("password".toList())).isTrue()
@@ -223,7 +260,7 @@ class AuthenticationInteractorTest : SysuiTestCase() {
fun authenticate_withIncorrectPassword_returnsFalse() =
testScope.runTest {
utils.authenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.Password
DataLayerAuthenticationMethodModel.Password
)
assertThat(underTest.authenticate("alohomora".toList())).isFalse()
@@ -233,7 +270,7 @@ class AuthenticationInteractorTest : SysuiTestCase() {
fun authenticate_withCorrectPattern_returnsTrue() =
testScope.runTest {
utils.authenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.Pattern
DataLayerAuthenticationMethodModel.Pattern
)
assertThat(underTest.authenticate(FakeAuthenticationRepository.PATTERN)).isTrue()
@@ -243,21 +280,21 @@ class AuthenticationInteractorTest : SysuiTestCase() {
fun authenticate_withIncorrectPattern_returnsFalse() =
testScope.runTest {
utils.authenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.Pattern
DataLayerAuthenticationMethodModel.Pattern
)
assertThat(
underTest.authenticate(
listOf(
AuthenticationMethodModel.Pattern.PatternCoordinate(
AuthenticationPatternCoordinate(
x = 2,
y = 0,
),
AuthenticationMethodModel.Pattern.PatternCoordinate(
AuthenticationPatternCoordinate(
x = 2,
y = 1,
),
AuthenticationMethodModel.Pattern.PatternCoordinate(
AuthenticationPatternCoordinate(
x = 2,
y = 2,
),
@@ -271,7 +308,9 @@ class AuthenticationInteractorTest : SysuiTestCase() {
fun tryAutoConfirm_withAutoConfirmPinAndShorterPin_returnsNullAndHasNoEffect() =
testScope.runTest {
val isThrottled by collectLastValue(underTest.isThrottled)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.Pin
)
utils.authenticationRepository.setAutoConfirmEnabled(true)
assertThat(
underTest.authenticate(
@@ -289,7 +328,9 @@ class AuthenticationInteractorTest : SysuiTestCase() {
fun tryAutoConfirm_withAutoConfirmWrongPinCorrectLength_returnsFalseAndDoesNotUnlockDevice() =
testScope.runTest {
val isUnlocked by collectLastValue(underTest.isUnlocked)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.Pin
)
utils.authenticationRepository.setAutoConfirmEnabled(true)
assertThat(
underTest.authenticate(
@@ -305,7 +346,9 @@ class AuthenticationInteractorTest : SysuiTestCase() {
fun tryAutoConfirm_withAutoConfirmLongerPin_returnsFalseAndDoesNotUnlockDevice() =
testScope.runTest {
val isUnlocked by collectLastValue(underTest.isUnlocked)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.Pin
)
utils.authenticationRepository.setAutoConfirmEnabled(true)
assertThat(
underTest.authenticate(
@@ -321,7 +364,9 @@ class AuthenticationInteractorTest : SysuiTestCase() {
fun tryAutoConfirm_withAutoConfirmCorrectPin_returnsTrueAndUnlocksDevice() =
testScope.runTest {
val isUnlocked by collectLastValue(underTest.isUnlocked)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.Pin
)
utils.authenticationRepository.setAutoConfirmEnabled(true)
assertThat(
underTest.authenticate(
@@ -337,7 +382,9 @@ class AuthenticationInteractorTest : SysuiTestCase() {
fun tryAutoConfirm_withoutAutoConfirmButCorrectPin_returnsNullAndHasNoEffects() =
testScope.runTest {
val isUnlocked by collectLastValue(underTest.isUnlocked)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.Pin
)
utils.authenticationRepository.setAutoConfirmEnabled(false)
assertThat(
underTest.authenticate(
@@ -354,7 +401,7 @@ class AuthenticationInteractorTest : SysuiTestCase() {
testScope.runTest {
val isUnlocked by collectLastValue(underTest.isUnlocked)
utils.authenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.Password
DataLayerAuthenticationMethodModel.Password
)
assertThat(underTest.authenticate("password".toList(), tryAutoConfirm = true)).isNull()
@@ -367,7 +414,9 @@ class AuthenticationInteractorTest : SysuiTestCase() {
val isUnlocked by collectLastValue(underTest.isUnlocked)
val throttling by collectLastValue(underTest.throttling)
val isThrottled by collectLastValue(underTest.isThrottled)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.Pin
)
underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)
assertThat(isUnlocked).isTrue()
assertThat(isThrottled).isFalse()
@@ -456,7 +505,9 @@ class AuthenticationInteractorTest : SysuiTestCase() {
fun hintedPinLength_withoutAutoConfirm_isNull() =
testScope.runTest {
val hintedPinLength by collectLastValue(underTest.hintedPinLength)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.Pin
)
utils.authenticationRepository.setAutoConfirmEnabled(false)
assertThat(hintedPinLength).isNull()
@@ -466,7 +517,9 @@ class AuthenticationInteractorTest : SysuiTestCase() {
fun hintedPinLength_withAutoConfirmPinTooShort_isNull() =
testScope.runTest {
val hintedPinLength by collectLastValue(underTest.hintedPinLength)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.Pin
)
utils.authenticationRepository.overrideCredential(
buildList {
repeat(utils.authenticationRepository.hintedPinLength - 1) { add(it + 1) }
@@ -481,7 +534,9 @@ class AuthenticationInteractorTest : SysuiTestCase() {
fun hintedPinLength_withAutoConfirmPinAtRightLength_isSameLength() =
testScope.runTest {
val hintedPinLength by collectLastValue(underTest.hintedPinLength)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.Pin
)
utils.authenticationRepository.setAutoConfirmEnabled(true)
utils.authenticationRepository.overrideCredential(
buildList { repeat(utils.authenticationRepository.hintedPinLength) { add(it + 1) } }
@@ -494,7 +549,9 @@ class AuthenticationInteractorTest : SysuiTestCase() {
fun hintedPinLength_withAutoConfirmPinTooLong_isNull() =
testScope.runTest {
val hintedPinLength by collectLastValue(underTest.hintedPinLength)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.Pin
)
utils.authenticationRepository.overrideCredential(
buildList {
repeat(utils.authenticationRepository.hintedPinLength + 1) { add(it + 1) }

View File

@@ -19,8 +19,9 @@ 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.model.AuthenticationMethodModel
import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.scene.SceneTestUtils
@@ -73,6 +74,7 @@ class BouncerInteractorTest : SysuiTestCase() {
val message by collectLastValue(underTest.message)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
runCurrent()
utils.authenticationRepository.setUnlocked(false)
underTest.showOrUnlockDevice()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
@@ -104,6 +106,7 @@ class BouncerInteractorTest : SysuiTestCase() {
val message by collectLastValue(underTest.message)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
runCurrent()
utils.authenticationRepository.setAutoConfirmEnabled(true)
utils.authenticationRepository.setUnlocked(false)
underTest.showOrUnlockDevice()
@@ -140,6 +143,7 @@ class BouncerInteractorTest : SysuiTestCase() {
val message by collectLastValue(underTest.message)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
runCurrent()
utils.authenticationRepository.setUnlocked(false)
underTest.showOrUnlockDevice()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
@@ -170,6 +174,7 @@ class BouncerInteractorTest : SysuiTestCase() {
utils.authenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.Password
)
runCurrent()
utils.authenticationRepository.setUnlocked(false)
underTest.showOrUnlockDevice()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
@@ -202,6 +207,7 @@ class BouncerInteractorTest : SysuiTestCase() {
utils.authenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.Pattern
)
runCurrent()
utils.authenticationRepository.setUnlocked(false)
underTest.showOrUnlockDevice()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
@@ -214,11 +220,7 @@ class BouncerInteractorTest : SysuiTestCase() {
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
// Wrong input.
assertThat(
underTest.authenticate(
listOf(AuthenticationMethodModel.Pattern.PatternCoordinate(1, 2))
)
)
assertThat(underTest.authenticate(listOf(AuthenticationPatternCoordinate(1, 2))))
.isFalse()
assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
@@ -248,7 +250,8 @@ class BouncerInteractorTest : SysuiTestCase() {
fun showOrUnlockDevice_authMethodNotSecure_switchesToGoneScene() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
utils.authenticationRepository.setLockscreenEnabled(true)
utils.authenticationRepository.setUnlocked(false)
underTest.showOrUnlockDevice()
@@ -264,6 +267,7 @@ class BouncerInteractorTest : SysuiTestCase() {
utils.authenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.Password
)
runCurrent()
utils.authenticationRepository.setUnlocked(false)
val customMessage = "Hello there!"

View File

@@ -18,19 +18,17 @@ package com.android.systemui.bouncer.ui.viewmodel
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.model.AuthenticationMethodModel
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
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class AuthMethodBouncerViewModelTest : SysuiTestCase() {

View File

@@ -18,8 +18,9 @@ package com.android.systemui.bouncer.ui.viewmodel
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.model.AuthenticationMethodModel as DataLayerAuthenticationMethodModel
import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainLayerAuthenticationMethodModel
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.scene.SceneTestUtils
import com.google.common.truth.Truth.assertThat
@@ -55,6 +56,7 @@ class BouncerViewModelTest : SysuiTestCase() {
private val underTest =
utils.bouncerViewModel(
bouncerInteractor = bouncerInteractor,
authenticationInteractor = authenticationInteractor,
)
@Test
@@ -86,7 +88,8 @@ class BouncerViewModelTest : SysuiTestCase() {
@Test
fun authMethod_reusesInstances() =
testScope.runTest {
val seen = mutableMapOf<AuthenticationMethodModel, AuthMethodBouncerViewModel>()
val seen =
mutableMapOf<DomainLayerAuthenticationMethodModel, AuthMethodBouncerViewModel>()
val authMethodViewModel: AuthMethodBouncerViewModel? by
collectLastValue(underTest.authMethod)
// First pass, populate our "seen" map:
@@ -105,7 +108,7 @@ class BouncerViewModelTest : SysuiTestCase() {
@Test
fun authMethodsToTest_returnsCompleteSampleOfAllAuthMethodTypes() {
assertThat(authMethodsToTest().map { it::class }.toSet())
.isEqualTo(AuthenticationMethodModel::class.sealedSubclasses.toSet())
.isEqualTo(DomainLayerAuthenticationMethodModel::class.sealedSubclasses.toSet())
}
@Test
@@ -113,7 +116,9 @@ class BouncerViewModelTest : SysuiTestCase() {
testScope.runTest {
val message by collectLastValue(underTest.message)
val throttling by collectLastValue(bouncerInteractor.throttling)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.Pin
)
assertThat(message?.isUpdateAnimated).isTrue()
repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING) {
@@ -136,7 +141,9 @@ class BouncerViewModelTest : SysuiTestCase() {
}
)
val throttling by collectLastValue(bouncerInteractor.throttling)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.Pin
)
assertThat(isInputEnabled).isTrue()
repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING) {
@@ -153,7 +160,9 @@ class BouncerViewModelTest : SysuiTestCase() {
fun throttlingDialogMessage() =
testScope.runTest {
val throttlingDialogMessage by collectLastValue(underTest.throttlingDialogMessage)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setAuthenticationMethod(
DataLayerAuthenticationMethodModel.Pin
)
repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING) {
// Wrong PIN.
@@ -166,13 +175,32 @@ class BouncerViewModelTest : SysuiTestCase() {
assertThat(throttlingDialogMessage).isNull()
}
private fun authMethodsToTest(): List<AuthenticationMethodModel> {
private fun authMethodsToTest(): List<DomainLayerAuthenticationMethodModel> {
return listOf(
AuthenticationMethodModel.None,
AuthenticationMethodModel.Swipe,
AuthenticationMethodModel.Pin,
AuthenticationMethodModel.Password,
AuthenticationMethodModel.Pattern,
DomainLayerAuthenticationMethodModel.None,
DomainLayerAuthenticationMethodModel.Swipe,
DomainLayerAuthenticationMethodModel.Pin,
DomainLayerAuthenticationMethodModel.Password,
DomainLayerAuthenticationMethodModel.Pattern,
)
}
private fun FakeAuthenticationRepository.setAuthenticationMethod(
model: DomainLayerAuthenticationMethodModel,
) {
setAuthenticationMethod(
when (model) {
is DomainLayerAuthenticationMethodModel.None,
is DomainLayerAuthenticationMethodModel.Swipe ->
DataLayerAuthenticationMethodModel.None
is DomainLayerAuthenticationMethodModel.Pin ->
DataLayerAuthenticationMethodModel.Pin
is DomainLayerAuthenticationMethodModel.Password ->
DataLayerAuthenticationMethodModel.Password
is DomainLayerAuthenticationMethodModel.Pattern ->
DataLayerAuthenticationMethodModel.Pattern
}
)
setLockscreenEnabled(model !is DomainLayerAuthenticationMethodModel.None)
}
}

View File

@@ -19,7 +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.shared.model.AuthenticationMethodModel
import com.android.systemui.authentication.data.model.AuthenticationMethodModel
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.scene.SceneTestUtils
import com.android.systemui.scene.shared.model.SceneKey
@@ -55,6 +55,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() {
private val bouncerViewModel =
utils.bouncerViewModel(
bouncerInteractor = bouncerInteractor,
authenticationInteractor = authenticationInteractor,
)
private val underTest =
PasswordBouncerViewModel(

View File

@@ -19,8 +19,8 @@ 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.model.AuthenticationMethodModel
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
import com.android.systemui.scene.shared.model.SceneKey
@@ -57,6 +57,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() {
private val bouncerViewModel =
utils.bouncerViewModel(
bouncerInteractor = bouncerInteractor,
authenticationInteractor = authenticationInteractor,
)
private val underTest =
PatternBouncerViewModel(

View File

@@ -19,8 +19,8 @@ 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.model.AuthenticationMethodModel
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
import com.android.systemui.scene.shared.model.SceneKey
@@ -57,6 +57,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
private val bouncerViewModel =
utils.bouncerViewModel(
bouncerInteractor = bouncerInteractor,
authenticationInteractor = authenticationInteractor,
)
private val underTest =
PinBouncerViewModel(

View File

@@ -1,172 +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.keyguard.domain.interactor
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
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 kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class LockscreenSceneInteractorTest : SysuiTestCase() {
private val utils = SceneTestUtils(this)
private val testScope = utils.testScope
private val sceneInteractor = utils.sceneInteractor()
private val authenticationInteractor =
utils.authenticationInteractor(
repository = utils.authenticationRepository(),
)
private val underTest =
utils.lockScreenSceneInteractor(
authenticationInteractor = authenticationInteractor,
bouncerInteractor =
utils.bouncerInteractor(
authenticationInteractor = authenticationInteractor,
sceneInteractor = sceneInteractor,
),
)
@Test
fun isDeviceLocked() =
testScope.runTest {
val isDeviceLocked by collectLastValue(underTest.isDeviceLocked)
utils.authenticationRepository.setUnlocked(false)
assertThat(isDeviceLocked).isTrue()
utils.authenticationRepository.setUnlocked(true)
assertThat(isDeviceLocked).isFalse()
}
@Test
fun isSwipeToDismissEnabled_deviceLockedAndAuthMethodSwipe_true() =
testScope.runTest {
val isSwipeToDismissEnabled by collectLastValue(underTest.isSwipeToDismissEnabled)
utils.authenticationRepository.setUnlocked(false)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
assertThat(isSwipeToDismissEnabled).isTrue()
}
@Test
fun isSwipeToDismissEnabled_deviceUnlockedAndAuthMethodSwipe_false() =
testScope.runTest {
val isSwipeToDismissEnabled by collectLastValue(underTest.isSwipeToDismissEnabled)
utils.authenticationRepository.setUnlocked(true)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
assertThat(isSwipeToDismissEnabled).isFalse()
}
@Test
fun dismissLockScreen_deviceLockedWithSecureAuthMethod_switchesToBouncer() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
utils.authenticationRepository.setUnlocked(false)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen))
underTest.dismissLockscreen()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
@Test
fun dismissLockScreen_deviceUnlocked_switchesToGone() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
utils.authenticationRepository.setUnlocked(true)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen))
underTest.dismissLockscreen()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
}
@Test
fun dismissLockScreen_deviceLockedWithInsecureAuthMethod_switchesToGone() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
utils.authenticationRepository.setUnlocked(false)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen))
underTest.dismissLockscreen()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
}
@Test
fun switchFromLockScreenToGone_authMethodNotSwipe_doesNotUnlockDevice() =
testScope.runTest {
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
sceneInteractor.setCurrentScene(SceneModel(SceneKey.Lockscreen), "reason")
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
assertThat(isUnlocked).isFalse()
sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone), "reason")
assertThat(isUnlocked).isFalse()
}
@Test
fun switchFromNonLockScreenToGone_authMethodSwipe_doesNotUnlockDevice() =
testScope.runTest {
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
runCurrent()
sceneInteractor.setCurrentScene(SceneModel(SceneKey.Shade), "reason")
runCurrent()
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
runCurrent()
assertThat(isUnlocked).isFalse()
sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone), "reason")
assertThat(isUnlocked).isFalse()
}
@Test
fun authMethodChangedToNone_notOnLockScreenScene_doesNotDismissLockScreen() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
runCurrent()
sceneInteractor.setCurrentScene(SceneModel(SceneKey.QuickSettings), "reason")
runCurrent()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.QuickSettings))
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.QuickSettings))
}
}

View File

@@ -19,7 +19,7 @@ package com.android.systemui.keyguard.ui.viewmodel
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.authentication.data.model.AuthenticationMethodModel
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.scene.SceneTestUtils
@@ -49,14 +49,11 @@ class LockscreenSceneViewModelTest : SysuiTestCase() {
private val underTest =
LockscreenSceneViewModel(
applicationScope = testScope.backgroundScope,
interactor =
utils.lockScreenSceneInteractor(
authenticationInteractor = authenticationInteractor,
bouncerInteractor =
utils.bouncerInteractor(
authenticationInteractor = authenticationInteractor,
bouncerInteractor =
utils.bouncerInteractor(
authenticationInteractor = authenticationInteractor,
sceneInteractor = sceneInteractor,
),
sceneInteractor = sceneInteractor,
),
)
@@ -87,17 +84,18 @@ class LockscreenSceneViewModelTest : SysuiTestCase() {
}
@Test
fun upTransitionSceneKey_swipeToUnlockedEnabled_gone() =
fun upTransitionSceneKey_swipeToUnlockEnabled_gone() =
testScope.runTest {
val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
utils.authenticationRepository.setLockscreenEnabled(true)
utils.authenticationRepository.setUnlocked(false)
assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone)
}
@Test
fun upTransitionSceneKey_swipeToUnlockedNotEnabled_bouncer() =
fun upTransitionSceneKey_swipeToUnlockNotEnabled_bouncer() =
testScope.runTest {
val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)

View File

@@ -18,7 +18,7 @@ package com.android.systemui.qs.ui.viewmodel
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.authentication.data.model.AuthenticationMethodModel
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.scene.SceneTestUtils
import com.android.systemui.scene.shared.model.SceneKey
@@ -46,14 +46,10 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() {
private val underTest =
QuickSettingsSceneViewModel(
lockscreenSceneInteractor =
utils.lockScreenSceneInteractor(
bouncerInteractor =
utils.bouncerInteractor(
authenticationInteractor = authenticationInteractor,
bouncerInteractor =
utils.bouncerInteractor(
authenticationInteractor = authenticationInteractor,
sceneInteractor = sceneInteractor,
),
sceneInteractor = sceneInteractor,
),
)

View File

@@ -18,7 +18,7 @@ package com.android.systemui.shade.ui.viewmodel
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.authentication.data.model.AuthenticationMethodModel
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.scene.SceneTestUtils
import com.android.systemui.scene.shared.model.SceneKey
@@ -47,14 +47,11 @@ class ShadeSceneViewModelTest : SysuiTestCase() {
private val underTest =
ShadeSceneViewModel(
applicationScope = testScope.backgroundScope,
lockscreenSceneInteractor =
utils.lockScreenSceneInteractor(
authenticationInteractor = authenticationInteractor,
bouncerInteractor =
utils.bouncerInteractor(
authenticationInteractor = authenticationInteractor,
bouncerInteractor =
utils.bouncerInteractor(
authenticationInteractor = authenticationInteractor,
sceneInteractor = sceneInteractor,
),
sceneInteractor = sceneInteractor,
),
)

View File

@@ -20,7 +20,8 @@ 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.data.model.AuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
import com.android.systemui.authentication.shared.model.AuthenticationResultModel
import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel
import kotlinx.coroutines.flow.MutableStateFlow
@@ -47,7 +48,7 @@ class FakeAuthenticationRepository(
private val _authenticationMethod =
MutableStateFlow<AuthenticationMethodModel>(DEFAULT_AUTHENTICATION_METHOD)
val authenticationMethod: StateFlow<AuthenticationMethodModel> =
override val authenticationMethod: StateFlow<AuthenticationMethodModel> =
_authenticationMethod.asStateFlow()
private var isLockscreenEnabled = true
@@ -154,13 +155,13 @@ class FakeAuthenticationRepository(
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),
AuthenticationPatternCoordinate(2, 0),
AuthenticationPatternCoordinate(2, 1),
AuthenticationPatternCoordinate(2, 2),
AuthenticationPatternCoordinate(1, 1),
AuthenticationPatternCoordinate(0, 0),
AuthenticationPatternCoordinate(0, 1),
AuthenticationPatternCoordinate(0, 2),
)
const val MAX_FAILED_AUTH_TRIES_BEFORE_THROTTLING = 5
const val THROTTLE_DURATION_MS = 30000
@@ -172,7 +173,6 @@ class FakeAuthenticationRepository(
is AuthenticationMethodModel.Pin -> SecurityMode.PIN
is AuthenticationMethodModel.Password -> SecurityMode.Password
is AuthenticationMethodModel.Pattern -> SecurityMode.Pattern
is AuthenticationMethodModel.Swipe,
is AuthenticationMethodModel.None -> SecurityMode.None
}
}
@@ -208,8 +208,7 @@ class FakeAuthenticationRepository(
}
}
private fun List<AuthenticationMethodModel.Pattern.PatternCoordinate>.toCells():
List<LockPatternView.Cell> {
private fun List<AuthenticationPatternCoordinate>.toCells(): List<LockPatternView.Cell> {
return map { coordinate -> LockPatternView.Cell.of(coordinate.y, coordinate.x) }
}
}

View File

@@ -32,7 +32,6 @@ import com.android.systemui.keyguard.data.repository.FakeCommandQueue
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.LockscreenSceneInteractor
import com.android.systemui.keyguard.shared.model.WakeSleepReason
import com.android.systemui.keyguard.shared.model.WakefulnessModel
import com.android.systemui.keyguard.shared.model.WakefulnessState
@@ -176,23 +175,14 @@ class SceneTestUtils(
fun bouncerViewModel(
bouncerInteractor: BouncerInteractor,
authenticationInteractor: AuthenticationInteractor,
): BouncerViewModel {
return BouncerViewModel(
applicationContext = context,
applicationScope = applicationScope(),
interactor = bouncerInteractor,
featureFlags = featureFlags,
)
}
fun lockScreenSceneInteractor(
authenticationInteractor: AuthenticationInteractor,
bouncerInteractor: BouncerInteractor,
): LockscreenSceneInteractor {
return LockscreenSceneInteractor(
applicationScope = applicationScope(),
authenticationInteractor = authenticationInteractor,
bouncerInteractor = bouncerInteractor,
authenticationInteractor = authenticationInteractor,
featureFlags = featureFlags,
)
}