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

Commit 1cf0f476 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

[flexiglass] Adds authmethod flow.

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

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

Change-Id: Ifbbf5be25ed6bdb49f6c8793f5b5093fb14679bf
parent 484b00fd
Loading
Loading
Loading
Loading
+6 −6
Original line number Diff line number Diff line
@@ -63,7 +63,7 @@ constructor(
            .stateIn(
                scope = applicationScope,
                started = SharingStarted.Eagerly,
                initialValue = destinationScenes(up = viewModel.upDestinationSceneKey.value)
                initialValue = destinationScenes(up = null)
            )

    @Composable
@@ -77,12 +77,12 @@ constructor(
    }

    private fun destinationScenes(
        up: SceneKey,
        up: SceneKey?,
    ): Map<UserAction, SceneModel> {
        return mapOf(
            UserAction.Swipe(Direction.UP) to SceneModel(up),
            UserAction.Swipe(Direction.DOWN) to SceneModel(SceneKey.Shade)
        )
        return buildMap {
            up?.let { this[UserAction.Swipe(Direction.UP)] = SceneModel(up) }
            this[UserAction.Swipe(Direction.DOWN)] = SceneModel(SceneKey.Shade)
        }
    }
}

+37 −0
Original line number Diff line number Diff line
/*
 * Copyright 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.authentication.data.model

/** Enumerates all known authentication methods. */
sealed class AuthenticationMethodModel(
    /**
     * Whether the authentication method is considered to be "secure".
     *
     * "Secure" authentication methods require authentication to unlock the device. Non-secure auth
     * methods simply require user dismissal.
     */
    open val isSecure: Boolean,
) {
    /** There is no authentication method on the device. We shouldn't even show the lock screen. */
    object None : AuthenticationMethodModel(isSecure = false)

    object Pin : AuthenticationMethodModel(isSecure = true)

    object Password : AuthenticationMethodModel(isSecure = true)

    object Pattern : AuthenticationMethodModel(isSecure = true)
}
+83 −16
Original line number Diff line number Diff line
@@ -14,15 +14,21 @@
 * limitations under the License.
 */

@file:OptIn(ExperimentalCoroutinesApi::class)

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

import android.app.admin.DevicePolicyManager
import android.content.IntentFilter
import android.os.UserHandle
import com.android.internal.widget.LockPatternChecker
import com.android.internal.widget.LockPatternUtils
import com.android.internal.widget.LockscreenCredential
import com.android.keyguard.KeyguardSecurityModel
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.authentication.data.model.AuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationResultModel
import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyguard.data.repository.KeyguardRepository
@@ -37,13 +43,17 @@ import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

