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

Commit 2fbc21e4 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

[flexiglass] Respects gesture exclusion regions.

There exists a developer-facing API where apps can specify a region (a
collection of one or more Rects) where system gestures (e.g.
interactions to do with System UI) are ignored, letting those apps take
over those gestures, allowing for example, for games to not quit each
time the player accidentally performs the back navigation gesture. That
API is documented here:
https://developer.android.com/develop/ui/views/touch-and-input/gestures/gesturenav

While gesture back navigation is already set up to respect these
regions, Flexiglass was not. This meant that, even if an app set up a
system gesture exclusion region, dragging down would still expand the
shade, for example.

This CL uses the API provided in the previous CL on this chain to
actually pass in a filterGesture lambda into STL and connects the
implementation of that lambda to the actual gesture exclusion regions as
provided by the WindowManager.

Bug: 367447743
Test: manually verified using the brightness slider in the flexiglass
version of QS which, itself publishes an exlcusion region. With this CL,
starting a drag from the bounds of the brightness slider and moving the
finger up does nothing. Starting the same gesture from above or below
the bounds of the slider works normally.
Test: unit tests added
Flag: com.android.systemui.scene_container

Change-Id: I94dc0d4bf7db66cd4f9d71bb0f470754267e9e35
parent f9a01533
Loading
Loading
Loading
Loading
+6 −10
Original line number Diff line number Diff line
@@ -126,17 +126,18 @@ fun SceneContainer(
                    awaitFirstDown(false)
                    viewModel.onSceneContainerUserInputStarted()
                }
            },
            }
    ) {
        SceneTransitionLayout(
            state = state,
            modifier = modifier.fillMaxSize(),
            swipeSourceDetector = viewModel.edgeDetector,
            gestureFilter = viewModel::shouldFilterGesture,
        ) {
            sceneByKey.forEach { (sceneKey, scene) ->
                scene(
                    key = sceneKey,
                    userActions = userActionsByContentKey.getOrDefault(sceneKey, emptyMap())
                    userActions = userActionsByContentKey.getOrDefault(sceneKey, emptyMap()),
                ) {
                    // Activate the scene.
                    LaunchedEffect(scene) { scene.activate() }
@@ -144,7 +145,7 @@ fun SceneContainer(
                    // Render the scene.
                    with(scene) {
                        this@scene.Content(
                            modifier = Modifier.element(sceneKey.rootElementKey).fillMaxSize(),
                            modifier = Modifier.element(sceneKey.rootElementKey).fillMaxSize()
                        )
                    }
                }
@@ -152,7 +153,7 @@ fun SceneContainer(
            overlayByKey.forEach { (overlayKey, overlay) ->
                overlay(
                    key = overlayKey,
                    userActions = userActionsByContentKey.getOrDefault(overlayKey, emptyMap())
                    userActions = userActionsByContentKey.getOrDefault(overlayKey, emptyMap()),
                ) {
                    // Activate the overlay.
                    LaunchedEffect(overlay) { overlay.activate() }
@@ -164,12 +165,7 @@ fun SceneContainer(
        }

        BottomRightCornerRibbon(
            content = {
                Text(
                    text = "flexi\uD83E\uDD43",
                    color = Color.White,
                )
            },
            content = { Text(text = "flexi\uD83E\uDD43", color = Color.White) },
            modifier = Modifier.align(Alignment.BottomEnd),
        )
    }
+112 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

@file:OptIn(ExperimentalCoroutinesApi::class)

package com.android.systemui.scene.ui.viewmodel

import android.graphics.Region
import android.view.setSystemGestureExclusionRegion
import androidx.compose.ui.geometry.Offset
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.scene.sceneContainerGestureFilterFactory
import com.android.systemui.settings.displayTracker
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith

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

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val displayId = kosmos.displayTracker.defaultDisplayId

    private val underTest = kosmos.sceneContainerGestureFilterFactory.create(displayId)
    private val activationJob = Job()

    @Test
    fun shouldFilterGesture_whenNoRegion_returnsFalse() =
        testScope.runTest {
            activate()
            setSystemGestureExclusionRegion(displayId, null)
            runCurrent()

            assertThat(underTest.shouldFilterGesture(Offset(100f, 100f))).isFalse()
        }

    @Test
    fun shouldFilterGesture_whenOutsideRegion_returnsFalse() =
        testScope.runTest {
            activate()
            setSystemGestureExclusionRegion(displayId, Region(0, 0, 200, 200))
            runCurrent()

            assertThat(underTest.shouldFilterGesture(Offset(300f, 100f))).isFalse()
        }

    @Test
    fun shouldFilterGesture_whenInsideRegion_returnsTrue() =
        testScope.runTest {
            activate()
            setSystemGestureExclusionRegion(displayId, Region(0, 0, 200, 200))
            runCurrent()

            assertThat(underTest.shouldFilterGesture(Offset(100f, 100f))).isTrue()
        }

    @Test(expected = IllegalStateException::class)
    fun shouldFilterGesture_beforeActivation_throws() =
        testScope.runTest {
            setSystemGestureExclusionRegion(displayId, Region(0, 0, 200, 200))
            runCurrent()

            underTest.shouldFilterGesture(Offset(100f, 100f))
        }

    @Test(expected = IllegalStateException::class)
    fun shouldFilterGesture_afterCancellation_throws() =
        testScope.runTest {
            activate()
            setSystemGestureExclusionRegion(displayId, Region(0, 0, 200, 200))
            runCurrent()

            cancel()

            underTest.shouldFilterGesture(Offset(100f, 100f))
        }

    private fun TestScope.activate() {
        underTest.activateIn(testScope, activationJob)
        runCurrent()
    }

    private fun TestScope.cancel() {
        activationJob.cancel()
        runCurrent()
    }
}
+5 −4
Original line number Diff line number Diff line
@@ -36,10 +36,12 @@ import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.fakeOverlaysByKeys
import com.android.systemui.scene.sceneContainerConfig
import com.android.systemui.scene.sceneContainerGestureFilterFactory
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.settings.displayTracker
import com.android.systemui.shade.data.repository.fakeShadeRepository
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.shared.flag.DualShade
@@ -86,6 +88,8 @@ class SceneContainerViewModelTest : SysuiTestCase() {
                shadeInteractor = kosmos.shadeInteractor,
                splitEdgeDetector = kosmos.splitEdgeDetector,
                logger = kosmos.sceneLogger,
                gestureFilterFactory = kosmos.sceneContainerGestureFilterFactory,
                displayId = kosmos.displayTracker.defaultDisplayId,
                motionEventHandlerReceiver = { motionEventHandler ->
                    this@SceneContainerViewModelTest.motionEventHandler = motionEventHandler
                },
@@ -283,10 +287,7 @@ class SceneContainerViewModelTest : SysuiTestCase() {
            fakeSceneDataSource.showOverlay(Overlays.NotificationsShade)
            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
            assertThat(currentOverlays)
                .containsExactly(
                    Overlays.QuickSettingsShade,
                    Overlays.NotificationsShade,
                )
                .containsExactly(Overlays.QuickSettingsShade, Overlays.NotificationsShade)

            val actionableContentKey =
                underTest.getActionableContentKey(
+56 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.scene.data.repository

import android.graphics.Region
import android.view.ISystemGestureExclusionListener
import android.view.IWindowManager
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import javax.inject.Inject
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow

@SysUISingleton
class SystemGestureExclusionRepository
@Inject
constructor(private val windowManager: IWindowManager) {

    /**
     * Returns [Flow] of the [Region] in which system gestures should be excluded on the display
     * identified with [displayId].
     */
    fun exclusionRegion(displayId: Int): Flow<Region?> {
        return conflatedCallbackFlow {
            val listener =
                object : ISystemGestureExclusionListener.Stub() {
                    override fun onSystemGestureExclusionChanged(
                        displayId: Int,
                        restrictedRegion: Region?,
                        unrestrictedRegion: Region?,
                    ) {
                        trySend(restrictedRegion)
                    }
                }
            windowManager.registerSystemGestureExclusionListener(listener, displayId)

            awaitClose {
                windowManager.unregisterSystemGestureExclusionListener(listener, displayId)
            }
        }
    }
}
+35 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.scene.domain.interactor

import android.graphics.Region
import com.android.systemui.scene.data.repository.SystemGestureExclusionRepository
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow

class SystemGestureExclusionInteractor
@Inject
constructor(private val repository: SystemGestureExclusionRepository) {

    /**
     * Returns [Flow] of the [Region] in which system gestures should be excluded on the display
     * identified with [displayId].
     */
    fun exclusionRegion(displayId: Int): Flow<Region?> {
        return repository.exclusionRegion(displayId)
    }
}
Loading