summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Treehugger Robot <android-test-infra-autosubmit@system.gserviceaccount.com> 2024-09-09 10:30:11 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2024-09-09 10:30:11 +0000
commit5620e115b93654c2891ed9e9f45b6c68333edf43 (patch)
tree0e33205c398c3fc70f375b596f35752693b20d63
parentefde44764cd00b995a42242e6e50eb7002b898b5 (diff)
parenta17aac0b5508e8bb9f22317c48fc2ef02ec733ad (diff)
Merge "Revert^2 "[bc25] Add Dual Shade invocation zone detection."" into main
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt54
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt274
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt46
-rw-r--r--packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt116
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt11
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt29
15 files changed, 608 insertions, 15 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index 3cb0d8af1ba4..df101c558dff 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -128,7 +128,11 @@ fun SceneContainer(
}
},
) {
- SceneTransitionLayout(state = state, modifier = modifier.fillMaxSize()) {
+ SceneTransitionLayout(
+ state = state,
+ modifier = modifier.fillMaxSize(),
+ swipeSourceDetector = viewModel.edgeDetector,
+ ) {
sceneByKey.forEach { (sceneKey, scene) ->
scene(
key = sceneKey,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index 4b132c4276ea..a0bb01797f2c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -18,9 +18,12 @@
package com.android.systemui.scene.ui.viewmodel
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.view.MotionEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.DefaultEdgeDetector
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.domain.interactor.falsingInteractor
import com.android.systemui.classifier.fakeFalsingManager
@@ -37,6 +40,10 @@ 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.shade.data.repository.fakeShadeRepository
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.shared.flag.DualShade
+import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
@@ -60,6 +67,7 @@ class SceneContainerViewModelTest : SysuiTestCase() {
private val testScope by lazy { kosmos.testScope }
private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource }
+ private val fakeShadeRepository by lazy { kosmos.fakeShadeRepository }
private val sceneContainerConfig by lazy { kosmos.sceneContainerConfig }
private val falsingManager by lazy { kosmos.fakeFalsingManager }
@@ -75,6 +83,8 @@ class SceneContainerViewModelTest : SysuiTestCase() {
sceneInteractor = sceneInteractor,
falsingInteractor = kosmos.falsingInteractor,
powerInteractor = kosmos.powerInteractor,
+ shadeInteractor = kosmos.shadeInteractor,
+ splitEdgeDetector = kosmos.splitEdgeDetector,
logger = kosmos.sceneLogger,
motionEventHandlerReceiver = { motionEventHandler ->
this@SceneContainerViewModelTest.motionEventHandler = motionEventHandler
@@ -287,4 +297,48 @@ class SceneContainerViewModelTest : SysuiTestCase() {
assertThat(actionableContentKey).isEqualTo(Overlays.QuickSettingsShade)
}
+
+ @Test
+ @DisableFlags(DualShade.FLAG_NAME)
+ fun edgeDetector_singleShade_usesDefaultEdgeDetector() =
+ testScope.runTest {
+ val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode)
+ fakeShadeRepository.setShadeLayoutWide(false)
+ assertThat(shadeMode).isEqualTo(ShadeMode.Single)
+
+ assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector)
+ }
+
+ @Test
+ @DisableFlags(DualShade.FLAG_NAME)
+ fun edgeDetector_splitShade_usesDefaultEdgeDetector() =
+ testScope.runTest {
+ val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode)
+ fakeShadeRepository.setShadeLayoutWide(true)
+ assertThat(shadeMode).isEqualTo(ShadeMode.Split)
+
+ assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector)
+ }
+
+ @Test
+ @EnableFlags(DualShade.FLAG_NAME)
+ fun edgeDetector_dualShade_narrowScreen_usesSplitEdgeDetector() =
+ testScope.runTest {
+ val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode)
+ fakeShadeRepository.setShadeLayoutWide(false)
+
+ assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
+ assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector)
+ }
+
+ @Test
+ @EnableFlags(DualShade.FLAG_NAME)
+ fun edgeDetector_dualShade_wideScreen_usesSplitEdgeDetector() =
+ testScope.runTest {
+ val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode)
+ fakeShadeRepository.setShadeLayoutWide(true)
+
+ assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
+ assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt
new file mode 100644
index 000000000000..3d76d280b2cc
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt
@@ -0,0 +1,274 @@
+/*
+ * 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.scene.ui.viewmodel
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.End
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Bottom
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Left
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Right
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.TopLeft
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.TopRight
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Start
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.TopEnd
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.TopStart
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SplitEdgeDetectorTest : SysuiTestCase() {
+
+ private val edgeSize = 40
+ private val screenWidth = 800
+ private val screenHeight = 600
+
+ private var edgeSplitFraction = 0.7f
+
+ private val underTest =
+ SplitEdgeDetector(
+ topEdgeSplitFraction = { edgeSplitFraction },
+ edgeSize = edgeSize.dp,
+ )
+
+ @Test
+ fun source_noEdge_detectsNothing() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = screenWidth / 2,
+ y = screenHeight / 2,
+ )
+ assertThat(detectedEdge).isNull()
+ }
+
+ @Test
+ fun source_swipeVerticallyOnTopLeft_detectsTopLeft() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = 1,
+ y = edgeSize - 1,
+ )
+ assertThat(detectedEdge).isEqualTo(TopLeft)
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnTopLeft_detectsLeft() {
+ val detectedEdge =
+ swipeHorizontallyFrom(
+ x = 1,
+ y = edgeSize - 1,
+ )
+ assertThat(detectedEdge).isEqualTo(Left)
+ }
+
+ @Test
+ fun source_swipeVerticallyOnTopRight_detectsTopRight() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = screenWidth - 1,
+ y = edgeSize - 1,
+ )
+ assertThat(detectedEdge).isEqualTo(TopRight)
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnTopRight_detectsRight() {
+ val detectedEdge =
+ swipeHorizontallyFrom(
+ x = screenWidth - 1,
+ y = edgeSize - 1,
+ )
+ assertThat(detectedEdge).isEqualTo(Right)
+ }
+
+ @Test
+ fun source_swipeVerticallyToLeftOfSplit_detectsTopLeft() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = (screenWidth * edgeSplitFraction).toInt() - 1,
+ y = edgeSize - 1,
+ )
+ assertThat(detectedEdge).isEqualTo(TopLeft)
+ }
+
+ @Test
+ fun source_swipeVerticallyToRightOfSplit_detectsTopRight() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = (screenWidth * edgeSplitFraction).toInt() + 1,
+ y = edgeSize - 1,
+ )
+ assertThat(detectedEdge).isEqualTo(TopRight)
+ }
+
+ @Test
+ fun source_edgeSplitFractionUpdatesDynamically() {
+ val middleX = (screenWidth * 0.5f).toInt()
+ val topY = 0
+
+ // Split closer to the right; middle of screen is considered "left".
+ edgeSplitFraction = 0.6f
+ assertThat(swipeVerticallyFrom(x = middleX, y = topY)).isEqualTo(TopLeft)
+
+ // Split closer to the left; middle of screen is considered "right".
+ edgeSplitFraction = 0.4f
+ assertThat(swipeVerticallyFrom(x = middleX, y = topY)).isEqualTo(TopRight)
+
+ // Illegal fraction.
+ edgeSplitFraction = 1.2f
+ assertFailsWith<IllegalArgumentException> { swipeVerticallyFrom(x = middleX, y = topY) }
+
+ // Illegal fraction.
+ edgeSplitFraction = -0.3f
+ assertFailsWith<IllegalArgumentException> { swipeVerticallyFrom(x = middleX, y = topY) }
+ }
+
+ @Test
+ fun source_swipeVerticallyOnBottom_detectsBottom() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = screenWidth / 3,
+ y = screenHeight - (edgeSize / 2),
+ )
+ assertThat(detectedEdge).isEqualTo(Bottom)
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnBottom_detectsNothing() {
+ val detectedEdge =
+ swipeHorizontallyFrom(
+ x = screenWidth / 3,
+ y = screenHeight - (edgeSize - 1),
+ )
+ assertThat(detectedEdge).isNull()
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnLeft_detectsLeft() {
+ val detectedEdge =
+ swipeHorizontallyFrom(
+ x = edgeSize - 1,
+ y = screenHeight / 2,
+ )
+ assertThat(detectedEdge).isEqualTo(Left)
+ }
+
+ @Test
+ fun source_swipeVerticallyOnLeft_detectsNothing() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = edgeSize - 1,
+ y = screenHeight / 2,
+ )
+ assertThat(detectedEdge).isNull()
+ }
+
+ @Test
+ fun source_swipeHorizontallyOnRight_detectsRight() {
+ val detectedEdge =
+ swipeHorizontallyFrom(
+ x = screenWidth - edgeSize + 1,
+ y = screenHeight / 2,
+ )
+ assertThat(detectedEdge).isEqualTo(Right)
+ }
+
+ @Test
+ fun source_swipeVerticallyOnRight_detectsNothing() {
+ val detectedEdge =
+ swipeVerticallyFrom(
+ x = screenWidth - edgeSize + 1,
+ y = screenHeight / 2,
+ )
+ assertThat(detectedEdge).isNull()
+ }
+
+ @Test
+ fun resolve_startInLtr_resolvesLeft() {
+ val resolvedEdge = Start.resolve(LayoutDirection.Ltr)
+ assertThat(resolvedEdge).isEqualTo(Left)
+ }
+
+ @Test
+ fun resolve_startInRtl_resolvesRight() {
+ val resolvedEdge = Start.resolve(LayoutDirection.Rtl)
+ assertThat(resolvedEdge).isEqualTo(Right)
+ }
+
+ @Test
+ fun resolve_endInLtr_resolvesRight() {
+ val resolvedEdge = End.resolve(LayoutDirection.Ltr)
+ assertThat(resolvedEdge).isEqualTo(Right)
+ }
+
+ @Test
+ fun resolve_endInRtl_resolvesLeft() {
+ val resolvedEdge = End.resolve(LayoutDirection.Rtl)
+ assertThat(resolvedEdge).isEqualTo(Left)
+ }
+
+ @Test
+ fun resolve_topStartInLtr_resolvesTopLeft() {
+ val resolvedEdge = TopStart.resolve(LayoutDirection.Ltr)
+ assertThat(resolvedEdge).isEqualTo(TopLeft)
+ }
+
+ @Test
+ fun resolve_topStartInRtl_resolvesTopRight() {
+ val resolvedEdge = TopStart.resolve(LayoutDirection.Rtl)
+ assertThat(resolvedEdge).isEqualTo(TopRight)
+ }
+
+ @Test
+ fun resolve_topEndInLtr_resolvesTopRight() {
+ val resolvedEdge = TopEnd.resolve(LayoutDirection.Ltr)
+ assertThat(resolvedEdge).isEqualTo(TopRight)
+ }
+
+ @Test
+ fun resolve_topEndInRtl_resolvesTopLeft() {
+ val resolvedEdge = TopEnd.resolve(LayoutDirection.Rtl)
+ assertThat(resolvedEdge).isEqualTo(TopLeft)
+ }
+
+ private fun swipeVerticallyFrom(x: Int, y: Int): SceneContainerEdge.Resolved? {
+ return swipeFrom(x, y, Orientation.Vertical)
+ }
+
+ private fun swipeHorizontallyFrom(x: Int, y: Int): SceneContainerEdge.Resolved? {
+ return swipeFrom(x, y, Orientation.Horizontal)
+ }
+
+ private fun swipeFrom(x: Int, y: Int, orientation: Orientation): SceneContainerEdge.Resolved? {
+ return underTest.source(
+ layoutSize = IntSize(width = screenWidth, height = screenHeight),
+ position = IntOffset(x, y),
+ density = Density(1f),
+ orientation = orientation,
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
index 3283ea154b3f..9464c75eeb71 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
@@ -24,7 +24,6 @@ import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.parameterizeSceneContainerFlag
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
@@ -39,6 +38,7 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.power.data.repository.fakePowerRepository
import com.android.systemui.power.shared.model.WakeSleepReason
import com.android.systemui.power.shared.model.WakefulnessState
+import com.android.systemui.shade.data.repository.fakeShadeRepository
import com.android.systemui.shade.data.repository.shadeRepository
import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.shade.shared.flag.DualShade
@@ -66,18 +66,18 @@ import platform.test.runner.parameterized.Parameters
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() {
- val kosmos = testKosmos()
- val testScope = kosmos.testScope
- val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
- val deviceProvisioningRepository by lazy { kosmos.fakeDeviceProvisioningRepository }
- val disableFlagsRepository by lazy { kosmos.fakeDisableFlagsRepository }
- val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
- val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
- val powerRepository by lazy { kosmos.fakePowerRepository }
- val shadeTestUtil by lazy { kosmos.shadeTestUtil }
- val userRepository by lazy { kosmos.fakeUserRepository }
- val userSetupRepository by lazy { kosmos.fakeUserSetupRepository }
- val dozeParameters by lazy { kosmos.dozeParameters }
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val deviceProvisioningRepository by lazy { kosmos.fakeDeviceProvisioningRepository }
+ private val disableFlagsRepository by lazy { kosmos.fakeDisableFlagsRepository }
+ private val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
+ private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
+ private val powerRepository by lazy { kosmos.fakePowerRepository }
+ private val shadeRepository by lazy { kosmos.fakeShadeRepository }
+ private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
+ private val userRepository by lazy { kosmos.fakeUserRepository }
+ private val userSetupRepository by lazy { kosmos.fakeUserSetupRepository }
+ private val dozeParameters by lazy { kosmos.dozeParameters }
lateinit var underTest: ShadeInteractorImpl
@@ -497,4 +497,24 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() {
assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
}
+
+ @Test
+ fun getTopEdgeSplitFraction_narrowScreen_splitInHalf() =
+ testScope.runTest {
+ // Ensure isShadeLayoutWide is collected.
+ val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide)
+ shadeRepository.setShadeLayoutWide(false)
+
+ assertThat(underTest.getTopEdgeSplitFraction()).isEqualTo(0.5f)
+ }
+
+ @Test
+ fun getTopEdgeSplitFraction_wideScreen_leftSideLarger() =
+ testScope.runTest {
+ // Ensure isShadeLayoutWide is collected.
+ val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide)
+ shadeRepository.setShadeLayoutWide(true)
+
+ assertThat(underTest.getTopEdgeSplitFraction()).isGreaterThan(0.5f)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt
index a5c07bc2fdbf..11854d9317c9 100644
--- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt
@@ -18,9 +18,13 @@ package com.android.systemui.notifications.ui.viewmodel
import com.android.compose.animation.scene.Back
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.compose.animation.scene.UserActionResult.ReplaceByOverlay
+import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -34,8 +38,10 @@ class NotificationsShadeUserActionsViewModel @AssistedInject constructor() :
override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
setActions(
mapOf(
- Swipe.Up to SceneFamilies.Home,
Back to SceneFamilies.Home,
+ Swipe.Up to SceneFamilies.Home,
+ Swipe(direction = SwipeDirection.Down, fromSource = SceneContainerEdge.TopRight) to
+ ReplaceByOverlay(Overlays.QuickSettingsShade),
)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt
index d3dc302d44ca..bd1872d455d0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt
@@ -18,9 +18,13 @@ package com.android.systemui.qs.ui.viewmodel
import com.android.compose.animation.scene.Back
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.compose.animation.scene.UserActionResult.ReplaceByOverlay
+import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -43,6 +47,13 @@ constructor(
.map { editing ->
buildMap {
put(Swipe.Up, UserActionResult(SceneFamilies.Home))
+ put(
+ Swipe(
+ direction = SwipeDirection.Down,
+ fromSource = SceneContainerEdge.TopLeft
+ ),
+ ReplaceByOverlay(Overlays.NotificationsShade)
+ )
if (!editing) {
put(Back, UserActionResult(SceneFamilies.Home))
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
index 00944b8d0849..834db98263f5 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
@@ -16,6 +16,7 @@
package com.android.systemui.scene
+import androidx.compose.ui.unit.dp
import com.android.systemui.CoreStartable
import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule
import com.android.systemui.scene.domain.SceneDomainModule
@@ -30,6 +31,8 @@ import com.android.systemui.scene.domain.startable.StatusBarStartable
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.shared.flag.DualShade
import dagger.Binds
import dagger.Module
@@ -119,5 +122,15 @@ interface KeyguardlessSceneContainerFrameworkModule {
.mapValues { checkNotNull(it.value) }
)
}
+
+ @Provides
+ fun splitEdgeDetector(shadeInteractor: ShadeInteractor): SplitEdgeDetector {
+ return SplitEdgeDetector(
+ topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction,
+ // TODO(b/338577208): This should be 60dp at the top in the dual-shade UI. Better to
+ // replace this constant with dynamic window insets.
+ edgeSize = 40.dp
+ )
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
index 4061ad851f57..a4c7d00d0e80 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
@@ -16,6 +16,7 @@
package com.android.systemui.scene
+import androidx.compose.ui.unit.dp
import com.android.systemui.CoreStartable
import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule
import com.android.systemui.scene.domain.SceneDomainModule
@@ -30,6 +31,8 @@ import com.android.systemui.scene.domain.startable.StatusBarStartable
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.shared.flag.DualShade
import dagger.Binds
import dagger.Module
@@ -129,5 +132,15 @@ interface SceneContainerFrameworkModule {
.mapValues { checkNotNull(it.value) }
)
}
+
+ @Provides
+ fun splitEdgeDetector(shadeInteractor: ShadeInteractor): SplitEdgeDetector {
+ return SplitEdgeDetector(
+ topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction,
+ // TODO(b/338577208): This should be 60dp at the top in the dual-shade UI. Better to
+ // replace this constant with dynamic window insets.
+ edgeSize = 40.dp
+ )
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index 4c6341b672ad..54823945a827 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -19,9 +19,11 @@ 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.DefaultEdgeDetector
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.SwipeSourceDetector
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.classifier.Classifier
@@ -33,12 +35,15 @@ 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.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
/** Models UI state for the scene container. */
class SceneContainerViewModel
@@ -47,6 +52,8 @@ constructor(
private val sceneInteractor: SceneInteractor,
private val falsingInteractor: FalsingInteractor,
private val powerInteractor: PowerInteractor,
+ private val shadeInteractor: ShadeInteractor,
+ private val splitEdgeDetector: SplitEdgeDetector,
private val logger: SceneLogger,
@Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
) : ExclusiveActivatable() {
@@ -59,6 +66,20 @@ constructor(
/** Whether the container is visible. */
val isVisible: Boolean by hydrator.hydratedStateOf("isVisible", sceneInteractor.isVisible)
+ /**
+ * The [SwipeSourceDetector] to use for defining which edges of the screen can be defined in the
+ * [UserAction]s for this container.
+ */
+ val edgeDetector: SwipeSourceDetector by
+ hydrator.hydratedStateOf(
+ traceName = "edgeDetector",
+ initialValue = DefaultEdgeDetector,
+ source =
+ shadeInteractor.shadeMode.map {
+ if (it is ShadeMode.Dual) splitEdgeDetector else DefaultEdgeDetector
+ }
+ )
+
override suspend fun onActivated(): Nothing {
try {
// Sends a MotionEventHandler to the owner of the view-model so they can report
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt
new file mode 100644
index 000000000000..f88bcb57a27d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.scene.ui.viewmodel
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.FixedSizeEdgeDetector
+import com.android.compose.animation.scene.SwipeSource
+import com.android.compose.animation.scene.SwipeSourceDetector
+
+/**
+ * The edge of a [SceneContainer]. It differs from a standard [Edge] by splitting the top edge into
+ * top-left and top-right.
+ */
+enum class SceneContainerEdge(private val resolveEdge: (LayoutDirection) -> Resolved) :
+ SwipeSource {
+ TopLeft(resolveEdge = { Resolved.TopLeft }),
+ TopRight(resolveEdge = { Resolved.TopRight }),
+ TopStart(
+ resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.TopLeft else Resolved.TopRight }
+ ),
+ TopEnd(
+ resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.TopRight else Resolved.TopLeft }
+ ),
+ Bottom(resolveEdge = { Resolved.Bottom }),
+ Left(resolveEdge = { Resolved.Left }),
+ Right(resolveEdge = { Resolved.Right }),
+ Start(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Left else Resolved.Right }),
+ End(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Right else Resolved.Left });
+
+ override fun resolve(layoutDirection: LayoutDirection): Resolved {
+ return resolveEdge(layoutDirection)
+ }
+
+ enum class Resolved : SwipeSource.Resolved {
+ TopLeft,
+ TopRight,
+ Bottom,
+ Left,
+ Right,
+ }
+}
+
+/**
+ * A [SwipeSourceDetector] that detects edges similarly to [FixedSizeEdgeDetector], except that the
+ * top edge is split in two: top-left and top-right. The split point between the two is dynamic and
+ * may change during runtime.
+ *
+ * Callers who need to detect the start and end edges based on the layout direction (LTR vs RTL)
+ * should subscribe to [SceneContainerEdge.TopStart] and [SceneContainerEdge.TopEnd] instead. These
+ * will be resolved at runtime to [SceneContainerEdge.Resolved.TopLeft] and
+ * [SceneContainerEdge.Resolved.TopRight] appropriately. Similarly, [SceneContainerEdge.Start] and
+ * [SceneContainerEdge.End] will be resolved appropriately to [SceneContainerEdge.Resolved.Left] and
+ * [SceneContainerEdge.Resolved.Right].
+ *
+ * @param topEdgeSplitFraction A function which returns the fraction between [0..1] (i.e.,
+ * percentage) of screen width to consider the split point between "top-left" and "top-right"
+ * edges. It is called on each source detection event.
+ * @param edgeSize The fixed size of each edge.
+ */
+class SplitEdgeDetector(
+ val topEdgeSplitFraction: () -> Float,
+ val edgeSize: Dp,
+) : SwipeSourceDetector {
+
+ private val fixedEdgeDetector = FixedSizeEdgeDetector(edgeSize)
+
+ override fun source(
+ layoutSize: IntSize,
+ position: IntOffset,
+ density: Density,
+ orientation: Orientation,
+ ): SceneContainerEdge.Resolved? {
+ val fixedEdge =
+ fixedEdgeDetector.source(
+ layoutSize,
+ position,
+ density,
+ orientation,
+ )
+ return when (fixedEdge) {
+ Edge.Resolved.Top -> {
+ val topEdgeSplitFraction = topEdgeSplitFraction()
+ require(topEdgeSplitFraction in 0f..1f) {
+ "topEdgeSplitFraction must return a value between 0.0 and 1.0"
+ }
+ val isLeftSide = position.x < layoutSize.width * topEdgeSplitFraction
+ if (isLeftSide) SceneContainerEdge.Resolved.TopLeft
+ else SceneContainerEdge.Resolved.TopRight
+ }
+ Edge.Resolved.Left -> SceneContainerEdge.Resolved.Left
+ Edge.Resolved.Bottom -> SceneContainerEdge.Resolved.Bottom
+ Edge.Resolved.Right -> SceneContainerEdge.Resolved.Right
+ null -> null
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
index 73e86a2be4aa..3cd91be469c1 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
@@ -16,6 +16,7 @@
package com.android.systemui.shade.domain.interactor
+import androidx.annotation.FloatRange
import com.android.systemui.shade.shared.model.ShadeMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@@ -69,6 +70,20 @@ interface ShadeInteractor : BaseShadeInteractor {
* wide as the entire screen.
*/
val isShadeLayoutWide: StateFlow<Boolean>
+
+ /**
+ * The fraction between [0..1] (i.e., percentage) of screen width to consider the threshold
+ * between "top-left" and "top-right" for the purposes of dual-shade invocation.
+ *
+ * When the dual-shade is not wide, this always returns 0.5 (the top edge is evenly split). On
+ * wide layouts however, a larger fraction is returned because only the area of the system
+ * status icons is considered top-right.
+ *
+ * Note that this fraction only determines the split between the absolute left and right
+ * directions. In RTL layouts, the "top-start" edge will resolve to "top-right", and "top-end"
+ * will resolve to "top-left".
+ */
+ @FloatRange(from = 0.0, to = 1.0) fun getTopEdgeSplitFraction(): Float
}
/** ShadeInteractor methods with implementations that differ between non-empty impls. */
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
index d51fd28d5458..6c0b55a5dd57 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
@@ -47,4 +47,6 @@ class ShadeInteractorEmptyImpl @Inject constructor() : ShadeInteractor {
override val isExpandToQsEnabled: Flow<Boolean> = inactiveFlowBoolean
override val shadeMode: StateFlow<ShadeMode> = MutableStateFlow(ShadeMode.Single)
override val isShadeLayoutWide: StateFlow<Boolean> = inactiveFlowBoolean
+
+ override fun getTopEdgeSplitFraction(): Float = 0.5f
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
index 3552092d24e7..b8d2dd2a764f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
@@ -16,6 +16,7 @@
package com.android.systemui.shade.domain.interactor
+import androidx.annotation.FloatRange
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardRepository
@@ -104,6 +105,16 @@ constructor(
override val isShadeLayoutWide: StateFlow<Boolean> = shadeRepository.isShadeLayoutWide
+ @FloatRange(from = 0.0, to = 1.0)
+ override fun getTopEdgeSplitFraction(): Float {
+ // Note: this implicitly relies on isShadeLayoutWide being hot (i.e. collected). This
+ // assumption allows us to query its value on demand (during swipe source detection) instead
+ // of running another infinite coroutine.
+ // TODO(b/338577208): Instead of being fixed at 0.8f, this should dynamically updated based
+ // on the position of system-status icons in the status bar.
+ return if (shadeRepository.isShadeLayoutWide.value) 0.8f else 0.5f
+ }
+
override val shadeMode: StateFlow<ShadeMode> =
isShadeLayoutWide
.map(this::determineShadeMode)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
index 55f3ed7062aa..874463819c73 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
@@ -12,6 +12,8 @@ import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.FakeOverlay
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
+import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
+import com.android.systemui.shade.domain.interactor.shadeInteractor
import kotlinx.coroutines.flow.MutableStateFlow
var Kosmos.sceneKeys by Fixture {
@@ -70,6 +72,8 @@ val Kosmos.sceneContainerViewModel by Fixture {
sceneInteractor = sceneInteractor,
falsingInteractor = falsingInteractor,
powerInteractor = powerInteractor,
+ shadeInteractor = shadeInteractor,
+ splitEdgeDetector = splitEdgeDetector,
motionEventHandlerReceiver = {},
logger = sceneLogger
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt
new file mode 100644
index 000000000000..e0b529261c4d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.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.scene.ui.viewmodel
+
+import androidx.compose.ui.unit.dp
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+
+var Kosmos.splitEdgeDetector: SplitEdgeDetector by
+ Kosmos.Fixture {
+ SplitEdgeDetector(
+ topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction,
+ edgeSize = 40.dp,
+ )
+ }