Merge changes Ia719fce3,If1a64a93,I90b025f5,I54449898,I32b7b585 into udc-dev

* changes:
  [flexiglass] Foundation for placeholder QS scene.
  [flexiglass] Foundation for placeholder shade scene.
  [flexiglass] Foundation for placeholder lock screen scene.
  [flexiglass] Foundation for placeholder bouncer scene.
  [flexiglass] Authentication logic.
This commit is contained in:
Ale Nijamkin
2023-05-11 18:56:56 +00:00
committed by Android (Google) Code Review
18 changed files with 2293 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
/*
* 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.authentication
import com.android.systemui.authentication.data.repository.AuthenticationRepositoryModule
import dagger.Module
@Module(
includes =
[
AuthenticationRepositoryModule::class,
],
)
object AuthenticationModule

View File

@@ -0,0 +1,94 @@
/*
* 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.authentication.data.repository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import dagger.Binds
import dagger.Module
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/** Defines interface for classes that can access authentication-related application state. */
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 lock screen is showing or dismissed.
*/
val isUnlocked: StateFlow<Boolean>
/**
* The currently-configured authentication method. This determines how the authentication
* challenge is completed in order to unlock an otherwise locked device.
*/
val authenticationMethod: StateFlow<AuthenticationMethodModel>
/**
* Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically
* dismisses once the authentication challenge is completed. For example, completing a biometric
* authentication challenge via face unlock or fingerprint sensor can automatically bypass the
* lock screen.
*/
val isBypassEnabled: StateFlow<Boolean>
/** See [isUnlocked]. */
fun setUnlocked(isUnlocked: Boolean)
/** See [authenticationMethod]. */
fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel)
/** See [isBypassEnabled]. */
fun setBypassEnabled(isBypassEnabled: Boolean)
}
class AuthenticationRepositoryImpl @Inject constructor() : AuthenticationRepository {
// TODO(b/280883900): get data from real data sources in SysUI.
private val _isUnlocked = MutableStateFlow(false)
override val isUnlocked: StateFlow<Boolean> = _isUnlocked.asStateFlow()
private val _authenticationMethod =
MutableStateFlow<AuthenticationMethodModel>(AuthenticationMethodModel.PIN(1234))
override val authenticationMethod: StateFlow<AuthenticationMethodModel> =
_authenticationMethod.asStateFlow()
private val _isBypassEnabled = MutableStateFlow(false)
override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled.asStateFlow()
override fun setUnlocked(isUnlocked: Boolean) {
_isUnlocked.value = isUnlocked
}
override fun setBypassEnabled(isBypassEnabled: Boolean) {
_isBypassEnabled.value = isBypassEnabled
}
override fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) {
_authenticationMethod.value = authenticationMethod
}
}
@Module
interface AuthenticationRepositoryModule {
@Binds fun repository(impl: AuthenticationRepositoryImpl): AuthenticationRepository
}

View File

@@ -0,0 +1,204 @@
/*
* 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.authentication.domain.interactor
import com.android.systemui.authentication.data.repository.AuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
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.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
/** Hosts application business logic related to authentication. */
@SysUISingleton
class AuthenticationInteractor
@Inject
constructor(
@Application applicationScope: CoroutineScope,
private val repository: AuthenticationRepository,
) {
/**
* The currently-configured authentication method. This determines how the authentication
* challenge is completed in order to unlock an otherwise locked device.
*/
val authenticationMethod: StateFlow<AuthenticationMethodModel> = repository.authenticationMethod
/**
* 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.
*/
val isUnlocked: StateFlow<Boolean> =
combine(authenticationMethod, repository.isUnlocked) { authMethod, isUnlocked ->
isUnlockedWithAuthMethod(
isUnlocked = isUnlocked,
authMethod = authMethod,
)
}
.stateIn(
scope = applicationScope,
started = SharingStarted.Eagerly,
initialValue =
isUnlockedWithAuthMethod(
isUnlocked = repository.isUnlocked.value,
authMethod = repository.authenticationMethod.value,
)
)
/**
* Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically
* dismisses once the authentication challenge is completed. For example, completing a biometric
* authentication challenge via face unlock or fingerprint sensor can automatically bypass the
* lock screen.
*/
val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled
init {
// UNLOCKS WHEN AUTH METHOD REMOVED.
//
// Unlocks the device if the auth method becomes None.
applicationScope.launch {
repository.authenticationMethod.collect {
if (it is AuthenticationMethodModel.None) {
unlockDevice()
}
}
}
}
/**
* Returns `true` if the device currently requires authentication before content can be viewed;
* `false` if content can be displayed without unlocking first.
*/
fun isAuthenticationRequired(): Boolean {
return !isUnlocked.value && authenticationMethod.value.isSecure
}
/**
* Unlocks the device, assuming that the authentication challenge has been completed
* successfully.
*/
fun unlockDevice() {
repository.setUnlocked(true)
}
/**
* Locks the device. From now on, the device will remain locked until [authenticate] is called
* with the correct input.
*/
fun lockDevice() {
repository.setUnlocked(false)
}
/**
* Attempts to authenticate the user and unlock the device.
*
* @param input The input from the user to try to authenticate with. This can be a list of
* different things, based on the current authentication method.
* @return `true` if the authentication succeeded and the device is now unlocked; `false`
* otherwise.
*/
fun authenticate(input: List<Any>): Boolean {
val isSuccessful =
when (val authMethod = this.authenticationMethod.value) {
is AuthenticationMethodModel.PIN -> input.asCode() == authMethod.code
is AuthenticationMethodModel.Password -> input.asPassword() == authMethod.password
is AuthenticationMethodModel.Pattern -> input.asPattern() == authMethod.coordinates
else -> true
}
if (isSuccessful) {
repository.setUnlocked(true)
}
return isSuccessful
}
/** Triggers a biometric-powered unlock of the device. */
fun biometricUnlock() {
// TODO(b/280883900): only allow this if the biometric is enabled and there's a match.
repository.setUnlocked(true)
}
/** See [authenticationMethod]. */
fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) {
repository.setAuthenticationMethod(authenticationMethod)
}
/** See [isBypassEnabled]. */
fun toggleBypassEnabled() {
repository.setBypassEnabled(!repository.isBypassEnabled.value)
}
companion object {
private fun isUnlockedWithAuthMethod(
isUnlocked: Boolean,
authMethod: AuthenticationMethodModel,
): Boolean {
return if (authMethod is AuthenticationMethodModel.None) {
true
} else {
isUnlocked
}
}
/**
* Returns a PIN code from the given list. It's assumed the given list elements are all
* [Int].
*/
private fun List<Any>.asCode(): Int? {
if (isEmpty()) {
return null
}
var code = 0
map { it as Int }.forEach { integer -> code = code * 10 + integer }
return code
}
/**
* 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) } }
}
/**
* 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) }
}
}
}
}

View File

@@ -0,0 +1,47 @@
/*
* 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.authentication.shared.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)
/** The most basic authentication method. The lock screen can be swiped away when displayed. */
object Swipe : AuthenticationMethodModel(isSecure = false)
data class PIN(val code: Int) : AuthenticationMethodModel(isSecure = true)
data class Password(val password: String) : AuthenticationMethodModel(isSecure = true)
data class Pattern(val coordinates: List<PatternCoordinate>) :
AuthenticationMethodModel(isSecure = true) {
data class PatternCoordinate(
val x: Int,
val y: Int,
)
}
}

