diff options
18 files changed, 2293 insertions, 0 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/AuthenticationModule.kt b/packages/SystemUI/src/com/android/systemui/authentication/AuthenticationModule.kt new file mode 100644 index 000000000000..7c394a62a5f4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/authentication/AuthenticationModule.kt @@ -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 diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt new file mode 100644 index 000000000000..cd195f6f9f97 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt @@ -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 +} diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt new file mode 100644 index 000000000000..5aea930401d9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt @@ -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) } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt new file mode 100644 index 000000000000..83250b638424 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt @@ -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, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt new file mode 100644 index 000000000000..4c817b2e46a8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repo/BouncerRepository.kt @@ -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 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt new file mode 100644 index 000000000000..57ce58049f13 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt @@ -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 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt new file mode 100644 index 000000000000..8a183ae6ca7c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt @@ -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. + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 97359d8fd249..70c859ebe02b 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -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, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractor.kt new file mode 100644 index 000000000000..61701802e231 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractor.kt @@ -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 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModel.kt new file mode 100644 index 000000000000..08b961306817 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModel.kt @@ -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 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt new file mode 100644 index 000000000000..6525a98b9102 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt @@ -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 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt new file mode 100644 index 000000000000..dcae258ed76f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt @@ -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 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt new file mode 100644 index 000000000000..2e62bebfe3f1 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt @@ -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() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt new file mode 100644 index 000000000000..7dd376ede361 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt @@ -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" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractorTest.kt new file mode 100644 index 000000000000..749e7a0481eb --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockScreenSceneInteractorTest.kt @@ -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" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModelTest.kt new file mode 100644 index 000000000000..d335b09b196a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockScreenSceneViewModelTest.kt @@ -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" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt new file mode 100644 index 000000000000..e8875bee5276 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt @@ -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" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt new file mode 100644 index 000000000000..688cce83a55d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt @@ -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" + } +} |