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

Commit af001916 authored by burakov's avatar burakov
Browse files

[flexiglass] Add a DeviceEntry module.

This module hosts business logic and state related to device entry, i.e.
 when the user successfully dismisses (or bypasses) the lockscreen. This
 is distinct from the authentication module, which is specifically
 concerned with determining a user's identity.

The names of certain state variables have changed, too:

* `isLockscreenDismissed` is now `isDeviceEntered`
* `canSwipeToDismiss` is now `canSwipeToEnter`
* `isLockscreenEnabled` is now `isInsecureLockscreenEnabled`

Bug: 301253588
Test: Existing unit tests still pass.
Test: Added new unit tests.
Test: Manually tested the 3 different bouncer auth methods, lockscreen
 bypass and swipe, and verified they still work as expected.
Change-Id: I9596513b4319eb0a9b9a25bee3a1c1726396e4ca
parent 48d97fae
Loading
Loading
Loading
Loading
+14 −17
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.keyguard;

import static android.app.StatusBarManager.SESSION_KEYGUARD;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;

import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_BIOMETRIC;
import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_EXTENDED_ACCESS;
import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_NONE_SECURITY;
@@ -68,8 +69,6 @@ import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
import com.android.keyguard.dagger.KeyguardBouncerScope;
import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.Gefingerpoken;
import com.android.systemui.res.R;
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor;
import com.android.systemui.biometrics.FaceAuthAccessibilityDelegate;
import com.android.systemui.biometrics.SideFpsController;
import com.android.systemui.biometrics.SideFpsUiRequestSource;
@@ -77,6 +76,7 @@ import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor;
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
import com.android.systemui.classifier.FalsingA11yDelegate;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor;
@@ -84,6 +84,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInterac
import com.android.systemui.log.SessionTracker;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.res.R;
import com.android.systemui.scene.shared.flag.SceneContainerFlags;
import com.android.systemui.shared.system.SysUiStatsLog;
import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -420,7 +421,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard
                }
            };
    private final UserInteractor mUserInteractor;
    private final Provider<AuthenticationInteractor> mAuthenticationInteractor;
    private final Provider<DeviceEntryInteractor> mDeviceEntryInteractor;
    private final Provider<JavaAdapter> mJavaAdapter;
    private final DeviceProvisionedController mDeviceProvisionedController;
    private final Lazy<PrimaryBouncerInteractor> mPrimaryBouncerInteractor;