View File

@@ -0,0 +1,35 @@
/*
* 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.data.repo
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/** Provides access to bouncer-related application state. */
@SysUISingleton
class BouncerRepository @Inject constructor() {
private val _message = MutableStateFlow<String?>(null)
/** The user-facing message to show in the bouncer. */
val message: StateFlow<String?> = _message.asStateFlow()
fun setMessage(message: String?) {
_message.value = message
}
}

View File

@@ -0,0 +1,172 @@
/*
* 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.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.bouncer.data.repo.BouncerRepository
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 dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
/** Encapsulates business logic and application state accessing use-cases. */
class BouncerInteractor
@AssistedInject
constructor(
@Application private val applicationScope: CoroutineScope,
@Application private val applicationContext: Context,
private val repository: BouncerRepository,
private val authenticationInteractor: AuthenticationInteractor,
private val sceneInteractor: SceneInteractor,
@Assisted private val containerName: String,
) {
/** The user-facing message to show in the bouncer. */
val message: StateFlow<String?> = repository.message
/**
* The currently-configured authentication method. This determines how the authentication
* challenge is completed in order to unlock an otherwise locked device.
*/
val authenticationMethod: StateFlow<AuthenticationMethodModel> =
authenticationInteractor.authenticationMethod
init {
applicationScope.launch {
combine(
sceneInteractor.currentScene(containerName),
authenticationInteractor.authenticationMethod,
::Pair,
)
.collect { (currentScene, authMethod) ->
if (currentScene.key == SceneKey.Bouncer) {
when (authMethod) {
is AuthenticationMethodModel.None ->
sceneInteractor.setCurrentScene(
containerName,
SceneModel(SceneKey.Gone),
)
is AuthenticationMethodModel.Swipe ->
sceneInteractor.setCurrentScene(
containerName,
SceneModel(SceneKey.LockScreen),
)
else -> Unit
}
}
}
}
}
/**
* Either shows the bouncer or unlocks the device, if the bouncer doesn't need to be shown.
*
* @param containerName The name of the scene container to show the bouncer in.
* @param message An optional message to show to the user in the bouncer.
*/
fun showOrUnlockDevice(
containerName: String,
message: String? = null,
) {
if (authenticationInteractor.isAuthenticationRequired()) {
repository.setMessage(message ?: promptMessage(authenticationMethod.value))
sceneInteractor.setCurrentScene(
containerName = containerName,
scene = SceneModel(SceneKey.Bouncer),
)
} else {
authenticationInteractor.unlockDevice()
sceneInteractor.setCurrentScene(
containerName = containerName,
scene = SceneModel(SceneKey.Gone),
)
}
}
/**
* Resets the user-facing message back to the default according to the current authentication
* method.
*/
fun resetMessage() {
repository.setMessage(promptMessage(authenticationMethod.value))
}
/** Removes the user-facing message. */
fun clearMessage() {
repository.setMessage(null)
}
/**
* Attempts to authenticate based on the given user input.
*
* If the input is correct, the device will be unlocked and the lock screen and bouncer will be
* dismissed and hidden.
*/
fun authenticate(
input: List<Any>,
) {
val isAuthenticated = authenticationInteractor.authenticate(input)
if (isAuthenticated) {
sceneInteractor.setCurrentScene(
containerName = containerName,
scene = SceneModel(SceneKey.Gone),
)
} else {
repository.setMessage(errorMessage(authenticationMethod.value))
}
}
private fun promptMessage(authMethod: AuthenticationMethodModel): String {
return when (authMethod) {
is AuthenticationMethodModel.PIN ->
applicationContext.getString(R.string.keyguard_enter_your_pin)
is AuthenticationMethodModel.Password ->
applicationContext.getString(R.string.keyguard_enter_your_password)
is AuthenticationMethodModel.Pattern ->
applicationContext.getString(R.string.keyguard_enter_your_pattern)
else -> ""
}
}
private fun errorMessage(authMethod: AuthenticationMethodModel): String {
return when (authMethod) {
is AuthenticationMethodModel.PIN -> applicationContext.getString(R.string.kg_wrong_pin)
is AuthenticationMethodModel.Password ->
applicationContext.getString(R.string.kg_wrong_password)
is AuthenticationMethodModel.Pattern ->
applicationContext.getString(R.string.kg_wrong_pattern)
else -> ""
}
}
@AssistedFactory
interface Factory {
fun create(
containerName: String,
): BouncerInteractor
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.ui.viewmodel
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.dagger.qualifiers.Application
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
/** Holds UI state and handles user input on bouncer UIs. */
class BouncerViewModel
@AssistedInject
constructor(
@Application private val applicationScope: CoroutineScope,
interactorFactory: BouncerInteractor.Factory,
containerName: String,
) {
private val interactor: BouncerInteractor = interactorFactory.create(containerName)
/** The user-facing message to show in the bouncer. */
val message: StateFlow<String> =
interactor.message
.map { it ?: "" }
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
initialValue = interactor.message.value ?: "",
)
/** Notifies that the authenticate button was clicked. */
fun onAuthenticateButtonClicked() {
// TODO(b/280877228): remove this and send the real input.
interactor.authenticate(
when (interactor.authenticationMethod.value) {
is AuthenticationMethodModel.PIN -> listOf(1, 2, 3, 4)
is AuthenticationMethodModel.Password -> "password".toList()
is AuthenticationMethodModel.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),
)
else -> emptyList()
}
)
}
/** Notifies that the emergency services button was clicked. */
fun onEmergencyServicesButtonClicked() {
// TODO(b/280877228): implement this.
}
}

