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

Commit ee98de19 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

[flexiglass] Fixes back/home navigation when occluded.

Before this, back or home navigation when the lockscreen is occluded by
another activity (for example, the camera app when locked) didn't work.

This CL fixes that issue.

The approach is one where SysUiState is hydrated with SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED when on lockscreen and occluded instead of SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING when on lockscreen but not occluded.

For posterity, the command used to figure out the difference betwene
SysUiState flags with Flexiglas on and off was:

$ adb shell dumpsys activity service com.android.systemui/.SystemUIService SysUiState

Fix: 308001302
Test: unit tests added
Test: manually verified that back and home gesture both work when
showing the camera app on top of the locked device
Flag: ACONFIG com.android.systemui.scene_container DEVELOPMENT

Change-Id: I707a46b502ad7697985794084b05f35493bcdf19
parent 0aa7287f
Loading
Loading
Loading
Loading
+44 −2
Original line number Diff line number Diff line
@@ -42,7 +42,7 @@ import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepo
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.kosmos.testScope
import com.android.systemui.model.SysUiState
import com.android.systemui.model.sysUiState
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
import com.android.systemui.power.domain.interactor.PowerInteractorFactory
@@ -51,6 +51,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
import com.android.systemui.shared.system.QuickStepContract
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
@@ -78,6 +79,7 @@ import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -99,7 +101,7 @@ class SceneContainerStartableTest : SysuiTestCase() {
    private val faceAuthRepository by lazy { kosmos.fakeDeviceEntryFaceAuthRepository }
    private val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor }
    private val keyguardInteractor by lazy { kosmos.keyguardInteractor }
    private val sysUiState: SysUiState = mock()
    private val sysUiState = spy(kosmos.sysUiState)
    private val falsingCollector: FalsingCollector = mock()
    private val powerInteractor = PowerInteractorFactory.create().powerInteractor
    private val fakeSceneDataSource = kosmos.fakeSceneDataSource
