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

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

[flexiglass] Pick up on non-secure auth method changes.

This CL adds logic that detects and responds to authentication method
changes between None and Swipe.

These changes occur in the Settings app, outside system UI. Unlike
changes between the different secure auth methods (PIN, password,
pattern) or between a secure auth method and a non-secure auth method
(none or swipe) and vice-versa, a switch from none to swipe or from
swipe to none has no callback mechanism nor does it produce any sort of
broadcast that system UI can depend on.

The CL takes the approach of refreshing the value whenever the scene is
starting to transition to the lockscreen scene. A flow was added to
DeviceEntryInteractor/Repository that's hydrated with a value each time
isLockscreenEnabled is invoked. Logic was added to the
SceneContainerStartable to trigger the isLockscreenEnabled method each
time a transition to the lockscreen scene is started.

Fix: 353323330
Test: added integration test at the SceneContainerStartableTest level
Test: manually verified that switching between none and swipe results in
the proper behaviour on the lockscreen
Test: manually verified switching from a secure auth method into one of
the two non-secure ones and back, also making sure it results with
proper behaviour on the lockscreen
Flag: com.android.systemui.scene_container

Change-Id: I4533345ea35511dee3ec2cc833f0b2856bd42bcd
parent d2715b13
Loading
Loading
Loading
Loading
+57 −0
Original line number Diff line number Diff line
@@ -1559,6 +1559,63 @@ class SceneContainerStartableTest : SysuiTestCase() {
            verify(dismissCallback).onDismissCancelled()
        }

    @Test
    fun refreshLockscreenEnabled() =
        testScope.runTest {
            val transitionState =
                prepareState(
                    isDeviceUnlocked = true,
                    initialSceneKey = Scenes.Gone,
                )
            underTest.start()
            val isLockscreenEnabled by
                collectLastValue(kosmos.deviceEntryInteractor.isLockscreenEnabled)
            assertThat(isLockscreenEnabled).isTrue()

            kosmos.fakeDeviceEntryRepository.setPendingLockscreenEnabled(false)
            runCurrent()
            // Pending value didn't propagate yet.
            assertThat(isLockscreenEnabled).isTrue()

            // Starting a transition to Lockscreen should refresh the value, causing the pending
            // value
            // to propagate to the real flow:
            transitionState.value =
                ObservableTransitionState.Transition(
                    fromScene = Scenes.Gone,
                    toScene = Scenes.Lockscreen,
                    currentScene = flowOf(Scenes.Gone),
                    progress = flowOf(0.1f),
                    isInitiatedByUserInput = false,
                    isUserInputOngoing = flowOf(false),
                )
            runCurrent()
            assertThat(isLockscreenEnabled).isFalse()

            kosmos.fakeDeviceEntryRepository.setPendingLockscreenEnabled(true)
            runCurrent()
            // Pending value didn't propagate yet.
            assertThat(isLockscreenEnabled).isFalse()
            transitionState.value = ObservableTransitionState.Idle(Scenes.Gone)
            runCurrent()
            assertThat(isLockscreenEnabled).isFalse()

            // Starting another transition to Lockscreen should refresh the value, causing the
            // pending
            // value to propagate to the real flow:
            transitionState.value =
                ObservableTransitionState.Transition(
                    fromScene = Scenes.Gone,
                    toScene = Scenes.Lockscreen,
                    currentScene = flowOf(Scenes.Gone),
                    progress = flowOf(0.1f),
                    isInitiatedByUserInput = false,
                    isUserInputOngoing = flowOf(false),
                )
            runCurrent()
            assertThat(isLockscreenEnabled).isTrue()
        }

    private fun TestScope.emulateSceneTransition(
        transitionStateFlow: MutableStateFlow<ObservableTransitionState>,
        toScene: SceneKey,
+21 −7
Original line number Diff line number Diff line
@@ -13,8 +13,10 @@ import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext

@@ -25,7 +27,7 @@ interface DeviceEntryRepository {
     * chosen any secure authentication method and even if they set the lockscreen to be dismissed
     * when the user swipes on it.
     */
    suspend fun isLockscreenEnabled(): Boolean
    val isLockscreenEnabled: StateFlow<Boolean>

    /**
     * Whether lockscreen bypass is enabled. When enabled, the lockscreen will be automatically
@@ -39,6 +41,13 @@ interface DeviceEntryRepository {
     * the lockscreen.
     */
    val isBypassEnabled: StateFlow<Boolean>

    /**
     * Whether the lockscreen is enabled for the current user. This is `true` whenever the user has
     * chosen any secure authentication method and even if they set the lockscreen to be dismissed
     * when the user swipes on it.
     */
    suspend fun isLockscreenEnabled(): Boolean
}

/** Encapsulates application state for device entry. */
@@ -53,12 +62,8 @@ constructor(
    private val keyguardBypassController: KeyguardBypassController,
) : DeviceEntryRepository {

    override suspend fun isLockscreenEnabled(): Boolean {
        return withContext(backgroundDispatcher) {
            val selectedUserId = userRepository.getSelectedUserInfo().id
            !lockPatternUtils.isLockScreenDisabled(selectedUserId)
        }
    }
    private val _isLockscreenEnabled = MutableStateFlow(true)
    override val isLockscreenEnabled: StateFlow<Boolean> = _isLockscreenEnabled.asStateFlow()

    override val isBypassEnabled: StateFlow<Boolean> =
        conflatedCallbackFlow {
@@ -78,6 +83,15 @@ constructor(
                SharingStarted.Eagerly,
                initialValue = keyguardBypassController.bypassEnabled,
            )

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

@Module
+21 −4
Original line number Diff line number Diff line
@@ -28,12 +28,14 @@ import com.android.systemui.utils.coroutines.flow.mapLatestConflated
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

@@ -101,6 +103,10 @@ constructor(
                initialValue = false,
            )

    val isLockscreenEnabled: Flow<Boolean> by lazy {
        repository.isLockscreenEnabled.onStart { refreshLockscreenEnabled() }
    }

    /**
     * Whether it's currently possible to swipe up to enter the device without requiring
     * authentication or when the device is already authenticated using a passive authentication
@@ -115,14 +121,14 @@ constructor(
     */
    val canSwipeToEnter: StateFlow<Boolean?> =
        combine(
                // This is true when the user has chosen to show the lockscreen but has not made it
                // secure.
                authenticationInteractor.authenticationMethod.map {
                    it == AuthenticationMethodModel.None && repository.isLockscreenEnabled()
                    it == AuthenticationMethodModel.None
                },
                isLockscreenEnabled,
                deviceUnlockedInteractor.deviceUnlockStatus,
                isDeviceEntered
            ) { isSwipeAuthMethod, deviceUnlockStatus, isDeviceEntered ->
            ) { isNoneAuthMethod, isLockscreenEnabled, deviceUnlockStatus, isDeviceEntered ->
                val isSwipeAuthMethod = isNoneAuthMethod && isLockscreenEnabled
                (isSwipeAuthMethod ||
                    (deviceUnlockStatus.isUnlocked &&
                        deviceUnlockStatus.deviceUnlockSource?.dismissesLockscreen == false)) &&
@@ -185,6 +191,17 @@ constructor(
        return repository.isLockscreenEnabled()
    }

    /**
     * Forces a refresh of the value of [isLockscreenEnabled] such that the flow emits the latest
     * value.
     *
     * Without calling this method, the flow will have a stale value unless the collector is removed
     * and re-added.
     */
    suspend fun refreshLockscreenEnabled() {
        isLockscreenEnabled()
    }

    /**
     * Whether lockscreen bypass is enabled. When enabled, the lockscreen will be automatically
     * dismissed once the authentication challenge is completed. For example, completing a biometric
+3 −3
Original line number Diff line number Diff line
@@ -25,7 +25,7 @@ import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
import com.android.systemui.keyguard.KeyguardWmStateRefactor
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.BiometricUnlockMode.Companion.isWakeAndUnlock
@@ -59,7 +59,7 @@ constructor(
    private val communalInteractor: CommunalInteractor,
    private val communalSceneInteractor: CommunalSceneInteractor,
    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
    val deviceEntryRepository: DeviceEntryRepository,
    val deviceEntryInteractor: DeviceEntryInteractor,
    private val wakeToGoneInteractor: KeyguardWakeDirectlyToGoneInteractor,
    private val dreamManager: DreamManager,
) :
@@ -146,7 +146,7 @@ constructor(
                        isIdleOnCommunal,
                        canTransitionToGoneOnWake,
                        primaryBouncerShowing) ->
                    if (!deviceEntryRepository.isLockscreenEnabled()) {
                    if (!deviceEntryInteractor.isLockscreenEnabled()) {
                        if (SceneContainerFlag.isEnabled) {
                            // TODO(b/336576536): Check if adaptation for scene framework is needed
                        } else {
+19 −0
Original line number Diff line number Diff line
@@ -149,6 +149,7 @@ constructor(
            resetShadeSessions()
            handleKeyguardEnabledness()
            notifyKeyguardDismissCallbacks()
            refreshLockscreenEnabled()
        } else {
            sceneLogger.logFrameworkEnabled(
                isEnabled = false,
@@ -735,4 +736,22 @@ constructor(
            }
        }
    }

    /**
     * Keeps the value of [DeviceEntryInteractor.isLockscreenEnabled] fresh.
     *
     * This is needed because that value is sourced from a non-observable data source
     * (`LockPatternUtils`, which doesn't expose a listener or callback for this value). Therefore,
     * every time a transition to the `Lockscreen` scene is started, the value is re-fetched and
     * cached.
     */
    private fun refreshLockscreenEnabled() {
        applicationScope.launch {
            sceneInteractor.transitionState
                .map { it.isTransitioning(to = Scenes.Lockscreen) }
                .distinctUntilChanged()
                .filter { it }
                .collectLatest { deviceEntryInteractor.refreshLockscreenEnabled() }
        }
    }
}
Loading