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

Commit 1a576fe3 authored by Ale Nijamkin's avatar Ale Nijamkin
Browse files

[flexiglass] Fixes crashloop on Wear

Wear System UI crashes because DeviceEntryInteractor which is depended
upon by LogContextInteractorImpl, in turn, pulls in many dependencies
and some of those dependencies cause runtime crashes either immediately
or soon after.

By making the dependencies and properties of DeviceEntryInteractor lazy,
we avoid pulling in those dependencies and their instantiation-time
side-effects.

To deploy System UI on a Wear device:
- lunch and flash a wear device with mp droid
- skip the setup wizard using:
$ adb shell am broadcast -a com.google.android.clockwork.action.TEST_MODE
- make sure remount by running this command, twice:
$ adb remount -R && adb wait-for-device root && adb remount
- make local changes to System UI
- deploy those changes by running:
$ make ClockworkSystemUI && adb shell stop && adb sync && adb shell start

Bug: 412766066
Test: manually deployed on a Wear device and turned on Flexiglass. The
crashloop is gone and the systemui process remains running
Flag: com.android.systemui.scene_container

Change-Id: Iecf147276e04596f22ad7761a5f640cb42b557cf
parent 6d70581d
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.display.data.repository.DeviceStateRepository
import com.android.systemui.display.data.repository.fakeDeviceStateRepository
import com.android.systemui.flags.DisableSceneContainer
@@ -41,6 +42,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -174,10 +176,15 @@ class LogContextInteractorImplTest : SysuiTestCase() {
            assertThat(displayState()).isEqualTo(AuthenticateOptions.DISPLAY_STATE_LOCKSCREEN)

            // Unlock the device.
            val isDeviceUnlocked by
                collectLastValue(
                    kosmos.deviceUnlockedInteractor.deviceUnlockStatus.map { it.isUnlocked }
                )
            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
                SuccessFingerprintAuthenticationStatus(0, true)
            )
            runCurrent()
            assertThat(isDeviceUnlocked).isTrue()
            kosmos.sceneInteractor.snapToScene(Scenes.Gone, "")

            keyguardTransitionRepository.startTransitionTo(KeyguardState.GONE)