@@ -457,7 +458,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard
            FaceAuthAccessibilityDelegate faceAuthAccessibilityDelegate,
            KeyguardTransitionInteractor keyguardTransitionInteractor,
            Lazy<PrimaryBouncerInteractor> primaryBouncerInteractor,
            Provider<AuthenticationInteractor> authenticationInteractor
            Provider<DeviceEntryInteractor> deviceEntryInteractor
    ) {
        super(view);
        view.setAccessibilityDelegate(faceAuthAccessibilityDelegate);
@@ -487,7 +488,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard
        mKeyguardFaceAuthInteractor = keyguardFaceAuthInteractor;
        mBouncerMessageInteractor = bouncerMessageInteractor;
        mUserInteractor = userInteractor;
        mAuthenticationInteractor = authenticationInteractor;
        mDeviceEntryInteractor = deviceEntryInteractor;
        mJavaAdapter = javaAdapter;
        mKeyguardTransitionInteractor = keyguardTransitionInteractor;
        mDeviceProvisionedController = deviceProvisionedController;
@@ -519,9 +520,9 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard
            // When the scene framework says that the lockscreen has been dismissed, dismiss the
            // keyguard here, revealing the underlying app or launcher:
            mSceneTransitionCollectionJob = mJavaAdapter.get().alwaysCollectFlow(
                mAuthenticationInteractor.get().isLockscreenDismissed(),
                isLockscreenDismissed -> {
                    if (isLockscreenDismissed) {
                mDeviceEntryInteractor.get().isDeviceEntered(),
                    isDeviceEntered -> {
                    if (isDeviceEntered) {
                        final int selectedUserId = mUserInteractor.getSelectedUserId();
                        showNextSecurityScreenOrFinish(
                            /* authenticated= */ true,
@@ -1081,15 +1082,11 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard
     * one side).
     */
    private boolean canUseOneHandedBouncer() {
        switch(mCurrentSecurityMode) {
            case PIN:
            case Pattern:
            case SimPin:
            case SimPuk:
                return getResources().getBoolean(R.bool.can_use_one_handed_bouncer);
            default:
                return false;
        }
        return switch (mCurrentSecurityMode) {
            case PIN, Pattern, SimPin, SimPuk -> getResources().getBoolean(
                    R.bool.can_use_one_handed_bouncer);
            default -> false;
        };
    }

    private boolean canDisplayUserSwitcher() {
+2 −31
Original line number Diff line number Diff line
@@ -29,9 +29,9 @@ 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.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.user.data.repository.UserRepository
import com.android.systemui.util.kotlin.pairwise
import com.android.systemui.util.time.SystemClock
@@ -59,18 +59,6 @@ import kotlinx.coroutines.withContext

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

    /**
     * Whether the auto confirm feature is enabled for the currently-selected user.
     *
@@ -129,14 +117,6 @@ interface AuthenticationRepository {
    /** Returns the length of the PIN or `0` if the current auth method is not PIN. */
    suspend fun getPinLength(): Int

    /**
     * Returns whether the lockscreen is enabled.
     *
     * When the lockscreen is not enabled, it shouldn't show in cases when the authentication method
     * is considered not secure (for example, "swipe" is considered to be "none").
     */
    suspend fun isLockscreenEnabled(): Boolean

    /** Reports an authentication attempt. */
    suspend fun reportAuthenticationAttempt(isSuccessful: Boolean)

@@ -167,6 +147,7 @@ interface AuthenticationRepository {
    suspend fun checkCredential(credential: LockscreenCredential): AuthenticationResultModel
}

@SysUISingleton
class AuthenticationRepositoryImpl
@Inject
constructor(
@@ -174,20 +155,10 @@ constructor(
    private val getSecurityMode: Function<Int, KeyguardSecurityModel.SecurityMode>,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    private val userRepository: UserRepository,
    keyguardRepository: KeyguardRepository,
    private val lockPatternUtils: LockPatternUtils,
    broadcastDispatcher: BroadcastDispatcher,
) : AuthenticationRepository {

    override val isUnlocked = keyguardRepository.isKeyguardUnlocked

    override suspend fun isLockscreenEnabled(): Boolean {
        return withContext(backgroundDispatcher) {
            val selectedUserId = userRepository.selectedUserId
            !lockPatternUtils.isLockScreenDisabled(selectedUserId)
        }
    }

    override val isAutoConfirmEnabled: StateFlow<Boolean> =
        refreshingFlow(
            initialValue = false,
+22 −104
Original line number Diff line number Diff line
@@ -26,9 +26,7 @@ import com.android.systemui.authentication.shared.model.AuthenticationThrottling
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository
import com.android.systemui.user.data.repository.UserRepository
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
@@ -42,15 +40,19 @@ 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.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

/** Hosts application business logic related to authentication. */
/**
 * Hosts application business logic related to user authentication.
 *
 * Note: there is a distinction between authentication (determining a user's identity) and device
 * entry (dismissing the lockscreen). For logic that is specific to device entry, please use
 * `DeviceEntryInteractor` instead.
 */
@SysUISingleton
class AuthenticationInteractor
@Inject
@@ -59,8 +61,7 @@ constructor(
    private val repository: AuthenticationRepository,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    private val userRepository: UserRepository,
    private val keyguardRepository: KeyguardRepository,
    sceneInteractor: SceneInteractor,
    private val deviceEntryRepository: DeviceEntryRepository,
    private val clock: SystemClock,
) {
    /**
@@ -77,76 +78,13 @@ constructor(
     * 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.
     * get past it. This also means that the value of `DeviceEntryInteractor#isUnlocked` remains
     * `true` 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, 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> =
        combine(
                repository.isUnlocked,
                authenticationMethod,
            ) { isUnlocked, authenticationMethod ->
                !authenticationMethod.isSecure || isUnlocked
            }
            .stateIn(
                scope = applicationScope,
                started = SharingStarted.Eagerly,
                initialValue = false,
            )

    /**
     * Whether the lockscreen has been dismissed (by any method). This can be false even when the
     * device is unlocked, e.g. when swipe to unlock is enabled.
     *
     * Note:
     * - `false` doesn't mean the lockscreen is visible (it may be occluded or covered by other UI).
     * - `true` doesn't mean the lockscreen is invisible (since this state changes before the
     *   transition occurs).
     */
    val isLockscreenDismissed: StateFlow<Boolean> =
        sceneInteractor.desiredScene
            .map { it.key }
            .filter { currentScene ->
                currentScene == SceneKey.Gone || currentScene == SceneKey.Lockscreen
            }
            .map { it == SceneKey.Gone }
            .stateIn(
                scope = applicationScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = false,
            )

    /**
     * Whether it's currently possible to swipe up to dismiss the lockscreen without requiring
     * authentication. This returns false whenever the lockscreen has been dismissed.
     *
     * Note: `true` doesn't mean the lockscreen is visible. It may be occluded or covered by other
     * UI.
     */
    val canSwipeToDismiss =
        combine(authenticationMethod, isLockscreenDismissed) {
                authenticationMethod,
                isLockscreenDismissed ->
                authenticationMethod is DomainLayerAuthenticationMethodModel.Swipe &&
                    !isLockscreenDismissed
            }
            .stateIn(
                scope = applicationScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = false,
            )

    /** The current authentication throttling state, only meaningful if [isThrottled] is `true`. */
    val throttling: StateFlow<AuthenticationThrottlingModel> = repository.throttling

@@ -211,31 +149,14 @@ constructor(
     * 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.
     * get past it. This also means that the value of `DeviceEntryInteractor#isUnlocked` remains
     * `true` even when the lockscreen is showing and still needs to be dismissed by the user to
     * proceed.
     */
    suspend fun getAuthenticationMethod(): DomainLayerAuthenticationMethodModel {
        return repository.getAuthenticationMethod().toDomainLayer()
    }

    /**
     * Returns `true` if the device currently requires authentication before content can be viewed;
     * `false` if content can be displayed without unlocking first.
     */
    suspend fun isAuthenticationRequired(): Boolean {
        return !isUnlocked.value && getAuthenticationMethod().isSecure
    }

    /**
     * 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.
     */
    fun isBypassEnabled(): Boolean {
        return keyguardRepository.isBypassEnabled()
    }

    /**
     * Attempts to authenticate the user and unlock the device.
     *
@@ -312,7 +233,7 @@ constructor(

    /** Starts refreshing the throttling state every second. */
    private suspend fun startThrottlingCountdown() {
        cancelCountdown()
        cancelThrottlingCountdown()
        throttlingCountdownJob =
            applicationScope.launch {
                while (refreshThrottling() > 0) {
@@ -322,14 +243,14 @@ constructor(
    }

    /** Cancels any throttling state countdown started in [startThrottlingCountdown]. */
    private fun cancelCountdown() {
    private fun cancelThrottlingCountdown() {
        throttlingCountdownJob?.cancel()
        throttlingCountdownJob = null
    }

    /** Notifies that the currently-selected user has changed. */
    private suspend fun onSelectedUserChanged() {
        cancelCountdown()
        cancelThrottlingCountdown()
        if (refreshThrottling() > 0) {
            startThrottlingCountdown()
        }
@@ -378,7 +299,7 @@ constructor(
        DomainLayerAuthenticationMethodModel {
        return when (this) {
            is DataLayerAuthenticationMethodModel.None ->
                if (repository.isLockscreenEnabled()) {
                if (deviceEntryRepository.isInsecureLockscreenEnabled()) {
                    DomainLayerAuthenticationMethodModel.Swipe
                } else {
                    DomainLayerAuthenticationMethodModel.None
@@ -394,13 +315,10 @@ constructor(

/** Result of a user authentication attempt. */
enum class AuthenticationResult {
    /** Authentication succeeded and the device is now unlocked. */
    /** Authentication succeeded. */
    SUCCEEDED,
    /** Authentication failed and the device remains unlocked. */
    /** Authentication failed. */
    FAILED,
    /**
     * Authentication was not performed, e.g. due to insufficient input, and the device remains
     * unlocked.
     */
    /** Authentication was not performed, e.g. due to insufficient input. */
    SKIPPED,
}
+3 −1
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import com.android.systemui.classifier.FalsingClassifier
import com.android.systemui.classifier.domain.interactor.FalsingInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlags
@@ -50,6 +51,7 @@ constructor(
    @Application private val applicationScope: CoroutineScope,
    @Application private val applicationContext: Context,
    private val repository: BouncerRepository,
    private val deviceEntryInteractor: DeviceEntryInteractor,
    private val authenticationInteractor: AuthenticationInteractor,
    private val sceneInteractor: SceneInteractor,
    flags: SceneContainerFlags,
@@ -144,7 +146,7 @@ constructor(
        message: String? = null,
    ) {
        applicationScope.launch {
            if (authenticationInteractor.isAuthenticationRequired()) {
            if (deviceEntryInteractor.isAuthenticationRequired()) {
                repository.setMessage(
                    message ?: promptMessage(authenticationInteractor.getAuthenticationMethod())
                )
+2 −0
Original line number Diff line number Diff line
@@ -48,6 +48,7 @@ import com.android.systemui.controls.dagger.ControlsModule;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dagger.qualifiers.SystemUser;
import com.android.systemui.demomode.dagger.DemoModeModule;
import com.android.systemui.deviceentry.DeviceEntryModule;
import com.android.systemui.display.DisplayModule;
import com.android.systemui.doze.dagger.DozeComponent;
import com.android.systemui.dreams.dagger.DreamModule;
@@ -173,6 +174,7 @@ import javax.inject.Named;
        ControlsModule.class,
        CoroutinesModule.class,
        DemoModeModule.class,
        DeviceEntryModule.class,
        DisableFlagsModule.class,
        DisplayModule.class,
        DreamModule.class,
Loading