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

Commit 2bd406ba authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

[flexiglass] Communal scene navigates to shade and bouncer.

Defines navigation user actions between the communal scene and the shade
scene as well as the bouncer scene. Also adds navigation back to Gone if
the user swipes right or up when the device is unlocked.

Still missing:
- The transition animations are missing and will be added in the next CL
- The shade scene returns to lockscreen instead of communal

Bug: 323068793
Bug: 339449476
Test: manually verified navigation. Swipe up for bouncer and swipe
right for lockscreen - when locked. Could not test when unlocked as I
couldn't figure out a way to unlock while on GH.  Tested that swipe down goes to shade when locked and
when unlocked.
Test: added unit test for new user action view-model
Flag: com.android.systemui.scene_container

Change-Id: I2966348858911eb69ef7fbb0d81bb90c11f4360b
parent 8f2be71b
Loading
Loading
Loading
Loading
+9 −16
Original line number Diff line number Diff line
@@ -21,11 +21,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.SwipeDirection
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.communal.shared.model.CommunalBackgroundType
import com.android.systemui.communal.ui.viewmodel.CommunalUserActionsViewModel
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.communal.util.CommunalColors
import com.android.systemui.dagger.SysUISingleton
@@ -33,38 +32,32 @@ import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.composable.Scene
import javax.inject.Inject
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow

/** The communal scene shows glanceable hub when the device is locked and docked. */
@SysUISingleton
class CommunalScene
@Inject
constructor(
    private val viewModel: CommunalViewModel,
    private val contentViewModel: CommunalViewModel,
    actionsViewModelFactory: CommunalUserActionsViewModel.Factory,
    private val communalColors: CommunalColors,
    private val communalContent: CommunalContent,
) : ExclusiveActivatable(), Scene {
    override val key = Scenes.Communal

    override val userActions: Flow<Map<UserAction, UserActionResult>> =
        MutableStateFlow(
                mapOf(
                    Swipe(SwipeDirection.End) to Scenes.Lockscreen,
                )
            )
            .asStateFlow()
    private val actionsViewModel: CommunalUserActionsViewModel = actionsViewModelFactory.create()

    override val userActions: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions

    override suspend fun onActivated(): Nothing {
        awaitCancellation()
        actionsViewModel.activate()
    }

    @Composable
    override fun SceneScope.Content(modifier: Modifier) {
        val backgroundType by
            viewModel.communalBackground.collectAsStateWithLifecycle(
            contentViewModel.communalBackground.collectAsStateWithLifecycle(
                initialValue = CommunalBackgroundType.ANIMATED
            )

@@ -72,7 +65,7 @@ constructor(
            backgroundType = backgroundType,
            colors = communalColors,
            content = communalContent,
            viewModel = viewModel,
            viewModel = contentViewModel,
            modifier = modifier.horizontalNestedScrollToScene(),
        )
    }
+223 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

@file:OptIn(ExperimentalCoroutinesApi::class)

package com.android.systemui.communal.ui.viewmodel

import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.coroutines.collectLastValue
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
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
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.powerInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
import com.android.systemui.shade.data.repository.fakeShadeRepository
import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableSceneContainer
class CommunalUserActionsViewModelTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope

    private lateinit var underTest: CommunalUserActionsViewModel

    @Before
    fun setUp() {
        underTest = kosmos.communalUserActionsViewModel
        underTest.activateIn(testScope)
    }

    @Test
    @DisableFlags(DualShade.FLAG_NAME)
    fun actions_singleShade() =
        testScope.runTest {
            val actions by collectLastValue(underTest.actions)

            setUpState(
                isShadeTouchable = true,
                isDeviceUnlocked = false,
                shadeMode = ShadeMode.Single,
            )
            assertThat(actions).isNotEmpty()
            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
            assertThat(actions?.get(Swipe.Down)).isEqualTo(UserActionResult(Scenes.Shade))

            setUpState(
                isShadeTouchable = false,
                isDeviceUnlocked = false,
                shadeMode = ShadeMode.Single,
            )
            assertThat(actions).isEmpty()

            setUpState(
                isShadeTouchable = true,
                isDeviceUnlocked = true,
                shadeMode = ShadeMode.Single,
            )
            assertThat(actions).isNotEmpty()
            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
            assertThat(actions?.get(Swipe.Down)).isEqualTo(UserActionResult(Scenes.Shade))
        }

    @Test
    @DisableFlags(DualShade.FLAG_NAME)
    fun actions_splitShade() =
        testScope.runTest {
            val actions by collectLastValue(underTest.actions)

            setUpState(
                isShadeTouchable = true,
                isDeviceUnlocked = false,
                shadeMode = ShadeMode.Split,
            )
            assertThat(actions).isNotEmpty()
            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
            assertThat(actions?.get(Swipe.Down))
                .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade))

            setUpState(
                isShadeTouchable = false,
                isDeviceUnlocked = false,
                shadeMode = ShadeMode.Split,
            )
            assertThat(actions).isEmpty()

            setUpState(
                isShadeTouchable = true,
                isDeviceUnlocked = true,
                shadeMode = ShadeMode.Split,
            )
            assertThat(actions).isNotEmpty()
            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
            assertThat(actions?.get(Swipe.Down))
                .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade))
        }

    @Test
    @EnableFlags(DualShade.FLAG_NAME)
    fun actions_dualShade() =
        testScope.runTest {
            val actions by collectLastValue(underTest.actions)

            setUpState(
                isShadeTouchable = true,
                isDeviceUnlocked = false,
                shadeMode = ShadeMode.Dual,
            )
            assertThat(actions).isNotEmpty()
            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
            assertThat(actions?.get(Swipe.Down))
                .isEqualTo(UserActionResult(Overlays.NotificationsShade))

            setUpState(
                isShadeTouchable = false,
                isDeviceUnlocked = false,
                shadeMode = ShadeMode.Dual,
            )
            assertThat(actions).isEmpty()

            setUpState(isShadeTouchable = true, isDeviceUnlocked = true, shadeMode = ShadeMode.Dual)
            assertThat(actions).isNotEmpty()
            assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
            assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
            assertThat(actions?.get(Swipe.Down))
                .isEqualTo(UserActionResult(Overlays.NotificationsShade))
        }

    private fun TestScope.setUpState(
        isShadeTouchable: Boolean,
        isDeviceUnlocked: Boolean,
        shadeMode: ShadeMode,
    ) {
        if (isShadeTouchable) {
            kosmos.powerInteractor.setAwakeForTest()
        } else {
            kosmos.powerInteractor.setAsleepForTest()
        }

        if (isDeviceUnlocked) {
            unlockDevice()
        } else {
            lockDevice()
        }

        if (shadeMode == ShadeMode.Dual) {
            assertThat(DualShade.isEnabled).isTrue()
        } else {
            assertThat(DualShade.isEnabled).isFalse()
            kosmos.fakeShadeRepository.setShadeLayoutWide(shadeMode == ShadeMode.Split)
        }
        runCurrent()
    }

    private fun TestScope.lockDevice() {
        val deviceUnlockStatus by
            collectLastValue(kosmos.deviceUnlockedInteractor.deviceUnlockStatus)

        kosmos.fakeAuthenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
        assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
        kosmos.sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
        runCurrent()
    }

    private fun TestScope.unlockDevice() {
        val deviceUnlockStatus by
            collectLastValue(kosmos.deviceUnlockedInteractor.deviceUnlockStatus)

        kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
            SuccessFingerprintAuthenticationStatus(0, true)
        )
        assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
        kosmos.sceneInteractor.changeScene(Scenes.Gone, "reason")
        runCurrent()
    }
}
+83 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.communal.ui.viewmodel