+7 −0
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
@@ -48,6 +49,7 @@ import com.google.common.truth.Truth.assertThat
import java.util.Locale
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
@@ -210,10 +212,15 @@ class ShadeSceneContentViewModelTest : SysuiTestCase() {
    private fun TestScope.setDeviceEntered(isEntered: Boolean) {
        if (isEntered) {
            // Unlock the device marking the device has entered.
            val isDeviceUnlocked by
                collectLastValue(
                    kosmos.deviceUnlockedInteractor.deviceUnlockStatus.map { it.isUnlocked }
                )
            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
                SuccessFingerprintAuthenticationStatus(0, true)
            )
            runCurrent()
            assertThat(isDeviceUnlocked).isTrue()
        }
        setScene(
            if (isEntered) {
+51 −36
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@ import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.util.kotlin.pairwise
import com.android.systemui.utils.coroutines.flow.mapLatestConflated
import dagger.Lazy
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@@ -59,14 +60,14 @@ class DeviceEntryInteractor
@Inject
constructor(
    @Application private val applicationScope: CoroutineScope,
    private val repository: DeviceEntryRepository,
    private val authenticationInteractor: AuthenticationInteractor,
    private val sceneInteractor: SceneInteractor,
    private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
    private val alternateBouncerInteractor: AlternateBouncerInteractor,
    private val dismissCallbackRegistry: DismissCallbackRegistry,
    sceneBackInteractor: SceneBackInteractor,
    @SceneFrameworkTableLog private val tableLogBuffer: TableLogBuffer,
    private val repository: Lazy<DeviceEntryRepository>,
    private val authenticationInteractor: Lazy<AuthenticationInteractor>,
    private val sceneInteractor: Lazy<SceneInteractor>,
    private val deviceUnlockedInteractor: Lazy<DeviceUnlockedInteractor>,
    private val alternateBouncerInteractor: Lazy<AlternateBouncerInteractor>,
    private val dismissCallbackRegistry: Lazy<DismissCallbackRegistry>,
    sceneBackInteractor: Lazy<SceneBackInteractor>,
    @SceneFrameworkTableLog private val tableLogBuffer: Lazy<TableLogBuffer>,
) {
    /**
     * Whether the device is unlocked.
@@ -77,14 +78,17 @@ constructor(
     * 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> =
        deviceUnlockedInteractor.deviceUnlockStatus
    val isUnlocked: StateFlow<Boolean> by lazy {
        deviceUnlockedInteractor
            .get()
            .deviceUnlockStatus
            .map { it.isUnlocked }
            .stateIn(
                scope = applicationScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked,
                initialValue = deviceUnlockedInteractor.get().deviceUnlockStatus.value.isUnlocked,
            )
    }

    /**
     * Emits `true` when the current scene switches to [Scenes.Gone] for the first time after having
@@ -96,8 +100,10 @@ constructor(
     * [Scenes.Gone] but the bottommost entry of the navigation back stack switched from
     * [Scenes.Lockscreen] to [Scenes.Gone] while the user is staring at another scene.
     */
    val isDeviceEnteredDirectly: StateFlow<Boolean> =
        sceneInteractor.currentScene
    val isDeviceEnteredDirectly: StateFlow<Boolean> by lazy {
        sceneInteractor
            .get()
            .currentScene
            .filter { currentScene ->
                currentScene == Scenes.Gone || currentScene == Scenes.Lockscreen
            }
@@ -105,7 +111,7 @@ constructor(
                if (scene == Scenes.Gone) {
                    // Make sure device unlock status is definitely unlocked before we
                    // consider the device "entered".
                    deviceUnlockedInteractor.deviceUnlockStatus.first { it.isUnlocked }
                    deviceUnlockedInteractor.get().deviceUnlockStatus.first { it.isUnlocked }
                    true
                } else {
                    false
@@ -116,6 +122,7 @@ constructor(
                started = SharingStarted.Eagerly,
                initialValue = false,
            )
    }

    /**
     * Whether the device has been entered (i.e. the lockscreen has been dismissed, by any method).
@@ -129,7 +136,7 @@ constructor(
     * navigation back stack into account and will only produce a `true` value even when the current
     * scene is actually [Scenes.Gone].
     */
    val isDeviceEntered: StateFlow<Boolean> =
    val isDeviceEntered: StateFlow<Boolean> by lazy {
        combine(
                // This flow emits true when the currentScene switches to Gone for the first time
                // after having been on Lockscreen.
@@ -137,7 +144,9 @@ constructor(
                // This flow emits true only if the bottom of the navigation back stack has been
                // switched from Lockscreen to Gone. In other words, only if the device was unlocked
                // while visiting at least one scene "above" the Lockscreen scene.
                sceneBackInteractor.backStack
                sceneBackInteractor
                    .get()
                    .backStack
                    // The bottom of the back stack, which is Lockscreen, Gone, or null if empty.
                    .map { it.asIterable().lastOrNull() }
                    // Filter out cases where the stack changes but the bottom remains unchanged.
@@ -153,7 +162,7 @@ constructor(
                enteredOnBackStack || enteredDirectly
            }
            .logDiffsForTable(
                tableLogBuffer = tableLogBuffer,
                tableLogBuffer = tableLogBuffer.get(),
                columnName = "isDeviceEntered",
                initialValue = false,
            )
@@ -162,9 +171,10 @@ constructor(
                started = SharingStarted.Eagerly,
                initialValue = false,
            )
    }

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

    /**
@@ -181,11 +191,11 @@ constructor(
     */
    val canSwipeToEnter: StateFlow<Boolean?> by lazy {
        combine(
                authenticationInteractor.authenticationMethod.map {
                authenticationInteractor.get().authenticationMethod.map {
                    it == AuthenticationMethodModel.None
                },
                isLockscreenEnabled,
                deviceUnlockedInteractor.deviceUnlockStatus,
                deviceUnlockedInteractor.get().deviceUnlockStatus,
                isDeviceEntered,
            ) { isNoneAuthMethod, isLockscreenEnabled, deviceUnlockStatus, isDeviceEntered ->
                val isSwipeAuthMethod = isNoneAuthMethod && isLockscreenEnabled
@@ -195,7 +205,7 @@ constructor(
                    !isDeviceEntered
            }
            .logDiffsForTable(
                tableLogBuffer = tableLogBuffer,
                tableLogBuffer = tableLogBuffer.get(),
                columnName = "canSwipeToEnter",
                initialValue = false,
            )
@@ -218,7 +228,7 @@ constructor(
     */
    @JvmOverloads
    fun attemptDeviceEntry(callback: IKeyguardDismissCallback? = null) {
        callback?.let { dismissCallbackRegistry.addCallback(it) }
        callback?.let { dismissCallbackRegistry.get().addCallback(it) }

        // TODO (b/307768356),
        //       1. Check if the device is already authenticated by trust agent/passive biometrics
@@ -228,18 +238,23 @@ constructor(
        //       4. Transition to bouncer scene
        applicationScope.launch {
            if (isAuthenticationRequired()) {
                if (alternateBouncerInteractor.canShowAlternateBouncer.value) {
                    alternateBouncerInteractor.forceShow()
                if (alternateBouncerInteractor.get().canShowAlternateBouncer.value) {
                    alternateBouncerInteractor.get().forceShow()
                } else {
                    sceneInteractor.showOverlay(
                    sceneInteractor
                        .get()
                        .showOverlay(
                            overlay = Overlays.Bouncer,
                            loggingReason = "request to unlock device while authentication required",
                        )
                }
            } else {
                sceneInteractor.changeScene(
                sceneInteractor
                    .get()
                    .changeScene(
                        toScene = Scenes.Gone,
                    loggingReason = "request to unlock device while authentication isn't required",
                        loggingReason =
                            "request to unlock device while authentication isn't required",
                    )
            }
        }
@@ -250,8 +265,8 @@ constructor(
     * `false` if the device can be entered without authenticating first.
     */
    suspend fun isAuthenticationRequired(): Boolean {
        return !deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked &&
            authenticationInteractor.getAuthenticationMethod().isSecure
        return !deviceUnlockedInteractor.get().deviceUnlockStatus.value.isUnlocked &&
            authenticationInteractor.get().getAuthenticationMethod().isSecure
    }

    /**
@@ -260,7 +275,7 @@ constructor(
     * when the user swipes on it.
     */
    suspend fun isLockscreenEnabled(): Boolean {
        return repository.isLockscreenEnabled()
        return repository.get().isLockscreenEnabled()
    }

    /**
@@ -276,6 +291,6 @@ constructor(

    /** Locks the device instantly. */
    fun lockNow(debuggingReason: String) {
        deviceUnlockedInteractor.lockNow(debuggingReason)
        deviceUnlockedInteractor.get().lockNow(debuggingReason)
    }
}
+8 −8
Original line number Diff line number Diff line
@@ -30,13 +30,13 @@ val Kosmos.deviceEntryInteractor by
    Kosmos.Fixture {
        DeviceEntryInteractor(
            applicationScope = applicationCoroutineScope,
            repository = deviceEntryRepository,
            authenticationInteractor = authenticationInteractor,
            sceneInteractor = sceneInteractor,
            deviceUnlockedInteractor = deviceUnlockedInteractor,
            alternateBouncerInteractor = alternateBouncerInteractor,
            dismissCallbackRegistry = dismissCallbackRegistry,
            sceneBackInteractor = sceneBackInteractor,
            tableLogBuffer = logcatTableLogBuffer(this, "sceneFrameworkTableLogBuffer"),
            repository = { deviceEntryRepository },
            authenticationInteractor = { authenticationInteractor },
            sceneInteractor = { sceneInteractor },
            deviceUnlockedInteractor = { deviceUnlockedInteractor },
            alternateBouncerInteractor = { alternateBouncerInteractor },
            dismissCallbackRegistry = { dismissCallbackRegistry },
            sceneBackInteractor = { sceneBackInteractor },
            tableLogBuffer = { logcatTableLogBuffer(this, "sceneFrameworkTableLogBuffer") },
        )
    }