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

Commit a5e9826e authored by burakov's avatar burakov
Browse files

[flexiglass] Apply user actions for the top content (overlay or scene).

We consider only the top-most content (by z-index order) to be "active"
for the purpose of mapping user actions to user action results.

Bonus 1: Use LinkedHashMap when creating the mappings "sortedSceneByKey"
and "sortedOverlayByKey", in order to have an explicitly guaranteed
iteration order on which STL relies.

Bonus 2: Activate overlays when they're added to STL.

Bug: 359173565
Bug: 356596436
Flag: com.android.systemui.scene_container
Test: Added unit tests.
Test: Existing unit tests still pass.
Change-Id: Ie0c6452d2842606bd015c860da8684f26243f00f
parent 1c32e124
Loading
Loading
Loading
Loading
+17 −8
Original line number Diff line number Diff line
@@ -84,7 +84,6 @@ fun SceneContainer(
            enableInterruptions = false,
        )
    }
    val currentSceneKey = state.transitionState.currentScene

    DisposableEffect(state) {
        val dataSource = SceneTransitionLayoutDataSource(state, coroutineScope)
@@ -97,19 +96,26 @@ fun SceneContainer(
        onDispose { viewModel.setTransitionState(null) }
    }

    val actionableContentKey =
        viewModel.getActionableContentKey(state.currentScene, state.currentOverlays, overlayByKey)
    val userActionsByContentKey: MutableMap<ContentKey, Map<UserAction, UserActionResult>> =
        remember {
            mutableStateMapOf()
        }
    // TODO(b/359173565): Add overlay user actions when the API is final.
    LaunchedEffect(currentSceneKey) {
    LaunchedEffect(actionableContentKey) {
        try {
            sceneByKey[currentSceneKey]?.userActions?.collectLatest { userActions ->
                userActionsByContentKey[currentSceneKey] =
            val actionableContent: ActionableContent =
                checkNotNull(
                    overlayByKey[actionableContentKey] ?: sceneByKey[actionableContentKey]
                ) {
                    "invalid ContentKey: $actionableContentKey"
                }
            actionableContent.userActions.collectLatest { userActions ->
                userActionsByContentKey[actionableContentKey] =
                    viewModel.resolveSceneFamilies(userActions)
            }
        } finally {
            userActionsByContentKey[currentSceneKey] = emptyMap()
            userActionsByContentKey[actionableContentKey] = emptyMap()
        }
    }

@@ -139,13 +145,16 @@ fun SceneContainer(
                    }
                }
            }
            overlayByKey.forEach { (overlayKey, composableOverlay) ->
            overlayByKey.forEach { (overlayKey, overlay) ->
                overlay(
                    key = overlayKey,
                    userActions = userActionsByContentKey.getOrDefault(overlayKey, emptyMap())
                ) {
                    // Activate the overlay.
                    LaunchedEffect(overlay) { overlay.activate() }

                    // Render the overlay.
                    with(composableOverlay) { this@overlay.Content(Modifier) }
                    with(overlay) { this@overlay.Content(Modifier) }
                }
            }
        }
+44 −0
Original line number Diff line number Diff line
@@ -31,8 +31,10 @@ import com.android.systemui.lifecycle.activateIn
import com.android.systemui.power.data.repository.fakePowerRepository
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.fakeOverlaysByKeys
import com.android.systemui.scene.sceneContainerConfig
import com.android.systemui.scene.shared.logger.sceneLogger
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
import com.android.systemui.testKosmos
@@ -243,4 +245,46 @@ class SceneContainerViewModelTest : SysuiTestCase() {

            assertThat(underTest.isVisible).isFalse()
        }

    @Test
    fun getActionableContentKey_noOverlays_returnsCurrentScene() =
        testScope.runTest {
            val currentScene by collectLastValue(underTest.currentScene)
            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
            assertThat(currentOverlays).isEmpty()

            val actionableContentKey =
                underTest.getActionableContentKey(
                    currentScene = checkNotNull(currentScene),
                    currentOverlays = checkNotNull(currentOverlays),
                    overlayByKey = kosmos.fakeOverlaysByKeys,
                )

            assertThat(actionableContentKey).isEqualTo(Scenes.Lockscreen)
        }

    @Test
    fun getActionableContentKey_multipleOverlays_returnsTopOverlay() =
        testScope.runTest {
            val currentScene by collectLastValue(underTest.currentScene)
            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
            fakeSceneDataSource.showOverlay(Overlays.QuickSettingsShade)
            fakeSceneDataSource.showOverlay(Overlays.NotificationsShade)
            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
            assertThat(currentOverlays)
                .containsExactly(
                    Overlays.QuickSettingsShade,
                    Overlays.NotificationsShade,
                )

            val actionableContentKey =
                underTest.getActionableContentKey(
                    currentScene = checkNotNull(currentScene),
                    currentOverlays = checkNotNull(currentOverlays),
                    overlayByKey = kosmos.fakeOverlaysByKeys,
                )

            assertThat(actionableContentKey).isEqualTo(Overlays.QuickSettingsShade)
        }
}
+19 −17
Original line number Diff line number Diff line
@@ -77,7 +77,8 @@ object SceneWindowRootViewBinder {
        alternateBouncerDependencies: AlternateBouncerDependencies,
    ) {
        val unsortedSceneByKey: Map<SceneKey, Scene> = scenes.associateBy { scene -> scene.key }
        val sortedSceneByKey: Map<SceneKey, Scene> = buildMap {
        val sortedSceneByKey: Map<SceneKey, Scene> =
            LinkedHashMap<SceneKey, Scene>(containerConfig.sceneKeys.size).apply {
                containerConfig.sceneKeys.forEach { sceneKey ->
                    val scene =
                        checkNotNull(unsortedSceneByKey[sceneKey]) {
@@ -90,7 +91,8 @@ object SceneWindowRootViewBinder {

        val unsortedOverlayByKey: Map<OverlayKey, Overlay> =
            overlays.associateBy { overlay -> overlay.key }
        val sortedOverlayByKey: Map<OverlayKey, Overlay> = buildMap {
        val sortedOverlayByKey: Map<OverlayKey, Overlay> =
            LinkedHashMap<OverlayKey, Overlay>(containerConfig.overlayKeys.size).apply {
                containerConfig.overlayKeys.forEach { overlayKey ->
                    val overlay =
                        checkNotNull(unsortedOverlayByKey[overlayKey]) {
+27 −1
Original line number Diff line number Diff line
@@ -18,7 +18,9 @@ package com.android.systemui.scene.ui.viewmodel

import android.view.MotionEvent
import androidx.compose.runtime.getValue
import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.OverlayKey
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
@@ -30,6 +32,7 @@ import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.logger.SceneLogger
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.composable.Overlay
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@@ -83,7 +86,7 @@ constructor(
    /**
     * Binds the given flow so the system remembers it.
     *
     * Note that you must call is with `null` when the UI is done or risk a memory leak.
     * Note that you must call this with `null` when the UI is done or risk a memory leak.
     */
    fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) {
        sceneInteractor.setTransitionState(transitionState)
@@ -197,6 +200,29 @@ constructor(
        }
    }

    /**
     * Returns the [ContentKey] whose user actions should be active.
     *
     * @param overlayByKey Mapping of [Overlay] by [OverlayKey], ordered by z-order such that the
     *   last overlay is rendered on top of all other overlays.
     */
    fun getActionableContentKey(
        currentScene: SceneKey,
        currentOverlays: Set<OverlayKey>,
        overlayByKey: Map<OverlayKey, Overlay>,
    ): ContentKey {
        // Overlay actions take precedence over scene actions.
        return when (currentOverlays.size) {
            // No overlays, the scene is actionable.
            0 -> currentScene
            // Small optimization for the most common case.
            1 -> currentOverlays.first()
            // Find the top-most overlay by z-index.
            else ->
                checkNotNull(overlayByKey.asSequence().findLast { it.key in currentOverlays }?.key)
        }
    }

    /** Defines interface for classes that can handle externally-reported [MotionEvent]s. */
    interface MotionEventHandler {
        /** Notifies that a [MotionEvent] has occurred. */
+3 −1
Original line number Diff line number Diff line
@@ -27,7 +27,9 @@ var Kosmos.overlayKeys by Fixture {
    )
}

val Kosmos.fakeOverlays by Fixture { overlayKeys.map { key -> FakeOverlay(key) }.toSet() }
val Kosmos.fakeOverlaysByKeys by Fixture { overlayKeys.associateWith { FakeOverlay(it) } }

val Kosmos.fakeOverlays by Fixture { fakeOverlaysByKeys.values.toSet() }

val Kosmos.overlays by Fixture { fakeOverlays }