Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ace6b806 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

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

- Source auth method from KeyguardSecurityModel, changing it from flow to getter

Bug: 280883900
Test: Unit tests still pass.
Test: manually verified that changing the screen lock setting in
Settings, between PIN, password, and pattern, properly affects
Flexiglass in System UI such that the right version of the bouncer UI is
displayed.

Change-Id: Id457458806dc6ac2720abe21a77d5e50b83836fc
parent 990ed238
Loading
Loading
Loading
Loading
+12 −1
Original line number Diff line number Diff line
@@ -16,8 +16,11 @@

package com.android.systemui.authentication

import com.android.keyguard.KeyguardSecurityModel
import com.android.systemui.authentication.data.repository.AuthenticationRepositoryModule
import dagger.Module
import dagger.Provides
import java.util.function.Function

@Module(
    includes =
@@ -25,4 +28,12 @@ import dagger.Module
            AuthenticationRepositoryModule::class,
        ],
)
object AuthenticationModule
object AuthenticationModule {

    @Provides
    fun getSecurityMode(
        model: KeyguardSecurityModel,
    ): Function<Int, KeyguardSecurityModel.SecurityMode> {
        return Function { userId -> model.getSecurityMode(userId) }
    }
}
+57 −23
Original line number Diff line number Diff line
@@ -16,13 +16,20 @@

package com.android.systemui.authentication.data.repository

import com.android.internal.widget.LockPatternUtils
import com.android.keyguard.KeyguardSecurityModel
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.user.data.repository.UserRepository
import dagger.Binds
import dagger.Module
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.withContext
import java.util.function.Function
import javax.inject.Inject

