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

Commit e6849c14 authored by amehfooz's avatar amehfooz Committed by Ahmed Mehfooz
Browse files

[DesktopStatusBar] Make the shadeOverlays behave like desktop panels

Changes the touchable region for the ShadeWindow when DualShade
is enabled on desktop. The touchable region is restricted to the
ShadeOverlay bounds when only an overlay is visible. When inside
a scene, everything except the status bar area is touchable.

Bug: 438498963
Test: Manual
Test: new Unit tests
Flag: com.android.systemui.status_bar_for_desktop
Change-Id: I9f2b1d74c726842ffb1b35d1ff82f7ea241d71a2
parent 8d0e4de2
Loading
Loading
Loading
Loading
+18 −0
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.DefaultEdgeDetector
import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.fakeFalsingManager
import com.android.systemui.desktop.domain.interactor.enableDesktopFeatureSet
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
@@ -430,4 +431,21 @@ class SceneContainerViewModelTest : SysuiTestCase() {
            assertThat(underTest.swipeSourceDetector)
                .isInstanceOf(SceneContainerSwipeDetector::class.java)
        }

    @Test
    fun onEmptySpaceMotionEvent_hidesDualShadeOverlays_onDesktopMode() =
        kosmos.runTest {
            // GIVEN a device in desktop mode with dual shade enabled and an overlay present
            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
            enableDualShade()
            enableDesktopFeatureSet()
            sceneInteractor.showOverlay(Overlays.QuickSettingsShade, "test")
            assertThat(currentOverlays).isNotEmpty()

            // WHEN a touch event occurs outside the shade window
            underTest.onEmptySpaceMotionEvent(MotionEvent.obtain(0, 0, ACTION_OUTSIDE, 0f, 0f, 0))

            // THEN the overlay is hidden
            assertThat(currentOverlays).isEmpty()
        }
}
+128 −47
Original line number Diff line number Diff line
@@ -16,65 +16,81 @@

package com.android.systemui.statusbar.phone