@@ -397,6 +399,46 @@ class SceneContainerStartableTest : SysuiTestCase() {
                }
        }

    @Test
    fun hydrateSystemUiState_onLockscreen_basedOnOcclusion() =
        testScope.runTest {
            prepareState(
                initialSceneKey = Scenes.Lockscreen,
            )
            underTest.start()
            runCurrent()
            clearInvocations(sysUiState)

            kosmos.keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(
                true,
                mock()
            )
            runCurrent()
            assertThat(
                    sysUiState.flags and
                        QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED != 0
                )
                .isTrue()
            assertThat(
                    sysUiState.flags and
                        QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING != 0
                )
                .isFalse()

            kosmos.keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(false)
            runCurrent()
            assertThat(
                    sysUiState.flags and
                        QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED != 0
                )
                .isFalse()
            assertThat(
                    sysUiState.flags and
                        QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING != 0
                )
                .isTrue()
        }

    @Test
    fun switchToGoneWhenDeviceStartsToWakeUp_authMethodNone() =
        testScope.runTest {
+33 −9
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.systemui.model
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.shared.model.Scenes
@@ -27,6 +28,7 @@ import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICA
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED
import dagger.Lazy
import javax.inject.Inject

@@ -38,8 +40,10 @@ import javax.inject.Inject
class SceneContainerPlugin
@Inject
constructor(
    private val interactor: Lazy<SceneInteractor>,
    private val sceneInteractor: Lazy<SceneInteractor>,
    private val occlusionInteractor: Lazy<SceneContainerOcclusionInteractor>,
) {

    /**
     * Returns an override value for the given [flag] or `null` if the scene framework isn't enabled
     * or if the flag value doesn't need to be overridden.
@@ -49,10 +53,18 @@ constructor(
            return null
        }

        val transitionState = interactor.get().transitionState.value
        val transitionState = sceneInteractor.get().transitionState.value
        val idleTransitionStateOrNull = transitionState as? ObservableTransitionState.Idle
        val currentSceneOrNull = idleTransitionStateOrNull?.scene
        return currentSceneOrNull?.let { sceneKey -> EvaluatorByFlag[flag]?.invoke(sceneKey) }
        val invisibleDueToOcclusion = occlusionInteractor.get().invisibleDueToOcclusion.value
        return currentSceneOrNull?.let { sceneKey ->
            EvaluatorByFlag[flag]?.invoke(
                SceneContainerPluginState(
                    scene = sceneKey,
                    invisibleDueToOcclusion = invisibleDueToOcclusion,
                )
            )
        }
    }

    companion object {
@@ -67,12 +79,24 @@ constructor(
         * to be overridden by the scene framework.
         */
        val EvaluatorByFlag =
            mapOf<Int, (SceneKey) -> Boolean>(
                SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE to { it != Scenes.Gone },
                SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED to { it == Scenes.Shade },
                SYSUI_STATE_QUICK_SETTINGS_EXPANDED to { it == Scenes.QuickSettings },
                SYSUI_STATE_BOUNCER_SHOWING to { it == Scenes.Bouncer },
                SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING to { it == Scenes.Lockscreen },
            mapOf<Int, (SceneContainerPluginState) -> Boolean>(
                SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE to { it.scene != Scenes.Gone },
                SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED to { it.scene == Scenes.Shade },
                SYSUI_STATE_QUICK_SETTINGS_EXPANDED to { it.scene == Scenes.QuickSettings },
                SYSUI_STATE_BOUNCER_SHOWING to { it.scene == Scenes.Bouncer },
                SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING to
                    {
                        it.scene == Scenes.Lockscreen && !it.invisibleDueToOcclusion
                    },
                SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED to
                    {
                        it.scene == Scenes.Lockscreen && it.invisibleDueToOcclusion
                    },
            )
    }

    data class SceneContainerPluginState(
        val scene: SceneKey,
        val invisibleDueToOcclusion: Boolean,
    )
}
+59 −13
Original line number Diff line number Diff line
@@ -19,45 +19,91 @@ package com.android.systemui.scene.domain.interactor
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.domain.interactor.KeyguardOcclusionInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.scene.shared.model.Scenes
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
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.onStart
import kotlinx.coroutines.flow.stateIn

/** Encapsulates logic regarding the occlusion state of the scene container. */
@SysUISingleton
class SceneContainerOcclusionInteractor
@Inject
constructor(
    @Application applicationScope: CoroutineScope,
    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
    sceneInteractor: SceneInteractor,
    keyguardTransitionInteractor: KeyguardTransitionInteractor,
) {
    /** Whether a show-when-locked activity is at the top of the current activity stack. */
    private val isOccludingActivityShown: StateFlow<Boolean> =
        keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop.stateIn(
            scope = applicationScope,
            started = SharingStarted.WhileSubscribed(),
            initialValue = false,
        )

    /**
     * Whether the scene container should become invisible due to "occlusion" by an in-foreground
     * "show when locked" activity.
     * Whether AOD is fully shown (not transitioning) or partially shown during a transition to/from
     * AOD.
     */
    val invisibleDueToOcclusion: Flow<Boolean> =
        combine(
                keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop,
                sceneInteractor.transitionState,
    private val isAodFullyOrPartiallyShown: StateFlow<Boolean> =
        keyguardTransitionInteractor
            .transitionValue(KeyguardState.AOD)
            .onStart { emit(0f) }
            .map { it > 0 }
                    .distinctUntilChanged(),
            .stateIn(
                scope = applicationScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = false,
            )

    /**
     * Whether the scene container should become invisible due to "occlusion" by an in-foreground
     * "show when locked" activity.
     */
    val invisibleDueToOcclusion: StateFlow<Boolean> =
        combine(
                isOccludingActivityShown,
                sceneInteractor.transitionState,
                isAodFullyOrPartiallyShown,
            ) { isOccludingActivityShown, sceneTransitionState, isAodFullyOrPartiallyShown ->
                isOccludingActivityShown &&
                invisibleDueToOcclusion(
                    isOccludingActivityShown = isOccludingActivityShown,
                    sceneTransitionState = sceneTransitionState,
                    isAodFullyOrPartiallyShown = isAodFullyOrPartiallyShown,
                )
            }
            .stateIn(
                scope = applicationScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue =
                    invisibleDueToOcclusion(
                        isOccludingActivityShown = isOccludingActivityShown.value,
                        sceneTransitionState = sceneInteractor.transitionState.value,
                        isAodFullyOrPartiallyShown = isAodFullyOrPartiallyShown.value,
                    ),
            )

    private fun invisibleDueToOcclusion(
        isOccludingActivityShown: Boolean,
        sceneTransitionState: ObservableTransitionState,
        isAodFullyOrPartiallyShown: Boolean,
    ): Boolean {
        return isOccludingActivityShown &&
            // Cannot be occluded in AOD.
            !isAodFullyOrPartiallyShown &&
            // Only some scenes can be occluded.
            sceneTransitionState.canBeOccluded
    }
            .distinctUntilChanged()

    private val ObservableTransitionState.canBeOccluded: Boolean
        get() =
+14 −6
Original line number Diff line number Diff line
@@ -317,15 +317,23 @@ constructor(
    /** Keeps [SysUiState] up-to-date */
    private fun hydrateSystemUiState() {
        applicationScope.launch {
            combine(
                    sceneInteractor.transitionState
                        .mapNotNull { it as? ObservableTransitionState.Idle }
                        .map { it.scene }
                .distinctUntilChanged()
                .collect { sceneKey ->
                        .distinctUntilChanged(),
                    occlusionInteractor.invisibleDueToOcclusion,
                ) { sceneKey, invisibleDueToOcclusion ->
                    SceneContainerPlugin.SceneContainerPluginState(
                        scene = sceneKey,
                        invisibleDueToOcclusion = invisibleDueToOcclusion,
                    )
                }
                .collect { sceneContainerPluginState ->
                    sysUiState.updateFlags(
                        displayId,
                        *SceneContainerPlugin.EvaluatorByFlag.map { (flag, evaluator) ->
                                flag to evaluator.invoke(sceneKey)
                                flag to evaluator.invoke(sceneContainerPluginState)
                            }
                            .toTypedArray(),
                    )
+7 −1
Original line number Diff line number Diff line
@@ -18,6 +18,12 @@ package com.android.systemui.model

import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor

val Kosmos.sceneContainerPlugin by Fixture { SceneContainerPlugin { sceneInteractor } }
val Kosmos.sceneContainerPlugin by Fixture {
    SceneContainerPlugin(
        sceneInteractor = { sceneInteractor },
        occlusionInteractor = { sceneContainerOcclusionInteractor },
    )
}
Loading