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

Commit 0ca7e7db authored by burakov's avatar burakov Committed by Danny Burakov
Browse files

[Dual Shade] Introduce SceneContainerSwipeDetector for detecting swipes.

This replaces the `SplitEdgeDetector` by a more general swipe detector
for scene container, which detects swipes on the left and right halves
of the screen in addition to the edges.

This also changes the invocation gesture of the Dual Shade to not only
be swipes on the top edge of the screen, but anywhere on the screen
besides the left, right, and bottom edges.

BONUS: Removes the redundant "topEdgeSplitFraction" references and
resulting dependency on ShadeInteractor, so the swipe detector no longer
needs to be injected.

Fix: 394154602
Test: Tested manually by swiping down from both the top edge and center
 of the screen (both left and right sides) and verifying the shade opens
 and closes as expected. Also tested by swiping down when an app is open
 and verified that only swipes on the top edge expand the shade.
Test: Updated unit tests.
Flag: com.android.systemui.scene_container
Change-Id: I599bd970176c900dc44bdd1ae13d07f87a7b18bf
parent b1c163f2
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -204,7 +204,7 @@ fun SceneContainer(
        SceneTransitionLayout(
            state = state,
            modifier = modifier.fillMaxSize(),
            swipeSourceDetector = viewModel.edgeDetector,
            swipeSourceDetector = viewModel.swipeSourceDetector,
        ) {
            sceneByKey.forEach { (sceneKey, scene) ->
                scene(
+7 −7
Original line number Diff line number Diff line
@@ -41,7 +41,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.TransitionKeys
import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
import com.android.systemui.scene.ui.viewmodel.SceneContainerArea
import com.android.systemui.shade.data.repository.shadeRepository
import com.android.systemui.shade.domain.interactor.disableDualShade
import com.android.systemui.shade.domain.interactor.enableDualShade
@@ -275,20 +275,20 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() {
                assertThat(downDestination?.transitionKey).isNull()
            }

            val downFromTopRightDestination =
            val downFromEndHalfDestination =
                userActions?.get(
                    Swipe.Down(
                        fromSource = SceneContainerEdge.TopRight,
                        fromSource = SceneContainerArea.EndHalf,
                        pointerCount = if (downWithTwoPointers) 2 else 1,
                    )
                )
            when {
                !isShadeTouchable -> assertThat(downFromTopRightDestination).isNull()
                downWithTwoPointers -> assertThat(downFromTopRightDestination).isNull()
                !isShadeTouchable -> assertThat(downFromEndHalfDestination).isNull()
                downWithTwoPointers -> assertThat(downFromEndHalfDestination).isNull()
                else -> {
                    assertThat(downFromTopRightDestination)
                    assertThat(downFromEndHalfDestination)
                        .isEqualTo(ShowOverlay(Overlays.QuickSettingsShade))
                    assertThat(downFromTopRightDestination?.transitionKey).isNull()
                    assertThat(downFromEndHalfDestination?.transitionKey).isNull()
                }
            }

+3 −3
Original line number Diff line number Diff line
@@ -30,7 +30,7 @@ import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
import com.android.systemui.scene.ui.viewmodel.SceneContainerArea
import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayActionsViewModel
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
@@ -71,13 +71,13 @@ class NotificationsShadeOverlayActionsViewModelTest : SysuiTestCase() {
        }

    @Test
    fun downFromTopRight_switchesToQuickSettingsShade() =
    fun downFromTopEnd_switchesToQuickSettingsShade() =
        testScope.runTest {
            val actions by collectLastValue(underTest.actions)
            underTest.activateIn(this)

            val action =
                (actions?.get(Swipe.Down(fromSource = SceneContainerEdge.TopRight)) as? ShowOverlay)
                (actions?.get(Swipe.Down(fromSource = SceneContainerArea.EndHalf)) as? ShowOverlay)
            assertThat(action?.overlay).isEqualTo(Overlays.QuickSettingsShade)
            val overlaysToHide = action?.hideCurrentOverlays as? HideCurrentOverlays.Some
            assertThat(overlaysToHide).isNotNull()
+4 −3
Original line number Diff line number Diff line
@@ -31,7 +31,7 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.qs.panels.ui.viewmodel.editModeViewModel
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
import com.android.systemui.scene.ui.viewmodel.SceneContainerArea
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
@@ -84,13 +84,14 @@ class QuickSettingsShadeOverlayActionsViewModelTest : SysuiTestCase() {
        }

    @Test
    fun downFromTopLeft_switchesToNotificationsShade() =
    fun downFromTopStart_switchesToNotificationsShade() =
        testScope.runTest {
            val actions by collectLastValue(underTest.actions)
            underTest.activateIn(this)

            val action =
                (actions?.get(Swipe.Down(fromSource = SceneContainerEdge.TopLeft)) as? ShowOverlay)
                (actions?.get(Swipe.Down(fromSource = SceneContainerArea.StartHalf))
                    as? ShowOverlay)
            assertThat(action?.overlay).isEqualTo(Overlays.NotificationsShade)
            val overlaysToHide = action?.hideCurrentOverlays as? HideCurrentOverlays.Some
            assertThat(overlaysToHide).isNotNull()
+196 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.SceneContainerArea.EndEdge
import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.EndHalf
import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.BottomEdge
import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.LeftEdge
import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.LeftHalf
import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.RightEdge
import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.Resolved.RightHalf
import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.StartEdge
import com.android.systemui.scene.ui.viewmodel.SceneContainerArea.StartHalf
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class SceneContainerSwipeDetectorTest : SysuiTestCase() {

    private val edgeSize = 40
    private val screenWidth = 800
    private val screenHeight = 600

    private val underTest = SceneContainerSwipeDetector(edgeSize = edgeSize.dp)

    @Test
    fun source_noEdge_detectsLeftHalf() {
        val detectedEdge = swipeVerticallyFrom(x = screenWidth / 2 - 1, y = screenHeight / 2)
        assertThat(detectedEdge).isEqualTo(LeftHalf)
    }

    @Test
    fun source_swipeVerticallyOnTopLeft_detectsLeftHalf() {
        val detectedEdge = swipeVerticallyFrom(x = 1, y = edgeSize - 1)
        assertThat(detectedEdge).isEqualTo(LeftHalf)
    }

    @Test
    fun source_swipeHorizontallyOnTopLeft_detectsLeftEdge() {
        val detectedEdge = swipeHorizontallyFrom(x = 1, y = edgeSize - 1)
        assertThat(detectedEdge).isEqualTo(LeftEdge)
    }

    @Test
    fun source_swipeVerticallyOnTopRight_detectsRightHalf() {
        val detectedEdge = swipeVerticallyFrom(x = screenWidth - 1, y = edgeSize - 1)
        assertThat(detectedEdge).isEqualTo(RightHalf)
    }

    @Test
    fun source_swipeHorizontallyOnTopRight_detectsRightEdge() {
        val detectedEdge = swipeHorizontallyFrom(x = screenWidth - 1, y = edgeSize - 1)
        assertThat(detectedEdge).isEqualTo(RightEdge)
    }

    @Test
    fun source_swipeVerticallyToLeftOfSplit_detectsLeftHalf() {
        val detectedEdge = swipeVerticallyFrom(x = (screenWidth / 2) - 1, y = edgeSize - 1)
        assertThat(detectedEdge).isEqualTo(LeftHalf)
    }

    @Test
    fun source_swipeVerticallyToRightOfSplit_detectsRightHalf() {
        val detectedEdge = swipeVerticallyFrom(x = (screenWidth / 2) + 1, y = edgeSize - 1)
        assertThat(detectedEdge).isEqualTo(RightHalf)
    }

    @Test
    fun source_swipeVerticallyOnBottom_detectsBottomEdge() {
        val detectedEdge =
            swipeVerticallyFrom(x = screenWidth / 3, y = screenHeight - (edgeSize / 2))
        assertThat(detectedEdge).isEqualTo(BottomEdge)
    }

    @Test
    fun source_swipeHorizontallyOnBottom_detectsLeftHalf() {
        val detectedEdge =
            swipeHorizontallyFrom(x = screenWidth / 3, y = screenHeight - (edgeSize - 1))
        assertThat(detectedEdge).isEqualTo(LeftHalf)
    }

    @Test
    fun source_swipeHorizontallyOnLeft_detectsLeftEdge() {
        val detectedEdge = swipeHorizontallyFrom(x = edgeSize - 1, y = screenHeight / 2)
        assertThat(detectedEdge).isEqualTo(LeftEdge)
    }

    @Test
    fun source_swipeVerticallyOnLeft_detectsLeftHalf() {
        val detectedEdge = swipeVerticallyFrom(x = edgeSize - 1, y = screenHeight / 2)
        assertThat(detectedEdge).isEqualTo(LeftHalf)
    }

    @Test
    fun source_swipeHorizontallyOnRight_detectsRightEdge() {
        val detectedEdge =
            swipeHorizontallyFrom(x = screenWidth - edgeSize + 1, y = screenHeight / 2)
        assertThat(detectedEdge).isEqualTo(RightEdge)
    }

    @Test
    fun source_swipeVerticallyOnRight_detectsRightHalf() {
        val detectedEdge = swipeVerticallyFrom(x = screenWidth - edgeSize + 1, y = screenHeight / 2)
        assertThat(detectedEdge).isEqualTo(RightHalf)
    }

    @Test
    fun resolve_startEdgeInLtr_resolvesLeftEdge() {
        val resolvedEdge = StartEdge.resolve(LayoutDirection.Ltr)
        assertThat(resolvedEdge).isEqualTo(LeftEdge)
    }

    @Test
    fun resolve_startEdgeInRtl_resolvesRightEdge() {
        val resolvedEdge = StartEdge.resolve(LayoutDirection.Rtl)
        assertThat(resolvedEdge).isEqualTo(RightEdge)
    }

    @Test
    fun resolve_endEdgeInLtr_resolvesRightEdge() {
        val resolvedEdge = EndEdge.resolve(LayoutDirection.Ltr)
        assertThat(resolvedEdge).isEqualTo(RightEdge)
    }

    @Test
    fun resolve_endEdgeInRtl_resolvesLeftEdge() {
        val resolvedEdge = EndEdge.resolve(LayoutDirection.Rtl)
        assertThat(resolvedEdge).isEqualTo(LeftEdge)
    }

    @Test
    fun resolve_startHalfInLtr_resolvesLeftHalf() {
        val resolvedEdge = StartHalf.resolve(LayoutDirection.Ltr)
        assertThat(resolvedEdge).isEqualTo(LeftHalf)
    }

    @Test
    fun resolve_startHalfInRtl_resolvesRightHalf() {
        val resolvedEdge = StartHalf.resolve(LayoutDirection.Rtl)
        assertThat(resolvedEdge).isEqualTo(RightHalf)
    }

    @Test
    fun resolve_endHalfInLtr_resolvesRightHalf() {
        val resolvedEdge = EndHalf.resolve(LayoutDirection.Ltr)
        assertThat(resolvedEdge).isEqualTo(RightHalf)
    }

    @Test
    fun resolve_endHalfInRtl_resolvesLeftHalf() {
        val resolvedEdge = EndHalf.resolve(LayoutDirection.Rtl)
        assertThat(resolvedEdge).isEqualTo(LeftHalf)
    }

    private fun swipeVerticallyFrom(x: Int, y: Int): SceneContainerArea.Resolved? {
        return swipeFrom(x, y, Orientation.Vertical)
    }

    private fun swipeHorizontallyFrom(x: Int, y: Int): SceneContainerArea.Resolved? {
        return swipeFrom(x, y, Orientation.Horizontal)
    }

    private fun swipeFrom(x: Int, y: Int, orientation: Orientation): SceneContainerArea.Resolved? {
        return underTest.source(
            layoutSize = IntSize(width = screenWidth, height = screenHeight),
            position = IntOffset(x, y),
            density = Density(1f),
            orientation = orientation,
        )
    }
}
Loading