import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.shade.ui.viewmodel.dualShadeActions
import com.android.systemui.shade.ui.viewmodel.singleShadeActions
import com.android.systemui.shade.ui.viewmodel.splitShadeActions
import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map

/** Provides scene container user actions and results. */
class CommunalUserActionsViewModel
@AssistedInject
constructor(
    private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
    private val shadeInteractor: ShadeInteractor,
) : UserActionsViewModel() {

    override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
        shadeInteractor.isShadeTouchable
            .flatMapLatestConflated { isShadeTouchable ->
                if (!isShadeTouchable) {
                    flowOf(emptyMap())
                } else {
                    combine(
                        deviceUnlockedInteractor.deviceUnlockStatus.map { it.isUnlocked },
                        shadeInteractor.shadeMode,
                    ) { isDeviceUnlocked, shadeMode ->
                        buildList {
                                val bouncerOrGone =
                                    if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer
                                add(Swipe.Up to bouncerOrGone)

                                // "Home" is either Lockscreen, or Gone - if the device is entered.
                                add(Swipe.End to SceneFamilies.Home)

                                addAll(
                                    when (shadeMode) {
                                        ShadeMode.Single -> singleShadeActions()
                                        ShadeMode.Split -> splitShadeActions()
                                        ShadeMode.Dual -> dualShadeActions()
                                    }
                                )
                            }
                            .associate { it }
                    }
                }
            }
            .collect { setActions(it) }
    }

    @AssistedFactory
    interface Factory {
        fun create(): CommunalUserActionsViewModel
    }
}
+3 −44
Original line number Diff line number Diff line
@@ -18,20 +18,18 @@

