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

Commit 778b918d authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

[flexiglass] Centralizes automatic scene changing business logic.

We had automatic scene changing business logic spread out amongst
various scene-specific interactors. This was suboptimal for a few reasons:
1. Hard to reason about what changes the scenes and when, when it's all
   distributed
2. Hard to gate this logic based on the feature flag; causing memory and
   boot time regressions as seen in the attached files, because more
   resources were being used at startup even when the feature was off

This CL moves business logic from LockScreenSceneInteractor and
BouncerInteractor into SystemUiDefaultSceneContainerStartable and makes
sure it's all gated behind the feature flag.

Fix: 280883900, 289353099, 289726647
Test: new unit tests added
Test: manually verified in system UI that locking the device moves back
to the Lockscreen scene and unlocking the device moves to the Gone scene

Change-Id: I1dc46fc2d1d71f1b9baddc822ef69f5ea7b21cfc
parent 0407508c
Loading
Loading
Loading
Loading
+9 −31
Original line number Diff line number Diff line
@@ -25,6 +25,8 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode
import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel
import com.android.systemui.bouncer.data.repository.BouncerRepository
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
@@ -38,9 +40,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

@@ -53,6 +52,7 @@ constructor(
    private val repository: BouncerRepository,
    private val authenticationInteractor: AuthenticationInteractor,
    private val sceneInteractor: SceneInteractor,
    featureFlags: FeatureFlags,
    @Assisted private val containerName: String,
) {

@@ -95,30 +95,7 @@ constructor(
    val isPatternVisible: StateFlow<Boolean> = authenticationInteractor.isPatternVisible

    init {
        // UNLOCKING SHOWS Gone.
        //
        // Move to the gone scene if the device becomes unlocked while on the bouncer scene.
        applicationScope.launch {
            sceneInteractor
                .currentScene(containerName)
                .flatMapLatest { currentScene ->
                    if (currentScene.key == SceneKey.Bouncer) {
                        authenticationInteractor.isUnlocked
                    } else {
                        flowOf(false)
                    }
                }
                .distinctUntilChanged()
                .collect { isUnlocked ->
                    if (isUnlocked) {
                        sceneInteractor.setCurrentScene(
                            containerName = containerName,
                            scene = SceneModel(SceneKey.Gone),
                        )
                    }
                }
        }

        if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
            // Clear the message if moved from throttling to no-longer throttling.
            applicationScope.launch {
                isThrottled.pairwise().collect { (wasThrottled, currentlyThrottled) ->
@@ -128,6 +105,7 @@ constructor(
                }
            }
        }
    }

    /**
     * Returns the currently-configured authentication method. This determines how the
+44 −41
Original line number Diff line number Diff line
@@ -23,6 +23,8 @@ import com.android.systemui.R
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.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.util.kotlin.pairwise
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@@ -49,6 +51,7 @@ constructor(
    @Application private val applicationContext: Context,
    @Application private val applicationScope: CoroutineScope,
    interactorFactory: BouncerInteractor.Factory,
    featureFlags: FeatureFlags,
    @Assisted containerName: String,
) {
    private val interactor: BouncerInteractor = interactorFactory.create(containerName)
@@ -102,6 +105,38 @@ constructor(
        )

    init {
        if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
            applicationScope.launch {
                interactor.isThrottled
                    .map { isThrottled ->
                        if (isThrottled) {
                            when (interactor.getAuthenticationMethod()) {
                                is AuthenticationMethodModel.Pin ->
                                    R.string.kg_too_many_failed_pin_attempts_dialog_message
                                is AuthenticationMethodModel.Password ->
                                    R.string.kg_too_many_failed_password_attempts_dialog_message
                                is AuthenticationMethodModel.Pattern ->
                                    R.string.kg_too_many_failed_pattern_attempts_dialog_message
                                else -> null
                            }?.let { stringResourceId ->
                                applicationContext.getString(
                                    stringResourceId,
                                    interactor.throttling.value.failedAttemptCount,
                                    ceil(interactor.throttling.value.remainingMs / 1000f).toInt(),
                                )
                            }
                        } else {
                            null
                        }
                    }
                    .distinctUntilChanged()
                    .collect { dialogMessageOrNull ->
                        if (dialogMessageOrNull != null) {
                            _throttlingDialogMessage.value = dialogMessageOrNull
                        }
                    }
            }

            applicationScope.launch {
                _authMethod.subscriptionCount
                    .pairwise()
@@ -113,6 +148,7 @@ constructor(
                    }
            }
        }
    }

    /** The user-facing message to show in the bouncer. */
    val message: StateFlow<MessageViewModel> =
@@ -144,39 +180,6 @@ constructor(
     */
    val throttlingDialogMessage: StateFlow<String?> = _throttlingDialogMessage.asStateFlow()

    init {
        applicationScope.launch {
            interactor.isThrottled
                .map { isThrottled ->
                    if (isThrottled) {
                        when (interactor.getAuthenticationMethod()) {
                            is AuthenticationMethodModel.Pin ->
                                R.string.kg_too_many_failed_pin_attempts_dialog_message
                            is AuthenticationMethodModel.Password ->
                                R.string.kg_too_many_failed_password_attempts_dialog_message
                            is AuthenticationMethodModel.Pattern ->
                                R.string.kg_too_many_failed_pattern_attempts_dialog_message
                            else -> null
                        }?.let { stringResourceId ->
                            applicationContext.getString(
                                stringResourceId,
                                interactor.throttling.value.failedAttemptCount,
                                ceil(interactor.throttling.value.remainingMs / 1000f).toInt(),
                            )
                        }
                    } else {
                        null
                    }
                }
                .distinctUntilChanged()
                .collect { dialogMessageOrNull ->
                    if (dialogMessageOrNull != null) {
                        _throttlingDialogMessage.value = dialogMessageOrNull
                    }
                }
        }
    }

    /** Notifies that the emergency services button was clicked. */
    fun onEmergencyServicesButtonClicked() {
        // TODO(b/280877228): implement this
+0 −47
Original line number Diff line number Diff line
@@ -20,20 +20,14 @@ import com.android.systemui.authentication.domain.interactor.AuthenticationInter
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 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 lockscreen scene. */
class LockscreenSceneInteractor
@@ -42,7 +36,6 @@ constructor(
    @Application applicationScope: CoroutineScope,
    private val authenticationInteractor: AuthenticationInteractor,
    bouncerInteractorFactory: BouncerInteractor.Factory,
    private val sceneInteractor: SceneInteractor,
    @Assisted private val containerName: String,
) {
    private val bouncerInteractor: BouncerInteractor =
@@ -72,46 +65,6 @@ constructor(
                initialValue = false,
            )

    init {
        // LOCKING SHOWS Lockscreen.
        //
        // Move to the lockscreen 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
        // lockscreen 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),
                        )
                    }
                }
        }
    }

    /** Attempts to dismiss the lockscreen. This will cause the bouncer to show, if needed. */
    fun dismissLockscreen() {
        bouncerInteractor.showOrUnlockDevice(containerName = containerName)
+50 −3
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.systemui.scene.domain.startable

import com.android.systemui.CoreStartable
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.flags.FeatureFlags
@@ -24,9 +25,11 @@ import com.android.systemui.flags.Flags
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.SceneContainerNames
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch

@@ -41,17 +44,19 @@ class SystemUiDefaultSceneContainerStartable
constructor(
    @Application private val applicationScope: CoroutineScope,
    private val sceneInteractor: SceneInteractor,
    private val authenticationInteractor: AuthenticationInteractor,
    private val featureFlags: FeatureFlags,
) : CoreStartable {

    override fun start() {
        if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
            keepVisibilityUpdated()
            hydrateVisibility()
            automaticallySwitchScenes()
        }
    }

    /** Drives visibility of the scene container. */
    private fun keepVisibilityUpdated() {
    /** Updates the visibility of the scene container based on the current scene. */
    private fun hydrateVisibility() {
        applicationScope.launch {
            sceneInteractor
                .currentScene(CONTAINER_NAME)
@@ -63,6 +68,48 @@ constructor(
        }
    }

    /** Switches between scenes based on ever-changing application state. */
    private fun automaticallySwitchScenes() {
        applicationScope.launch {
            authenticationInteractor.isUnlocked
                .map { isUnlocked ->
                    val currentSceneKey = sceneInteractor.currentScene(CONTAINER_NAME).value.key
                    val isBypassEnabled = authenticationInteractor.isBypassEnabled.value
                    when {
                        isUnlocked ->
                            when (currentSceneKey) {
                                // When the device becomes unlocked in Bouncer, go to the Gone.
                                is SceneKey.Bouncer -> SceneKey.Gone
                                // When the device becomes unlocked in Lockscreen, go to Gone if
                                // bypass is enabled.
                                is SceneKey.Lockscreen -> SceneKey.Gone.takeIf { isBypassEnabled }
                                // We got unlocked while on a scene that's not Lockscreen or
                                // Bouncer, no need to change scenes.
                                else -> null
                            }
                        // When the device becomes locked, to Lockscreen.
                        !isUnlocked ->
                            when (currentSceneKey) {
                                // Already on lockscreen or bouncer, no need to change scenes.
                                is SceneKey.Lockscreen,
                                is SceneKey.Bouncer -> null
                                // We got locked while on a scene that's not Lockscreen or Bouncer,
                                // go to Lockscreen.
                                else -> SceneKey.Lockscreen
                            }
                        else -> null
                    }
                }
                .filterNotNull()
                .collect { targetSceneKey ->
                    sceneInteractor.setCurrentScene(
                        containerName = CONTAINER_NAME,
                        scene = SceneModel(targetSceneKey),
                    )
                }
        }
    }

    companion object {
        private const val CONTAINER_NAME = SceneContainerNames.SYSTEM_UI_DEFAULT
    }
+0 −17
Original line number Diff line number Diff line
@@ -345,23 +345,6 @@ class BouncerInteractorTest : SysuiTestCase() {
            assertThat(throttling).isEqualTo(AuthenticationThrottlingModel())
        }

    @Test
    fun switchesToGone_whenUnlocked() =
        testScope.runTest {
            utils.authenticationRepository.setUnlocked(false)
            sceneInteractor.setCurrentScene(
                SceneTestUtils.CONTAINER_1,
                SceneModel(SceneKey.Bouncer)
            )
            val currentScene by
                collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1))
            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))

            utils.authenticationRepository.setUnlocked(true)

            assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Gone))
        }

    private fun assertTryAgainMessage(
        message: String?,
        time: Int,
Loading