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

Commit 25b8f59d authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge changes Ic8538604,I29663488 into main

* changes:
  [flexiglass] Changes Lockscreen's Swipe.Left to Swipe.Start
  [flexiglass] Communal scene navigates to shade and bouncer.
parents fc644a33 5c49ad48
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()
    }
}
+15 −15
Original line number Diff line number Diff line
@@ -147,7 +147,7 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() {
            }
        }

        private fun expectedLeftDestination(
        private fun expectedStartDestination(
            isCommunalAvailable: Boolean,
            isShadeTouchable: Boolean,
        ): SceneKey? {
@@ -246,17 +246,17 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() {
                    )
                )

            val leftScene by
            val startScene by
                collectLastValue(
                    (userActions?.get(Swipe.Left) as? UserActionResult.ChangeScene)?.toScene?.let {
                        scene ->
                        kosmos.sceneInteractor.resolveSceneFamily(scene)
                    } ?: flowOf(null)
                    (userActions?.get(Swipe.Start) as? UserActionResult.ChangeScene)
                        ?.toScene
                        ?.let { scene -> kosmos.sceneInteractor.resolveSceneFamily(scene) }
                        ?: flowOf(null)
                )

            assertThat(leftScene)
            assertThat(startScene)
                .isEqualTo(
                    expectedLeftDestination(
                    expectedStartDestination(
                        isCommunalAvailable = isCommunalAvailable,
                        isShadeTouchable = isShadeTouchable,
                    )
@@ -341,17 +341,17 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() {
                    )
                )

            val leftScene by
            val startScene by
                collectLastValue(
                    (userActions?.get(Swipe.Left) as? UserActionResult.ChangeScene)?.toScene?.let {
                        scene ->
                        kosmos.sceneInteractor.resolveSceneFamily(scene)
                    } ?: flowOf(null)
                    (userActions?.get(Swipe.Start) as? UserActionResult.ChangeScene)
                        ?.toScene
                        ?.let { scene -> kosmos.sceneInteractor.resolveSceneFamily(scene) }
                        ?: flowOf(null)
                )

            assertThat(leftScene)
            assertThat(startScene)
                .isEqualTo(
                    expectedLeftDestination(
                    expectedStartDestination(
                        isCommunalAvailable = isCommunalAvailable,
                        isShadeTouchable = isShadeTouchable,
                    )
+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
    }
}
+4 −45
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
@@ -62,7 +60,7 @@ constructor(
                ) { isDeviceUnlocked, isCommunalAvailable, shadeMode ->
                    buildList {
                            if (isCommunalAvailable) {
                                add(Swipe.Left to Scenes.Communal)
                                add(Swipe.Start to Scenes.Communal)
                            }

                            add(Swipe.Up to if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer)
@@ -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
Loading