@@ -54,9 +64,10 @@ interface AuthenticationRepository {
     * Whether the device is unlocked.
     *
     * A device that is not yet unlocked requires unlocking by completing an authentication
     * challenge according to the current authentication method.
     *
     * Note that this state has no real bearing on whether the lockscreen is showing or dismissed.
     * challenge according to the current authentication method, unless in cases when the current
     * authentication method is not "secure" (for example, None); in such cases, the value of this
     * flow will always be `true`, even if the lockscreen is showing and still needs to be dismissed
     * by the user to proceed.
     */
    val isUnlocked: StateFlow<Boolean>

@@ -85,9 +96,30 @@ interface AuthenticationRepository {
    /** The current throttling state, as cached via [setThrottling]. */
    val throttling: StateFlow<AuthenticationThrottlingModel>

    /**
     * The currently-configured authentication method. This determines how the authentication
     * challenge needs to be completed in order to unlock an otherwise locked device.
     *
     * Note: there may be other ways to unlock the device that "bypass" the need for this
     * authentication challenge (notably, biometrics like fingerprint or face unlock).
     *
     * Note: by design, this is a [Flow] and not a [StateFlow]; a consumer who wishes to get a
     * snapshot of the current authentication method without establishing a collector of the flow
     * can do so by invoking [getAuthenticationMethod].
     */
    val authenticationMethod: Flow<AuthenticationMethodModel>

    /**
     * Returns the currently-configured authentication method. This determines how the
     * authentication challenge is completed in order to unlock an otherwise locked device.
     * authentication challenge needs to be completed in order to unlock an otherwise locked device.
     *
     * Note: there may be other ways to unlock the device that "bypass" the need for this
     * authentication challenge (notably, biometrics like fingerprint or face unlock).
     *
     * Note: by design, this is offered as a convenience method alongside [authenticationMethod].
     * The flow should be used for code that wishes to stay up-to-date its logic as the
     * authentication changes over time and this method should be used for simple code that only
     * needs to check the current value.
     */
    suspend fun getAuthenticationMethod(): AuthenticationMethodModel

@@ -141,6 +173,7 @@ constructor(
    private val userRepository: UserRepository,
    keyguardRepository: KeyguardRepository,
    private val lockPatternUtils: LockPatternUtils,
    broadcastDispatcher: BroadcastDispatcher,
) : AuthenticationRepository {

    override val isUnlocked = keyguardRepository.isKeyguardUnlocked
@@ -148,7 +181,7 @@ constructor(
    override suspend fun isLockscreenEnabled(): Boolean {
        return withContext(backgroundDispatcher) {
            val selectedUserId = userRepository.selectedUserId
            !lockPatternUtils.isLockPatternEnabled(selectedUserId)
            !lockPatternUtils.isLockScreenDisabled(selectedUserId)
        }
    }

@@ -172,18 +205,31 @@ constructor(
    private val UserRepository.selectedUserId: Int
        get() = getSelectedUserInfo().id

    override val authenticationMethod: Flow<AuthenticationMethodModel> =
        userRepository.selectedUserInfo
            .map { it.id }
            .distinctUntilChanged()
            .flatMapLatest { selectedUserId ->
                broadcastDispatcher
                    .broadcastFlow(
                        filter =
                            IntentFilter(
                                DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED
                            ),
                        user = UserHandle.of(selectedUserId),
                    )
                    .onStart { emit(Unit) }
                    .map { selectedUserId }
            }
            .map { selectedUserId ->
                withContext(backgroundDispatcher) {
                    blockingAuthenticationMethodInternal(selectedUserId)
                }
            }

    override suspend fun getAuthenticationMethod(): AuthenticationMethodModel {
        return withContext(backgroundDispatcher) {
            val selectedUserId = userRepository.selectedUserId
            when (getSecurityMode.apply(selectedUserId)) {
                KeyguardSecurityModel.SecurityMode.PIN,
                KeyguardSecurityModel.SecurityMode.SimPin,
                KeyguardSecurityModel.SecurityMode.SimPuk -> AuthenticationMethodModel.Pin
                KeyguardSecurityModel.SecurityMode.Password -> AuthenticationMethodModel.Password
                KeyguardSecurityModel.SecurityMode.Pattern -> AuthenticationMethodModel.Pattern
                KeyguardSecurityModel.SecurityMode.None -> AuthenticationMethodModel.None
                KeyguardSecurityModel.SecurityMode.Invalid -> error("Invalid security mode!")
            }
            blockingAuthenticationMethodInternal(userRepository.selectedUserId)
        }
    }

@@ -301,6 +347,27 @@ constructor(

        return flow.asStateFlow()
    }

    /**
     * Returns the authentication method for the given user ID.
     *
     * WARNING: this is actually a blocking IPC/"binder" call that's expensive to do on the main
     * thread. We keep it not marked as `suspend` because we want to be able to run this without a
     * `runBlocking` which has a ton of performance/blocking problems.
     */
    private fun blockingAuthenticationMethodInternal(
        userId: Int,
    ): AuthenticationMethodModel {
        return when (getSecurityMode.apply(userId)) {
            KeyguardSecurityModel.SecurityMode.PIN,
            KeyguardSecurityModel.SecurityMode.SimPin,
            KeyguardSecurityModel.SecurityMode.SimPuk -> AuthenticationMethodModel.Pin
            KeyguardSecurityModel.SecurityMode.Password -> AuthenticationMethodModel.Password
            KeyguardSecurityModel.SecurityMode.Pattern -> AuthenticationMethodModel.Pattern
            KeyguardSecurityModel.SecurityMode.None -> AuthenticationMethodModel.None
            KeyguardSecurityModel.SecurityMode.Invalid -> error("Invalid security mode!")
        }
    }
}

@Module
+73 −27
Original line number Diff line number Diff line
@@ -18,8 +18,10 @@ package com.android.systemui.authentication.domain.interactor

import com.android.internal.widget.LockPatternView
import com.android.internal.widget.LockscreenCredential
import com.android.systemui.authentication.data.model.AuthenticationMethodModel as DataLayerAuthenticationMethodModel
import com.android.systemui.authentication.data.repository.AuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainLayerAuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -35,8 +37,10 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
@@ -55,22 +59,41 @@ constructor(
    private val keyguardRepository: KeyguardRepository,
    private val clock: SystemClock,
) {
    /**
     * The currently-configured authentication method. This determines how the authentication
     * challenge needs to be completed in order to unlock an otherwise locked device.
     *
     * Note: there may be other ways to unlock the device that "bypass" the need for this
     * authentication challenge (notably, biometrics like fingerprint or face unlock).
     *
     * Note: by design, this is a [Flow] and not a [StateFlow]; a consumer who wishes to get a
     * snapshot of the current authentication method without establishing a collector of the flow
     * can do so by invoking [getAuthenticationMethod].
     *
     * Note: this layer adds the synthetic authentication method of "swipe" which is special. When
     * the current authentication method is "swipe", the user does not need to complete any
     * authentication challenge to unlock the device; they just need to dismiss the lockscreen to
     * get past it. This also means that the value of [isUnlocked] remains `false` even when the
     * lockscreen is showing and still needs to be dismissed by the user to proceed.
     */
    val authenticationMethod: Flow<DomainLayerAuthenticationMethodModel> =
        repository.authenticationMethod.map { rawModel -> rawModel.toDomainLayer() }

    /**
     * Whether the device is unlocked.
     *
     * A device that is not yet unlocked requires unlocking by completing an authentication
     * challenge according to the current authentication method.
     *
     * Note that this state has no real bearing on whether the lock screen is showing or dismissed.
     * challenge according to the current authentication method, unless in cases when the current
     * authentication method is not "secure" (for example, None and Swipe); in such cases, the value
     * of this flow will always be `true`, even if the lockscreen is showing and still needs to be
     * dismissed by the user to proceed.
     */
    val isUnlocked: StateFlow<Boolean> =
        repository.isUnlocked
            .map { isUnlocked ->
                if (getAuthenticationMethod() is AuthenticationMethodModel.None) {
                    true
                } else {
                    isUnlocked
                }
        combine(
                repository.isUnlocked,
                authenticationMethod,
            ) { isUnlocked, authenticationMethod ->
                authenticationMethod is DomainLayerAuthenticationMethodModel.None || isUnlocked
            }
            .stateIn(
                scope = applicationScope,
@@ -129,18 +152,24 @@ constructor(

    /**
     * Returns the currently-configured authentication method. This determines how the
     * authentication challenge is completed in order to unlock an otherwise locked device.
     * authentication challenge needs to be completed in order to unlock an otherwise locked device.
     *
     * Note: there may be other ways to unlock the device that "bypass" the need for this
     * authentication challenge (notably, biometrics like fingerprint or face unlock).
     *
     * Note: by design, this is offered as a convenience method alongside [authenticationMethod].
     * The flow should be used for code that wishes to stay up-to-date its logic as the
     * authentication changes over time and this method should be used for simple code that only
     * needs to check the current value.
     *
     * Note: this layer adds the synthetic authentication method of "swipe" which is special. When
     * the current authentication method is "swipe", the user does not need to complete any
     * authentication challenge to unlock the device; they just need to dismiss the lockscreen to
     * get past it. This also means that the value of [isUnlocked] remains `false` even when the
     * lockscreen is showing and still needs to be dismissed by the user to proceed.
     */
    suspend fun getAuthenticationMethod(): AuthenticationMethodModel {
        val authMethod = repository.getAuthenticationMethod()
        return if (
            authMethod is AuthenticationMethodModel.None && repository.isLockscreenEnabled()
        ) {
            // We treat "None" as "Swipe" when the lockscreen is enabled.
            AuthenticationMethodModel.Swipe
        } else {
            authMethod
        }
    suspend fun getAuthenticationMethod(): DomainLayerAuthenticationMethodModel {
        return repository.getAuthenticationMethod().toDomainLayer()
    }

    /**
@@ -270,21 +299,38 @@ constructor(
        }
    }

    private fun AuthenticationMethodModel.createCredential(
    private fun DomainLayerAuthenticationMethodModel.createCredential(
        input: List<Any>
    ): LockscreenCredential? {
        return when (this) {
            is AuthenticationMethodModel.Pin ->
            is DomainLayerAuthenticationMethodModel.Pin ->
                LockscreenCredential.createPin(input.joinToString(""))
            is AuthenticationMethodModel.Password ->
            is DomainLayerAuthenticationMethodModel.Password ->
                LockscreenCredential.createPassword(input.joinToString(""))
            is AuthenticationMethodModel.Pattern ->
            is DomainLayerAuthenticationMethodModel.Pattern ->
                LockscreenCredential.createPattern(
                    input
                        .map { it as AuthenticationMethodModel.Pattern.PatternCoordinate }
                        .map { it as AuthenticationPatternCoordinate }
                        .map { LockPatternView.Cell.of(it.y, it.x) }
                )
            else -> null
        }
    }

    private suspend fun DataLayerAuthenticationMethodModel.toDomainLayer():
        DomainLayerAuthenticationMethodModel {
        return when (this) {
            is DataLayerAuthenticationMethodModel.None ->
                if (repository.isLockscreenEnabled()) {
                    DomainLayerAuthenticationMethodModel.Swipe
                } else {
                    DomainLayerAuthenticationMethodModel.None
                }
            is DataLayerAuthenticationMethodModel.Pin -> DomainLayerAuthenticationMethodModel.Pin
            is DataLayerAuthenticationMethodModel.Password ->
                DomainLayerAuthenticationMethodModel.Password
            is DataLayerAuthenticationMethodModel.Pattern ->
                DomainLayerAuthenticationMethodModel.Pattern
        }
    }
}
+2 −7
Original line number Diff line number Diff line
@@ -14,7 +14,7 @@
 * limitations under the License.
 */

package com.android.systemui.authentication.shared.model
package com.android.systemui.authentication.domain.model

/** Enumerates all known authentication methods. */
sealed class AuthenticationMethodModel(
@@ -36,10 +36,5 @@ sealed class AuthenticationMethodModel(

    object Password : AuthenticationMethodModel(isSecure = true)

    object Pattern : AuthenticationMethodModel(isSecure = true) {
        data class PatternCoordinate(
            val x: Int,
            val y: Int,
        )
    }
    object Pattern : AuthenticationMethodModel(isSecure = true)
}
Loading