package com.android.systemui.keyguard.ui.viewmodel

import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.SwipeDirection
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.shade.ui.viewmodel.dualShadeActions
import com.android.systemui.shade.ui.viewmodel.singleShadeActions
import com.android.systemui.shade.ui.viewmodel.splitShadeActions
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -81,45 +79,6 @@ constructor(
            .collect { setActions(it) }
    }

    private fun singleShadeActions(): Array<Pair<UserAction, UserActionResult>> {
        return arrayOf(
            // Swiping down, not from the edge, always goes to shade.
            Swipe.Down to Scenes.Shade,
            swipeDown(pointerCount = 2) to Scenes.Shade,
            // Swiping down from the top edge goes to QS.
            swipeDownFromTop(pointerCount = 1) to Scenes.QuickSettings,
            swipeDownFromTop(pointerCount = 2) to Scenes.QuickSettings,
        )
    }

    private fun splitShadeActions(): Array<Pair<UserAction, UserActionResult>> {
        val splitShadeSceneKey = UserActionResult(Scenes.Shade, ToSplitShade)
        return arrayOf(
            // Swiping down, not from the edge, always goes to shade.
            Swipe.Down to splitShadeSceneKey,
            swipeDown(pointerCount = 2) to splitShadeSceneKey,
            // Swiping down from the top edge goes to QS.
            swipeDownFromTop(pointerCount = 1) to splitShadeSceneKey,
            swipeDownFromTop(pointerCount = 2) to splitShadeSceneKey,
        )
    }

    private fun dualShadeActions(): Array<Pair<UserAction, UserActionResult>> {
        return arrayOf(
            Swipe.Down to Overlays.NotificationsShade,
            Swipe(direction = SwipeDirection.Down, fromSource = SceneContainerEdge.TopRight) to
                Overlays.QuickSettingsShade,
        )
    }

    private fun swipeDownFromTop(pointerCount: Int): Swipe {
        return Swipe(SwipeDirection.Down, fromSource = Edge.Top, pointerCount = pointerCount)
    }

    private fun swipeDown(pointerCount: Int): Swipe {
        return Swipe(SwipeDirection.Down, pointerCount = pointerCount)
    }

    @AssistedFactory
    interface Factory {
        fun create(): LockscreenUserActionsViewModel
+14 −37
Original line number Diff line number Diff line
@@ -16,16 +16,13 @@

package com.android.systemui.scene.ui.viewmodel

import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.SwipeDirection
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.shade.ui.viewmodel.dualShadeActions
import com.android.systemui.shade.ui.viewmodel.singleShadeActions
import com.android.systemui.shade.ui.viewmodel.splitShadeActions
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject

@@ -36,39 +33,19 @@ constructor(private val shadeInteractor: ShadeInteractor) : UserActionsViewModel
    override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
        shadeInteractor.shadeMode.collect { shadeMode ->
            setActions(
                buildList {
                        addAll(
                            when (shadeMode) {
                    ShadeMode.Single -> singleShadeActions()
                                ShadeMode.Single ->
                                    singleShadeActions(requireTwoPointersForTopEdgeForQs = true)
                                ShadeMode.Split -> splitShadeActions()
                                ShadeMode.Dual -> dualShadeActions()
                            }
                        )
                    }
    }

    private fun singleShadeActions(): Map<UserAction, UserActionResult> {
        return mapOf(
            Swipe.Down to Scenes.Shade,
            swipeDownFromTopWithTwoFingers() to Scenes.QuickSettings,
        )
    }

    private fun splitShadeActions(): Map<UserAction, UserActionResult> {
        return mapOf(
            Swipe.Down to UserActionResult(Scenes.Shade, ToSplitShade),
            swipeDownFromTopWithTwoFingers() to UserActionResult(Scenes.Shade, ToSplitShade),
                    .associate { it }
            )
        }

    private fun dualShadeActions(): Map<UserAction, UserActionResult> {
        return mapOf(
            Swipe.Down to Overlays.NotificationsShade,
            Swipe(direction = SwipeDirection.Down, fromSource = SceneContainerEdge.TopRight) to
                Overlays.QuickSettingsShade,
        )
    }

    private fun swipeDownFromTopWithTwoFingers(): UserAction {
        return Swipe(direction = SwipeDirection.Down, pointerCount = 2, fromSource = Edge.Top)
    }

    @AssistedFactory
Loading