/** Defines interface for classes that can access authentication-related application state. */
interface AuthenticationRepository {
@@ -37,12 +44,6 @@ interface AuthenticationRepository {
     */
    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
@@ -57,12 +58,15 @@ interface AuthenticationRepository {
     */
    val failedAuthenticationAttempts: StateFlow<Int>

    /**
     * Returns the currently-configured authentication method. This determines how the
     * authentication challenge is completed in order to unlock an otherwise locked device.
     */
    suspend fun getAuthenticationMethod(): AuthenticationMethodModel

    /** See [isUnlocked]. */
    fun setUnlocked(isUnlocked: Boolean)

    /** See [authenticationMethod]. */
    fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel)

    /** See [isBypassEnabled]. */
    fun setBypassEnabled(isBypassEnabled: Boolean)

@@ -70,19 +74,18 @@ interface AuthenticationRepository {
    fun setFailedAuthenticationAttempts(failedAuthenticationAttempts: Int)
}

class AuthenticationRepositoryImpl @Inject constructor() : AuthenticationRepository {
    // TODO(b/280883900): get data from real data sources in SysUI.
class AuthenticationRepositoryImpl
@Inject
constructor(
    private val getSecurityMode: Function<Int, KeyguardSecurityModel.SecurityMode>,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    private val userRepository: UserRepository,
    private val lockPatternUtils: LockPatternUtils,
) : AuthenticationRepository {

    private val _isUnlocked = MutableStateFlow(false)
    override val isUnlocked: StateFlow<Boolean> = _isUnlocked.asStateFlow()

    private val _authenticationMethod =
        MutableStateFlow<AuthenticationMethodModel>(
            AuthenticationMethodModel.Pin(listOf(1, 2, 3, 4), autoConfirm = false)
        )
    override val authenticationMethod: StateFlow<AuthenticationMethodModel> =
        _authenticationMethod.asStateFlow()

    private val _isBypassEnabled = MutableStateFlow(false)
    override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled.asStateFlow()

@@ -90,6 +93,41 @@ class AuthenticationRepositoryImpl @Inject constructor() : AuthenticationReposit
    override val failedAuthenticationAttempts: StateFlow<Int> =
        _failedAuthenticationAttempts.asStateFlow()

    override suspend fun getAuthenticationMethod(): AuthenticationMethodModel {
        return withContext(backgroundDispatcher) {
            val selectedUserId = userRepository.getSelectedUserInfo().id
            when (getSecurityMode.apply(selectedUserId)) {
                KeyguardSecurityModel.SecurityMode.PIN,
                KeyguardSecurityModel.SecurityMode.SimPin ->
                    AuthenticationMethodModel.Pin(
                        code = listOf(1, 2, 3, 4), // TODO(b/280883900): remove this
                        autoConfirm = lockPatternUtils.isAutoPinConfirmEnabled(selectedUserId),
                    )
                KeyguardSecurityModel.SecurityMode.Password,
                KeyguardSecurityModel.SecurityMode.SimPuk ->
                    AuthenticationMethodModel.Password(
                        password = "password", // TODO(b/280883900): remove this
                    )
                KeyguardSecurityModel.SecurityMode.Pattern ->
                    AuthenticationMethodModel.Pattern(
                        coordinates =
                            listOf(
                                AuthenticationMethodModel.Pattern.PatternCoordinate(2, 0),
                                AuthenticationMethodModel.Pattern.PatternCoordinate(2, 1),
                                AuthenticationMethodModel.Pattern.PatternCoordinate(2, 2),
                                AuthenticationMethodModel.Pattern.PatternCoordinate(1, 1),
                                AuthenticationMethodModel.Pattern.PatternCoordinate(0, 0),
                                AuthenticationMethodModel.Pattern.PatternCoordinate(0, 1),
                                AuthenticationMethodModel.Pattern.PatternCoordinate(0, 2),
                            ), // TODO(b/280883900): remove this
                    )
                KeyguardSecurityModel.SecurityMode.None -> AuthenticationMethodModel.None
                KeyguardSecurityModel.SecurityMode.Invalid -> error("Invalid security mode!")
                null -> error("Invalid security is null!")
            }
        }
    }

    override fun setUnlocked(isUnlocked: Boolean) {
        _isUnlocked.value = isUnlocked
    }
@@ -98,10 +136,6 @@ class AuthenticationRepositoryImpl @Inject constructor() : AuthenticationReposit
        _isBypassEnabled.value = isBypassEnabled
    }

    override fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) {
        _authenticationMethod.value = authenticationMethod
    }

    override fun setFailedAuthenticationAttempts(failedAuthenticationAttempts: Int) {
        _failedAuthenticationAttempts.value = failedAuthenticationAttempts
    }
+19 −49
Original line number Diff line number Diff line
@@ -25,9 +25,8 @@ 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.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

/** Hosts application business logic related to authentication. */
@SysUISingleton
@@ -37,12 +36,6 @@ 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.
     *
@@ -52,20 +45,18 @@ constructor(
     * 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,
                )
        repository.isUnlocked
            .map { isUnlocked ->
                if (getAuthenticationMethod() is AuthenticationMethodModel.None) {
                    true
                } else {
                    isUnlocked
                }
            }
            .stateIn(
                scope = applicationScope,
                started = SharingStarted.Eagerly,
                initialValue =
                    isUnlockedWithAuthMethod(
                        isUnlocked = repository.isUnlocked.value,
                        authMethod = repository.authenticationMethod.value,
                    )
                initialValue = true,
            )

    /**
@@ -82,25 +73,20 @@ constructor(
     */
    val failedAuthenticationAttempts: StateFlow<Int> = repository.failedAuthenticationAttempts

    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 the currently-configured authentication method. This determines how the
     * authentication challenge is completed in order to unlock an otherwise locked device.
     */
    suspend fun getAuthenticationMethod(): AuthenticationMethodModel {
        return repository.getAuthenticationMethod()
    }

    /**
     * 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
    suspend fun isAuthenticationRequired(): Boolean {
        return !isUnlocked.value && getAuthenticationMethod().isSecure
    }

    /**
@@ -133,8 +119,8 @@ constructor(
     * @return `true` if the authentication succeeded and the device is now unlocked; `false` when
     *   authentication failed, `null` if the check was not performed.
     */
    fun authenticate(input: List<Any>, tryAutoConfirm: Boolean = false): Boolean? {
        val authMethod = this.authenticationMethod.value
    suspend fun authenticate(input: List<Any>, tryAutoConfirm: Boolean = false): Boolean? {
        val authMethod = getAuthenticationMethod()
        if (tryAutoConfirm) {
            if ((authMethod as? AuthenticationMethodModel.Pin)?.autoConfirm != true) {
                // Do not attempt to authenticate unless the PIN lock is set to auto-confirm.
@@ -176,28 +162,12 @@ constructor(
        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] in the range [0-9].
+40 −42
Original line number Diff line number Diff line
@@ -68,26 +68,14 @@ constructor(
                    )
            )

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

    /** The current authentication throttling state. If `null`, there's no throttling. */
    val throttling: StateFlow<AuthenticationThrottledModel?> = repository.throttling

    init {
        applicationScope.launch {
            combine(
                    sceneInteractor.currentScene(containerName),
                    authenticationInteractor.authenticationMethod,
                    ::Pair,
                )
                .collect { (currentScene, authMethod) ->
            sceneInteractor.currentScene(containerName).collect { currentScene ->
                if (currentScene.key == SceneKey.Bouncer) {
                        when (authMethod) {
                    when (getAuthenticationMethod()) {
                        is AuthenticationMethodModel.None ->
                            sceneInteractor.setCurrentScene(
                                containerName,
@@ -105,6 +93,14 @@ constructor(
        }
    }

    /**
     * Returns the currently-configured authentication method. This determines how the
     * authentication challenge is completed in order to unlock an otherwise locked device.
     */
    suspend fun getAuthenticationMethod(): AuthenticationMethodModel {
        return authenticationInteractor.getAuthenticationMethod()
    }

    /**
     * Either shows the bouncer or unlocks the device, if the bouncer doesn't need to be shown.
     *
@@ -115,8 +111,9 @@ constructor(
        containerName: String,
        message: String? = null,
    ) {
        applicationScope.launch {
            if (authenticationInteractor.isAuthenticationRequired()) {
            repository.setMessage(message ?: promptMessage(authenticationMethod.value))
                repository.setMessage(message ?: promptMessage(getAuthenticationMethod()))
                sceneInteractor.setCurrentScene(
                    containerName = containerName,
                    scene = SceneModel(SceneKey.Bouncer),
@@ -129,13 +126,14 @@ constructor(
                )
            }
        }
    }

    /**
     * Resets the user-facing message back to the default according to the current authentication
     * method.
     */
    fun resetMessage() {
        repository.setMessage(promptMessage(authenticationMethod.value))
        applicationScope.launch { repository.setMessage(promptMessage(getAuthenticationMethod())) }
    }

    /** Removes the user-facing message. */
@@ -160,7 +158,7 @@ constructor(
     * @return `true` if the authentication succeeded and the device is now unlocked; `false` when
     *   authentication failed, `null` if the check was not performed.
     */
    fun authenticate(
    suspend fun authenticate(
        input: List<Any>,
        tryAutoConfirm: Boolean = false,
    ): Boolean? {
@@ -198,7 +196,7 @@ constructor(
                    repository.setThrottling(null)
                    clearMessage()
                }
            else -> repository.setMessage(errorMessage(authenticationMethod.value))
            else -> repository.setMessage(errorMessage(getAuthenticationMethod()))
        }

        return isAuthenticated
+22 −20
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@@ -66,6 +67,7 @@ constructor(

    private val password: PasswordBouncerViewModel by lazy {
        PasswordBouncerViewModel(
            applicationScope = applicationScope,
            interactor = interactor,
            isInputEnabled = isInputEnabled,
        )
@@ -81,13 +83,24 @@ constructor(
    }

    /** View-model for the current UI, based on the current authentication method. */
    val authMethod: StateFlow<AuthMethodBouncerViewModel?> =
        interactor.authenticationMethod
            .map { authMethod -> toViewModel(authMethod) }
    val authMethod: StateFlow<AuthMethodBouncerViewModel?>
        get() =
            flow {
                    emit(null)
                    emit(interactor.getAuthenticationMethod())
                }
                .map { authMethod ->
                    when (authMethod) {
                        is AuthenticationMethodModel.Pin -> pin
                        is AuthenticationMethodModel.Password -> password
                        is AuthenticationMethodModel.Pattern -> pattern
                        else -> null
                    }
                }
                .stateIn(
                    scope = applicationScope,
                    started = SharingStarted.WhileSubscribed(),
                initialValue = toViewModel(interactor.authenticationMethod.value),
                    initialValue = null,
                )

    /** The user-facing message to show in the bouncer. */
@@ -125,7 +138,7 @@ constructor(
            interactor.throttling
                .map { model ->
                    model?.let {
                        when (interactor.authenticationMethod.value) {
                        when (interactor.getAuthenticationMethod()) {
                            is AuthenticationMethodModel.Pin ->
                                R.string.kg_too_many_failed_pin_attempts_dialog_message
                            is AuthenticationMethodModel.Password ->
@@ -161,17 +174,6 @@ constructor(
        _throttlingDialogMessage.value = null
    }

    private fun toViewModel(
        authMethod: AuthenticationMethodModel,
    ): AuthMethodBouncerViewModel? {
        return when (authMethod) {
            is AuthenticationMethodModel.Pin -> pin
            is AuthenticationMethodModel.Password -> password
            is AuthenticationMethodModel.Pattern -> pattern
            else -> null
        }
    }

    private fun toMessageViewModel(
        message: String?,
        throttling: AuthenticationThrottledModel?,
Loading