View File

@@ -32,6 +32,7 @@ import com.android.systemui.accessibility.AccessibilityModule;
import com.android.systemui.accessibility.data.repository.AccessibilityRepositoryModule;
import com.android.systemui.appops.dagger.AppOpsModule;
import com.android.systemui.assist.AssistModule;
import com.android.systemui.authentication.AuthenticationModule;
import com.android.systemui.biometrics.AlternateUdfpsTouchProvider;
import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider;
import com.android.systemui.biometrics.FingerprintReEnrollNotification;
@@ -153,6 +154,7 @@ import javax.inject.Named;
AccessibilityRepositoryModule.class,
AppOpsModule.class,
AssistModule.class,
AuthenticationModule.class,
BiometricsModule.class,
BouncerViewModule.class,
ClipboardOverlayModule.class,

View File

@@ -0,0 +1,186 @@
/*
* 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.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 kotlinx.coroutines.CoroutineScope
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
import kotlinx.coroutines.launch
/** Hosts business and application state accessing logic for the lock screen scene. */
class LockScreenSceneInteractor
@AssistedInject
constructor(
@Application applicationScope: CoroutineScope,
private val authenticationInteractor: AuthenticationInteractor,
bouncerInteractorFactory: BouncerInteractor.Factory,
private val sceneInteractor: SceneInteractor,
@Assisted private val containerName: String,
) {
private val bouncerInteractor: BouncerInteractor =
bouncerInteractorFactory.create(containerName)
/** 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 lock screen. */
val isSwipeToDismissEnabled: StateFlow<Boolean> =
combine(
authenticationInteractor.isUnlocked,
authenticationInteractor.authenticationMethod,
) { isUnlocked, authMethod ->
isSwipeToUnlockEnabled(
isUnlocked = isUnlocked,
authMethod = authMethod,
)
}
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
initialValue =
isSwipeToUnlockEnabled(
isUnlocked = authenticationInteractor.isUnlocked.value,
authMethod = authenticationInteractor.authenticationMethod.value,
),
)
init {
// LOCKING SHOWS LOCK SCREEN.
//
// Move to the lock screen scene if the device becomes locked while in any scene.
applicationScope.launch {
authenticationInteractor.isUnlocked
.map { !it }
.distinctUntilChanged()
.collect { isLocked ->
if (isLocked) {
sceneInteractor.setCurrentScene(
containerName = containerName,
scene = SceneModel(SceneKey.LockScreen),
)
}
}
}
// BYPASS UNLOCK.
//
// Moves to the gone scene if bypass is enabled and the device becomes unlocked while in the
// lock screen scene.
applicationScope.launch {
combine(
authenticationInteractor.isBypassEnabled,
authenticationInteractor.isUnlocked,
sceneInteractor.currentScene(containerName),
::Triple,
)
.collect { (isBypassEnabled, isUnlocked, currentScene) ->
if (isBypassEnabled && isUnlocked && currentScene.key == SceneKey.LockScreen) {
sceneInteractor.setCurrentScene(
containerName = containerName,
scene = SceneModel(SceneKey.Gone),
)
}
}
}
// SWIPE TO DISMISS LOCK SCREEN.
//
// If switched from the lock screen to the gone scene and the auth method was a swipe,
// unlocks the device.
applicationScope.launch {
combine(
authenticationInteractor.authenticationMethod,
sceneInteractor.currentScene(containerName).pairwise(),
::Pair,
)
.collect { (authMethod, scenes) ->
val (previousScene, currentScene) = scenes
if (
authMethod is AuthenticationMethodModel.Swipe &&
previousScene.key == SceneKey.LockScreen &&
currentScene.key == SceneKey.Gone
) {
authenticationInteractor.unlockDevice()
}
}
}
// DISMISS LOCK SCREEN IF AUTH METHOD IS REMOVED.
//
// If the auth method becomes None while on the lock screen scene, dismisses the lock
// screen.
applicationScope.launch {
combine(
authenticationInteractor.authenticationMethod,
sceneInteractor.currentScene(containerName),
::Pair,
)
.collect { (authMethod, scene) ->
if (
scene.key == SceneKey.LockScreen &&
authMethod == AuthenticationMethodModel.None
) {
sceneInteractor.setCurrentScene(
containerName = containerName,
scene = SceneModel(SceneKey.Gone),
)
}
}
}
}
/** Attempts to dismiss the lock screen. This will cause the bouncer to show, if needed. */
fun dismissLockScreen() {
bouncerInteractor.showOrUnlockDevice(containerName = containerName)
}
private fun isSwipeToUnlockEnabled(
isUnlocked: Boolean,
authMethod: AuthenticationMethodModel,
): Boolean {
return !isUnlocked && authMethod is AuthenticationMethodModel.Swipe
}
@AssistedFactory
interface Factory {
fun create(
containerName: String,
): LockScreenSceneInteractor
}
}