import android.content.res.Configuration
import android.content.testableContext
import android.graphics.Rect
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.ObservableTransitionState.Idle
import com.android.compose.animation.scene.OverlayKey
import com.android.internal.policy.SystemBarUtils
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.flags.DisableSceneContainer
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.kosmos.testScope
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.res.R
import com.android.systemui.scene.data.repository.sceneContainerRepository
import com.android.systemui.scene.data.repository.setSceneTransition
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.shade.domain.interactor.enableDualShade
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.domain.interactor.shadeMode
import com.android.systemui.shade.notificationShadeWindowView
import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.statusbar.policy.configurationController
import com.android.systemui.testKosmos
import com.android.systemui.util.kotlin.getValue
import com.google.common.truth.Truth.assertThat
import dagger.Lazy
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.whenever

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class ShadeTouchableRegionManagerTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val sceneRepository = kosmos.sceneContainerRepository

    private val underTest by Lazy { kosmos.shadeTouchableRegionManager }
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val Kosmos.underTest by Kosmos.Fixture { kosmos.shadeTouchableRegionManager }

    @Before
    fun setUp() {
        kosmos.notificationShadeWindowView.apply {
            whenever(width).thenReturn(1000)
            whenever(height).thenReturn(1000)
        }
        kosmos.underTest.setup(kosmos.notificationShadeWindowView)
    }

    @Test
    @EnableSceneContainer
    fun entireScreenTouchable_sceneContainerEnabled_isRemoteUserInteractionOngoing() =
        testScope.runTest {
            sceneRepository.setTransitionState(
                flowOf(ObservableTransitionState.Idle(currentScene = Scenes.Gone))
            )
            runCurrent()
        kosmos.runTest {
            sceneContainerRepository.setTransitionState(flowOf(Idle(currentScene = Scenes.Gone)))
            assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse()

            sceneRepository.isRemoteUserInputOngoing.value = true
            runCurrent()
            sceneContainerRepository.isRemoteUserInputOngoing.value = true
            assertThat(underTest.shouldMakeEntireScreenTouchable()).isTrue()

            sceneRepository.isRemoteUserInputOngoing.value = false
            runCurrent()
            sceneContainerRepository.isRemoteUserInputOngoing.value = false
            assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse()
        }

    @Test
    @DisableSceneContainer
    fun entireScreenTouchable_sceneContainerDisabled_isRemoteUserInteractionOngoing() =
        testScope.runTest {
        kosmos.runTest {
            assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse()

            sceneRepository.isRemoteUserInputOngoing.value = true
            runCurrent()
            sceneContainerRepository.isRemoteUserInputOngoing.value = true

            assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse()
        }
@@ -82,36 +98,24 @@ class ShadeTouchableRegionManagerTest : SysuiTestCase() {
    @Test
    @EnableSceneContainer
    fun entireScreenTouchable_sceneContainerEnabled_isIdleOnGone() =
        testScope.runTest {
            sceneRepository.setTransitionState(
                flowOf(ObservableTransitionState.Idle(currentScene = Scenes.Gone))
            )
            runCurrent()
        kosmos.runTest {
            sceneContainerRepository.setTransitionState(flowOf(Idle(currentScene = Scenes.Gone)))
            assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse()

            sceneRepository.setTransitionState(
                flowOf(ObservableTransitionState.Idle(currentScene = Scenes.Shade))
            )
            runCurrent()
            sceneContainerRepository.setTransitionState(flowOf(Idle(currentScene = Scenes.Shade)))
            assertThat(underTest.shouldMakeEntireScreenTouchable()).isTrue()

            sceneRepository.setTransitionState(
                flowOf(ObservableTransitionState.Idle(currentScene = Scenes.Gone))
            )
            runCurrent()
            sceneContainerRepository.setTransitionState(flowOf(Idle(currentScene = Scenes.Gone)))
            assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse()
        }

    @Test
    @DisableSceneContainer
    fun entireScreenTouchable_sceneContainerDisabled_isIdleOnGone() =
        testScope.runTest {
        kosmos.runTest {
            assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse()

            sceneRepository.setTransitionState(
                flowOf(ObservableTransitionState.Idle(currentScene = Scenes.Shade))
            )
            runCurrent()
            sceneContainerRepository.setTransitionState(flowOf(Idle(currentScene = Scenes.Shade)))

            assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse()
        }
@@ -119,17 +123,94 @@ class ShadeTouchableRegionManagerTest : SysuiTestCase() {
    @Test
    @DisableSceneContainer
    fun entireScreenTouchable_communalVisible() =
        testScope.runTest {
        kosmos.runTest {
            assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse()

            kosmos.fakeCommunalSceneRepository.instantlyTransitionTo(CommunalScenes.Communal)
            runCurrent()

            assertThat(underTest.shouldMakeEntireScreenTouchable()).isTrue()

            kosmos.fakeCommunalSceneRepository.instantlyTransitionTo(CommunalScenes.Blank)
            runCurrent()

            assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse()
        }

    @Test
    @EnableSceneContainer
    fun entireScreenTouchable_desktopMode() =
        kosmos.runTest {
            enableStatusBarForDesktop()
            openShadeOverlay(Overlays.QuickSettingsShade)

            assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse()
        }

    @Test
    @EnableSceneContainer
    fun calculateTouchableRegionForDesktop_sceneGone_withShadeBounds() =
        kosmos.runTest {
            val bounds = Rect(0, 0, 100, 100)
            enableStatusBarForDesktop()
            lockDevice()
            unlockDevice()
            openShadeOverlay(Overlays.QuickSettingsShade)
            shadeInteractor.setShadeOverlayBounds(bounds)

            val rects = underTest.calculateTouchableRegionForDesktop()

            assertThat(rects).containsExactly(bounds)
        }

    @Test
    @EnableSceneContainer
    fun calculateTouchableRegionForDesktop_sceneVisible_withoutShadeBounds() =
        kosmos.runTest {
            enableStatusBarForDesktop()
            lockDevice()
            shadeInteractor.setShadeOverlayBounds(null)
            val statusBarHeight = SystemBarUtils.getStatusBarHeight(mContext)
            val expectedRect = Rect(0, statusBarHeight, 1000, 1000)

            val rects = underTest.calculateTouchableRegionForDesktop()

            assertThat(rects).containsExactly(expectedRect)
        }

    private fun Kosmos.openShadeOverlay(overlay: OverlayKey) {
        val shadeMode by collectLastValue(shadeMode)
        val currentScene by collectLastValue(sceneInteractor.currentScene)
        val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
        val initialScene = checkNotNull(currentScene)
        assertThat(shadeMode).isEqualTo(ShadeMode.Dual)

        sceneInteractor.showOverlay(overlay, "test")
        setSceneTransition(Idle(initialScene, checkNotNull(currentOverlays)))
        assertThat(currentScene).isEqualTo(initialScene)
        assertThat(currentOverlays).contains(overlay)
    }

    private fun Kosmos.closeShadeOverlay(overlay: OverlayKey) {
        sceneInteractor.hideOverlay(overlay, "test")
    }

    private fun Kosmos.enableStatusBarForDesktop() {
        enableDualShade()
        testableContext.orCreateTestableResources.addOverride(
            R.bool.config_enableDesktopFeatureSet,
            true,
        )
        configurationController.onConfigurationChanged(Configuration())
    }

    private fun Kosmos.unlockDevice() {
        fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
            SuccessFingerprintAuthenticationStatus(0, true)
        )
        sceneInteractor.changeScene(Scenes.Gone, "unlock")
        sceneContainerRepository.setTransitionState(flowOf(Idle(Scenes.Gone)))
    }

    private fun Kosmos.lockDevice() {
        sceneContainerRepository.instantlyTransitionTo(Scenes.Lockscreen)
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ constructor(
    configurationController: ConfigurationController,
) {

    // TODO(441100057): This StateFlow should support Connected Displays.
    /** Whether the desktop feature set is enabled. */
    val isDesktopFeatureSetEnabled: StateFlow<Boolean> =
        configurationController.onConfigChanged
+28 −2
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.classifier.Classifier
import com.android.systemui.classifier.domain.interactor.FalsingInteractor
import com.android.systemui.desktop.domain.interactor.DesktopInteractor
import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
@@ -40,7 +41,6 @@ import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.qs.panels.ui.viewmodel.AnimateQsTilesViewModel
import com.android.systemui.statusbar.notification.domain.interactor.NotificationContainerInteractor
import com.android.systemui.scene.domain.interactor.OnBootTransitionInteractor
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.logger.SceneLogger
@@ -49,7 +49,9 @@ import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.composable.Overlay
import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.statusbar.core.StatusBarForDesktop
import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor
import com.android.systemui.statusbar.notification.domain.interactor.NotificationContainerInteractor
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
import com.android.systemui.wallpapers.ui.viewmodel.WallpaperViewModel
import dagger.assisted.Assisted
@@ -66,12 +68,13 @@ class SceneContainerViewModel
@AssistedInject
constructor(
    private val sceneInteractor: SceneInteractor,
    private val desktopInteractor: DesktopInteractor,
    private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
    private val falsingInteractor: FalsingInteractor,
    private val powerInteractor: PowerInteractor,
    private val onBootTransitionInteractor: OnBootTransitionInteractor,
    private val shadeModeInteractor: ShadeModeInteractor,
    private val notificationContainerInteractor: NotificationContainerInteractor,
    shadeModeInteractor: ShadeModeInteractor,
    private val remoteInputInteractor: RemoteInputInteractor,
    private val logger: SceneLogger,
    hapticsViewModelFactory: SceneContainerHapticsViewModel.Factory,
@@ -124,6 +127,16 @@ constructor(
            initialValue = 1f,
        )

    private val isDesktopStatusBarEnabled by
        hydrator.hydratedStateOf(
            traceName = "isDesktopStatusBarEnabled",
            source =
                desktopInteractor.isDesktopFeatureSetEnabled.map { enabled ->
                    enabled && StatusBarForDesktop.isEnabled
                },
            initialValue = false,
        )

    override suspend fun onActivated(): Nothing {
        try {
            // Sends a MotionEventHandler to the owner of the view-model so they can report
@@ -193,6 +206,19 @@ constructor(
     * Call this after the [MotionEvent] has finished propagating through the UI hierarchy.
     */
    fun onEmptySpaceMotionEvent(event: MotionEvent) {
        // Hide dual shade overlays when there is a touch outside the shade window.
        // This is only applicable when the desktop status bar is enabled.
        if (
            shadeModeInteractor.isDualShade &&
                isDesktopStatusBarEnabled &&
                event.action == MotionEvent.ACTION_OUTSIDE &&
                sceneInteractor.currentOverlays.value.isNotEmpty()
        ) {
            sceneInteractor.currentOverlays.value.forEach {
                sceneInteractor.hideOverlay(it, "Empty space touch")
            }
        }

        // check if the touch is outside the window and if remote input is active.
        // If true, close any active remote inputs.
        if (
+172 −28

File changed.

Preview size limit exceeded, changes collapsed.

Loading