diff options
8 files changed, 454 insertions, 113 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt index e41a7df39c21..a88ad946d95b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalScene.kt @@ -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(), ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt new file mode 100644 index 000000000000..58b59ffd8894 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt @@ -0,0 +1,223 @@ +/* + * 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() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt index a330cf01624f..fb1bf281715d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt @@ -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, ) diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt new file mode 100644 index 000000000000..e35fdfe9087c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt @@ -0,0 +1,83 @@ +/* + * 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 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt index 3b266f945aab..6f29004d4f3f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt @@ -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 diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModel.kt index 5ff507a45d2e..fc172e8ca1d8 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneUserActionsViewModel.kt @@ -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,41 +33,21 @@ constructor(private val shadeInteractor: ShadeInteractor) : UserActionsViewModel override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { shadeInteractor.shadeMode.collect { shadeMode -> setActions( - when (shadeMode) { - ShadeMode.Single -> singleShadeActions() - ShadeMode.Split -> splitShadeActions() - ShadeMode.Dual -> dualShadeActions() - } + buildList { + addAll( + when (shadeMode) { + ShadeMode.Single -> + singleShadeActions(requireTwoPointersForTopEdgeForQs = true) + ShadeMode.Split -> splitShadeActions() + ShadeMode.Dual -> dualShadeActions() + } + ) + } + .associate { it } ) } } - 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), - ) - } - - 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 interface Factory { fun create(): GoneUserActionsViewModel diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt new file mode 100644 index 000000000000..65b6231705d4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActions.kt @@ -0,0 +1,77 @@ +/* + * 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.shade.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.scene.ui.viewmodel.SceneContainerEdge + +/** Returns collection of [UserAction] to [UserActionResult] pairs for opening the single shade. */ +fun singleShadeActions( + requireTwoPointersForTopEdgeForQs: Boolean = false +): 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. + swipeDownFromTop(pointerCount = 1) to + if (requireTwoPointersForTopEdgeForQs) { + Scenes.Shade + } else { + Scenes.QuickSettings + }, + swipeDownFromTop(pointerCount = 2) to Scenes.QuickSettings, + ) +} + +/** Returns collection of [UserAction] to [UserActionResult] pairs for opening the split shade. */ +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, + ) +} + +/** Returns collection of [UserAction] to [UserActionResult] pairs for opening the dual shade. */ +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) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelKosmos.kt new file mode 100644 index 000000000000..1c84133d3821 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelKosmos.kt @@ -0,0 +1,29 @@ +/* + * 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.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.shade.domain.interactor.shadeInteractor + +val Kosmos.communalUserActionsViewModel by Fixture { + CommunalUserActionsViewModel( + deviceUnlockedInteractor = deviceUnlockedInteractor, + shadeInteractor = shadeInteractor, + ) +} |