View File

@@ -0,0 +1,108 @@
/*
* 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.ui.viewmodel
import com.android.systemui.R
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor
import com.android.systemui.scene.shared.model.SceneKey
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
/** Models UI state and handles user input for the lock screen scene. */
class LockScreenSceneViewModel
@AssistedInject
constructor(
@Application applicationScope: CoroutineScope,
interactorFactory: LockScreenSceneInteractor.Factory,
@Assisted containerName: String,
) {
private val interactor: LockScreenSceneInteractor = interactorFactory.create(containerName)
/** The icon for the "lock" button on the lock screen. */
val lockButtonIcon: StateFlow<Icon> =
interactor.isDeviceLocked
.map { isLocked -> lockIcon(isLocked = isLocked) }
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
initialValue = lockIcon(isLocked = interactor.isDeviceLocked.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),
)
/** Notifies that the lock button on the lock screen was clicked. */
fun onLockButtonClicked() {
interactor.dismissLockScreen()
}
/** Notifies that some content on the lock screen was clicked. */
fun onContentClicked() {
interactor.dismissLockScreen()
}
private fun upDestinationSceneKey(
isSwipeToUnlockEnabled: Boolean,
): SceneKey {
return if (isSwipeToUnlockEnabled) SceneKey.Gone else SceneKey.Bouncer
}
private fun lockIcon(
isLocked: Boolean,
): Icon {
return Icon.Resource(
res =
if (isLocked) {
R.drawable.ic_device_lock_on
} else {
R.drawable.ic_device_lock_off
},
contentDescription =
ContentDescription.Resource(
res =
if (isLocked) {
R.string.accessibility_lock_icon
} else {
R.string.accessibility_unlock_button
}
)
)
}
@AssistedFactory
interface Factory {
fun create(
containerName: String,
): LockScreenSceneViewModel
}
}

View File

@@ -0,0 +1,45 @@
/*
* 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.qs.ui.viewmodel
import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
/** Models UI state and handles user input for the quick settings scene. */
class QuickSettingsSceneViewModel
@AssistedInject
constructor(
lockScreenSceneInteractorFactory: LockScreenSceneInteractor.Factory,
@Assisted containerName: String,
) {
private val lockScreenSceneInteractor: LockScreenSceneInteractor =
lockScreenSceneInteractorFactory.create(containerName)
/** Notifies that some content in quick settings was clicked. */
fun onContentClicked() {
lockScreenSceneInteractor.dismissLockScreen()
}
@AssistedFactory
interface Factory {
fun create(
containerName: String,
): QuickSettingsSceneViewModel
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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.shade.ui.viewmodel
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor
import com.android.systemui.scene.shared.model.SceneKey
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
/** Models UI state and handles user input for the shade scene. */
class ShadeSceneViewModel
@AssistedInject
constructor(
@Application private val applicationScope: CoroutineScope,
lockScreenSceneInteractorFactory: LockScreenSceneInteractor.Factory,
@Assisted private val containerName: String,
) {
private val lockScreenInteractor: LockScreenSceneInteractor =
lockScreenSceneInteractorFactory.create(containerName)
/** The key of the scene we should switch to when swiping up. */
val upDestinationSceneKey: StateFlow<SceneKey> =
lockScreenInteractor.isDeviceLocked
.map { isLocked -> upDestinationSceneKey(isLocked = isLocked) }
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
initialValue =
upDestinationSceneKey(
isLocked = lockScreenInteractor.isDeviceLocked.value,
),
)
/** Notifies that some content in the shade was clicked. */
fun onContentClicked() {
lockScreenInteractor.dismissLockScreen()
}
private fun upDestinationSceneKey(
isLocked: Boolean,
): SceneKey {
return if (isLocked) SceneKey.LockScreen else SceneKey.Gone
}
@AssistedFactory
interface Factory {
fun create(
containerName: String,
): ShadeSceneViewModel
}
}

View File

@@ -0,0 +1,291 @@
/*
* 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.authentication.domain.interactor
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.AuthenticationRepositoryImpl
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.coroutines.collectLastValue
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class AuthenticationInteractorTest : SysuiTestCase() {
private val testScope = TestScope()
private val repository: AuthenticationRepository = AuthenticationRepositoryImpl()
private val underTest =
AuthenticationInteractor(
applicationScope = testScope.backgroundScope,
repository = repository,
)
@Test
fun authMethod() =
testScope.runTest {
val authMethod by collectLastValue(underTest.authenticationMethod)
assertThat(authMethod).isEqualTo(AuthenticationMethodModel.PIN(1234))
underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Password("password"))
}
@Test
fun isUnlocked_whenAuthMethodIsNone_isTrue() =
testScope.runTest {
val isUnlocked by collectLastValue(underTest.isUnlocked)
assertThat(isUnlocked).isFalse()
underTest.setAuthenticationMethod(AuthenticationMethodModel.None)
assertThat(isUnlocked).isTrue()
}
@Test
fun unlockDevice() =
testScope.runTest {
val isUnlocked by collectLastValue(underTest.isUnlocked)
assertThat(isUnlocked).isFalse()
underTest.unlockDevice()
runCurrent()
assertThat(isUnlocked).isTrue()
}
@Test
fun biometricUnlock() =
testScope.runTest {
val isUnlocked by collectLastValue(underTest.isUnlocked)
assertThat(isUnlocked).isFalse()
underTest.biometricUnlock()
runCurrent()
assertThat(isUnlocked).isTrue()
}
@Test
fun toggleBypassEnabled() =
testScope.runTest {
val isBypassEnabled by collectLastValue(underTest.isBypassEnabled)
assertThat(isBypassEnabled).isFalse()
underTest.toggleBypassEnabled()
assertThat(isBypassEnabled).isTrue()
underTest.toggleBypassEnabled()
assertThat(isBypassEnabled).isFalse()
}
@Test
fun isAuthenticationRequired_lockedAndSecured_true() =
testScope.runTest {
underTest.lockDevice()
runCurrent()
underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
assertThat(underTest.isAuthenticationRequired()).isTrue()
}
@Test
fun isAuthenticationRequired_lockedAndNotSecured_false() =
testScope.runTest {
underTest.lockDevice()
runCurrent()
underTest.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
assertThat(underTest.isAuthenticationRequired()).isFalse()
}
@Test
fun isAuthenticationRequired_unlockedAndSecured_false() =
testScope.runTest {
underTest.unlockDevice()
runCurrent()
underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
assertThat(underTest.isAuthenticationRequired()).isFalse()
}
@Test
fun isAuthenticationRequired_unlockedAndNotSecured_false() =
testScope.runTest {
underTest.unlockDevice()
runCurrent()
underTest.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
assertThat(underTest.isAuthenticationRequired()).isFalse()
}
@Test
fun authenticate_withCorrectPin_returnsTrueAndUnlocksDevice() =
testScope.runTest {
val isUnlocked by collectLastValue(underTest.isUnlocked)
underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
assertThat(isUnlocked).isFalse()
assertThat(underTest.authenticate(listOf(1, 2, 3, 4))).isTrue()
assertThat(isUnlocked).isTrue()
}
@Test
fun authenticate_withIncorrectPin_returnsFalseAndDoesNotUnlockDevice() =
testScope.runTest {
val isUnlocked by collectLastValue(underTest.isUnlocked)
underTest.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
assertThat(isUnlocked).isFalse()
assertThat(underTest.authenticate(listOf(9, 8, 7))).isFalse()
assertThat(isUnlocked).isFalse()
}
@Test
fun authenticate_withCorrectPassword_returnsTrueAndUnlocksDevice() =
testScope.runTest {
val isUnlocked by collectLastValue(underTest.isUnlocked)
underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
assertThat(isUnlocked).isFalse()
assertThat(underTest.authenticate("password".toList())).isTrue()
assertThat(isUnlocked).isTrue()
}
@Test
fun authenticate_withIncorrectPassword_returnsFalseAndDoesNotUnlockDevice() =
testScope.runTest {
val isUnlocked by collectLastValue(underTest.isUnlocked)
underTest.setAuthenticationMethod(AuthenticationMethodModel.Password("password"))
assertThat(isUnlocked).isFalse()
assertThat(underTest.authenticate("alohomora".toList())).isFalse()
assertThat(isUnlocked).isFalse()
}
@Test
fun authenticate_withCorrectPattern_returnsTrueAndUnlocksDevice() =
testScope.runTest {
val isUnlocked by collectLastValue(underTest.isUnlocked)
underTest.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,
),
)
)
)
assertThat(isUnlocked).isFalse()
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(isUnlocked).isTrue()
}
@Test
fun authenticate_withIncorrectPattern_returnsFalseAndDoesNotUnlockDevice() =
testScope.runTest {
val isUnlocked by collectLastValue(underTest.isUnlocked)
underTest.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,
),
)
)
)
assertThat(isUnlocked).isFalse()
assertThat(
underTest.authenticate(
listOf(
AuthenticationMethodModel.Pattern.PatternCoordinate(
x = 2,
y = 0,
),
AuthenticationMethodModel.Pattern.PatternCoordinate(
x = 2,
y = 1,
),
AuthenticationMethodModel.Pattern.PatternCoordinate(
x = 2,
y = 2,
),
)
)
)
.isFalse()
assertThat(isUnlocked).isFalse()
}
@Test
fun unlocksDevice_whenAuthMethodBecomesNone() =
testScope.runTest {
val isUnlocked by collectLastValue(underTest.isUnlocked)
assertThat(isUnlocked).isFalse()
repository.setAuthenticationMethod(AuthenticationMethodModel.None)
assertThat(isUnlocked).isTrue()
}
}

View File

@@ -0,0 +1,223 @@
/*
* 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.domain.interactor
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.data.repo.BouncerRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.scene.data.repository.fakeSceneContainerRepository
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.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class BouncerInteractorTest : SysuiTestCase() {
private val testScope = TestScope()
private val authenticationInteractor =
AuthenticationInteractor(
applicationScope = testScope.backgroundScope,
repository = AuthenticationRepositoryImpl(),
)
private val sceneInteractor =
SceneInteractor(
repository = fakeSceneContainerRepository(),
)
private val underTest =
BouncerInteractor(
applicationScope = testScope.backgroundScope,
applicationContext = context,
repository = BouncerRepository(),
authenticationInteractor = authenticationInteractor,
sceneInteractor = sceneInteractor,
containerName = "container1",
)
@Before
fun setUp() {
overrideResource(R.string.keyguard_enter_your_pin, MESSAGE_ENTER_YOUR_PIN)
overrideResource(R.string.keyguard_enter_your_password, MESSAGE_ENTER_YOUR_PASSWORD)
overrideResource(R.string.keyguard_enter_your_pattern, MESSAGE_ENTER_YOUR_PATTERN)
overrideResource(R.string.kg_wrong_pin, MESSAGE_WRONG_PIN)
overrideResource(R.string.kg_wrong_password, MESSAGE_WRONG_PASSWORD)
overrideResource(R.string.kg_wrong_pattern, MESSAGE_WRONG_PATTERN)
}
@Test
fun pinAuthMethod() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
val message by collectLastValue(underTest.message)
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.lockDevice()
underTest.showOrUnlockDevice("container1")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
underTest.clearMessage()
assertThat(message).isNull()
underTest.resetMessage()
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
// Wrong input.
underTest.authenticate(listOf(9, 8, 7))
assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
underTest.resetMessage()
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
// Correct input.
underTest.authenticate(listOf(1, 2, 3, 4))
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
}
@Test
fun passwordAuthMethod() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
val message by collectLastValue(underTest.message)
authenticationInteractor.setAuthenticationMethod(
AuthenticationMethodModel.Password("password")
)
authenticationInteractor.lockDevice()
underTest.showOrUnlockDevice("container1")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
underTest.clearMessage()
assertThat(message).isNull()
underTest.resetMessage()
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
// Wrong input.
underTest.authenticate("alohamora".toList())
assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
underTest.resetMessage()
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
// Correct input.
underTest.authenticate("password".toList())
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
}
@Test
fun patternAuthMethod() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
val message by collectLastValue(underTest.message)
authenticationInteractor.setAuthenticationMethod(
AuthenticationMethodModel.Pattern(emptyList())
)
authenticationInteractor.lockDevice()
underTest.showOrUnlockDevice("container1")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
underTest.clearMessage()
assertThat(message).isNull()
underTest.resetMessage()
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
// Wrong input.
underTest.authenticate(
listOf(AuthenticationMethodModel.Pattern.PatternCoordinate(3, 4))
)
assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
underTest.resetMessage()
assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
// Correct input.
underTest.authenticate(emptyList())
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
}
@Test
fun showOrUnlockDevice_notLocked_switchesToGoneScene() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
authenticationInteractor.unlockDevice()
runCurrent()
underTest.showOrUnlockDevice("container1")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
}
@Test
fun showOrUnlockDevice_authMethodNotSecure_switchesToGoneScene() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
authenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
authenticationInteractor.lockDevice()
underTest.showOrUnlockDevice("container1")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
}
@Test
fun showOrUnlockDevice_customMessageShown() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene("container1"))
val message by collectLastValue(underTest.message)
authenticationInteractor.setAuthenticationMethod(
AuthenticationMethodModel.Password("password")
)
authenticationInteractor.lockDevice()
val customMessage = "Hello there!"
underTest.showOrUnlockDevice("container1", customMessage)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
assertThat(message).isEqualTo(customMessage)
}
companion object {
private const val MESSAGE_ENTER_YOUR_PIN = "Enter your PIN"
private const val MESSAGE_ENTER_YOUR_PASSWORD = "Enter your password"
private const val MESSAGE_ENTER_YOUR_PATTERN = "Enter your pattern"
private const val MESSAGE_WRONG_PIN = "Wrong PIN"
private const val MESSAGE_WRONG_PASSWORD = "Wrong password"
private const val MESSAGE_WRONG_PATTERN = "Wrong pattern"
}
}

View File

@@ -0,0 +1,270 @@
/*
* 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.data.repository.AuthenticationRepositoryImpl
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.data.repo.BouncerRepository
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.scene.data.repository.fakeSceneContainerRepository
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.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class LockScreenSceneInteractorTest : SysuiTestCase() {
private val testScope = TestScope()
private val sceneInteractor =
SceneInteractor(
repository = fakeSceneContainerRepository(),
)
private val mAuthenticationInteractor =
AuthenticationInteractor(
applicationScope = testScope.backgroundScope,
repository = AuthenticationRepositoryImpl(),
)
private val underTest =
LockScreenSceneInteractor(
applicationScope = testScope.backgroundScope,
authenticationInteractor = mAuthenticationInteractor,
bouncerInteractorFactory =
object : BouncerInteractor.Factory {
override fun create(containerName: String): BouncerInteractor {
return BouncerInteractor(
applicationScope = testScope.backgroundScope,
applicationContext = context,
repository = BouncerRepository(),
authenticationInteractor = mAuthenticationInteractor,
sceneInteractor = sceneInteractor,
containerName = containerName,
)
}
},
sceneInteractor = sceneInteractor,
containerName = CONTAINER_NAME,
)
@Test
fun isDeviceLocked() =
testScope.runTest {
val isDeviceLocked by collectLastValue(underTest.isDeviceLocked)
mAuthenticationInteractor.lockDevice()
assertThat(isDeviceLocked).isTrue()
mAuthenticationInteractor.unlockDevice()
assertThat(isDeviceLocked).isFalse()
}
@Test
fun isSwipeToDismissEnabled_deviceLockedAndAuthMethodSwipe_true() =
testScope.runTest {
val isSwipeToDismissEnabled by collectLastValue(underTest.isSwipeToDismissEnabled)
mAuthenticationInteractor.lockDevice()
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
assertThat(isSwipeToDismissEnabled).isTrue()
}
@Test
fun isSwipeToDismissEnabled_deviceUnlockedAndAuthMethodSwipe_false() =
testScope.runTest {
val isSwipeToDismissEnabled by collectLastValue(underTest.isSwipeToDismissEnabled)
mAuthenticationInteractor.unlockDevice()
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
assertThat(isSwipeToDismissEnabled).isFalse()
}
@Test
fun dismissLockScreen_deviceLockedWithSecureAuthMethod_switchesToBouncer() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
mAuthenticationInteractor.lockDevice()
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
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(CONTAINER_NAME))
mAuthenticationInteractor.unlockDevice()
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
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(CONTAINER_NAME))
mAuthenticationInteractor.lockDevice()
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
underTest.dismissLockScreen()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
}
@Test
fun deviceLockedInNonLockScreenScene_switchesToLockScreenScene() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
runCurrent()
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone))
runCurrent()
mAuthenticationInteractor.unlockDevice()
runCurrent()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
mAuthenticationInteractor.lockDevice()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
}
@Test
fun deviceBiometricUnlockedInLockScreen_bypassEnabled_switchesToGone() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
mAuthenticationInteractor.lockDevice()
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen))
if (!mAuthenticationInteractor.isBypassEnabled.value) {
mAuthenticationInteractor.toggleBypassEnabled()
}
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
mAuthenticationInteractor.biometricUnlock()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
}
@Test
fun deviceBiometricUnlockedInLockScreen_bypassNotEnabled_doesNotSwitch() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
mAuthenticationInteractor.lockDevice()
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen))
if (mAuthenticationInteractor.isBypassEnabled.value) {
mAuthenticationInteractor.toggleBypassEnabled()
}
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
mAuthenticationInteractor.biometricUnlock()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
}
@Test
fun switchFromLockScreenToGone_authMethodSwipe_unlocksDevice() =
testScope.runTest {
val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked)
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen))
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
assertThat(isUnlocked).isFalse()
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone))
assertThat(isUnlocked).isTrue()
}
@Test
fun switchFromLockScreenToGone_authMethodNotSwipe_doesNotUnlockDevice() =
testScope.runTest {
val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked)
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen))
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
assertThat(isUnlocked).isFalse()
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone))
assertThat(isUnlocked).isFalse()
}
@Test
fun switchFromNonLockScreenToGone_authMethodSwipe_doesNotUnlockDevice() =
testScope.runTest {
val isUnlocked by collectLastValue(mAuthenticationInteractor.isUnlocked)
runCurrent()
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Shade))
runCurrent()
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
runCurrent()
assertThat(isUnlocked).isFalse()
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.Gone))
assertThat(isUnlocked).isFalse()
}
@Test
fun authMethodChangedToNone_onLockScreenScene_dismissesLockScreen() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.LockScreen))
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen))
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.None)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
}
@Test
fun authMethodChangedToNone_notOnLockScreenScene_doesNotDismissLockScreen() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
runCurrent()
sceneInteractor.setCurrentScene(CONTAINER_NAME, SceneModel(SceneKey.QuickSettings))
runCurrent()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.QuickSettings))
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.None)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.QuickSettings))
}
companion object {
private const val CONTAINER_NAME = "container1"
}
}

View File

@@ -0,0 +1,190 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.keyguard.ui.viewmodel
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.data.repo.BouncerRepository
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor
import com.android.systemui.scene.data.repository.fakeSceneContainerRepository
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.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class LockScreenSceneViewModelTest : SysuiTestCase() {
private val testScope = TestScope()
private val sceneInteractor =
SceneInteractor(
repository = fakeSceneContainerRepository(),
)
private val mAuthenticationInteractor =
AuthenticationInteractor(
applicationScope = testScope.backgroundScope,
repository = AuthenticationRepositoryImpl(),
)
private val underTest =
LockScreenSceneViewModel(
applicationScope = testScope.backgroundScope,
interactorFactory =
object : LockScreenSceneInteractor.Factory {
override fun create(containerName: String): LockScreenSceneInteractor {
return LockScreenSceneInteractor(
applicationScope = testScope.backgroundScope,
authenticationInteractor = mAuthenticationInteractor,
bouncerInteractorFactory =
object : BouncerInteractor.Factory {
override fun create(containerName: String): BouncerInteractor {
return BouncerInteractor(
applicationScope = testScope.backgroundScope,
applicationContext = context,
repository = BouncerRepository(),
authenticationInteractor = mAuthenticationInteractor,
sceneInteractor = sceneInteractor,
containerName = containerName,
)
}
},
sceneInteractor = sceneInteractor,
containerName = CONTAINER_NAME,
)
}
},
containerName = CONTAINER_NAME
)
@Test
fun lockButtonIcon_whenLocked() =
testScope.runTest {
val lockButtonIcon by collectLastValue(underTest.lockButtonIcon)
mAuthenticationInteractor.setAuthenticationMethod(
AuthenticationMethodModel.Password("password")
)
mAuthenticationInteractor.lockDevice()
assertThat((lockButtonIcon as? Icon.Resource)?.res)
.isEqualTo(R.drawable.ic_device_lock_on)
}
@Test
fun lockButtonIcon_whenUnlocked() =
testScope.runTest {
val lockButtonIcon by collectLastValue(underTest.lockButtonIcon)
mAuthenticationInteractor.setAuthenticationMethod(
AuthenticationMethodModel.Password("password")
)
mAuthenticationInteractor.unlockDevice()
assertThat((lockButtonIcon as? Icon.Resource)?.res)
.isEqualTo(R.drawable.ic_device_lock_off)
}
@Test
fun upTransitionSceneKey_swipeToUnlockedEnabled_gone() =
testScope.runTest {
val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
mAuthenticationInteractor.lockDevice()
assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone)
}
@Test
fun upTransitionSceneKey_swipeToUnlockedNotEnabled_bouncer() =
testScope.runTest {
val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
mAuthenticationInteractor.lockDevice()
assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Bouncer)
}
@Test
fun onLockButtonClicked_deviceLockedSecurely_switchesToBouncer() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
mAuthenticationInteractor.lockDevice()
runCurrent()
underTest.onLockButtonClicked()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
@Test
fun onContentClicked_deviceUnlocked_switchesToGone() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
mAuthenticationInteractor.unlockDevice()
runCurrent()
underTest.onContentClicked()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
}
@Test
fun onContentClicked_deviceLockedSecurely_switchesToBouncer() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
mAuthenticationInteractor.lockDevice()
runCurrent()
underTest.onContentClicked()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
@Test
fun onLockButtonClicked_deviceUnlocked_switchesToGone() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
mAuthenticationInteractor.unlockDevice()
runCurrent()
underTest.onLockButtonClicked()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
}
companion object {
private const val CONTAINER_NAME = "container1"
}
}

View File

@@ -0,0 +1,115 @@
/*
* 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.qs.ui.viewmodel
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.data.repo.BouncerRepository
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor
import com.android.systemui.scene.data.repository.fakeSceneContainerRepository
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.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class QuickSettingsSceneViewModelTest : SysuiTestCase() {
private val testScope = TestScope()
private val sceneInteractor =
SceneInteractor(
repository = fakeSceneContainerRepository(),
)
private val mAuthenticationInteractor =
AuthenticationInteractor(
applicationScope = testScope.backgroundScope,
repository = AuthenticationRepositoryImpl(),
)
private val underTest =
QuickSettingsSceneViewModel(
lockScreenSceneInteractorFactory =
object : LockScreenSceneInteractor.Factory {
override fun create(containerName: String): LockScreenSceneInteractor {
return LockScreenSceneInteractor(
applicationScope = testScope.backgroundScope,
authenticationInteractor = mAuthenticationInteractor,
bouncerInteractorFactory =
object : BouncerInteractor.Factory {
override fun create(containerName: String): BouncerInteractor {
return BouncerInteractor(
applicationScope = testScope.backgroundScope,
applicationContext = context,
repository = BouncerRepository(),
authenticationInteractor = mAuthenticationInteractor,
sceneInteractor = sceneInteractor,
containerName = containerName,
)
}
},
sceneInteractor = sceneInteractor,
containerName = CONTAINER_NAME,
)
}
},
containerName = CONTAINER_NAME
)
@Test
fun onContentClicked_deviceUnlocked_switchesToGone() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
mAuthenticationInteractor.unlockDevice()
runCurrent()
underTest.onContentClicked()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
}
@Test
fun onContentClicked_deviceLockedSecurely_switchesToBouncer() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
mAuthenticationInteractor.lockDevice()
runCurrent()
underTest.onContentClicked()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
companion object {
private const val CONTAINER_NAME = "container1"
}
}

View File

@@ -0,0 +1,136 @@
/*
* 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.shade.ui.viewmodel
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.AuthenticationRepositoryImpl
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.data.repo.BouncerRepository
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.domain.interactor.LockScreenSceneInteractor
import com.android.systemui.scene.data.repository.fakeSceneContainerRepository
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.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class ShadeSceneViewModelTest : SysuiTestCase() {
private val testScope = TestScope()
private val sceneInteractor =
SceneInteractor(
repository = fakeSceneContainerRepository(),
)
private val mAuthenticationInteractor =
AuthenticationInteractor(
applicationScope = testScope.backgroundScope,
repository = AuthenticationRepositoryImpl(),
)
private val underTest =
ShadeSceneViewModel(
applicationScope = testScope.backgroundScope,
lockScreenSceneInteractorFactory =
object : LockScreenSceneInteractor.Factory {
override fun create(containerName: String): LockScreenSceneInteractor {
return LockScreenSceneInteractor(
applicationScope = testScope.backgroundScope,
authenticationInteractor = mAuthenticationInteractor,
bouncerInteractorFactory =
object : BouncerInteractor.Factory {
override fun create(containerName: String): BouncerInteractor {
return BouncerInteractor(
applicationScope = testScope.backgroundScope,
applicationContext = context,
repository = BouncerRepository(),
authenticationInteractor = mAuthenticationInteractor,
sceneInteractor = sceneInteractor,
containerName = containerName,
)
}
},
sceneInteractor = sceneInteractor,
containerName = CONTAINER_NAME,
)
}
},
containerName = CONTAINER_NAME
)
@Test
fun upTransitionSceneKey_deviceLocked_lockScreen() =
testScope.runTest {
val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
mAuthenticationInteractor.lockDevice()
assertThat(upTransitionSceneKey).isEqualTo(SceneKey.LockScreen)
}
@Test
fun upTransitionSceneKey_deviceUnlocked_gone() =
testScope.runTest {
val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey)
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
mAuthenticationInteractor.unlockDevice()
assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone)
}
@Test
fun onContentClicked_deviceUnlocked_switchesToGone() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
mAuthenticationInteractor.unlockDevice()
runCurrent()
underTest.onContentClicked()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
}
@Test
fun onContentClicked_deviceLockedSecurely_switchesToBouncer() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene(CONTAINER_NAME))
mAuthenticationInteractor.setAuthenticationMethod(AuthenticationMethodModel.PIN(1234))
mAuthenticationInteractor.lockDevice()
runCurrent()
underTest.onContentClicked()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
}
companion object {
private const val CONTAINER_NAME = "container1"
}
}