Merge "[Spa] Upgrade Google Material to 1.12.0" into main
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 df101c5..dc9e267 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
@@ -126,17 +126,18 @@
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 @@
// Render the scene.
with(scene) {
this@scene.Content(
- modifier = Modifier.element(sceneKey.rootElementKey).fillMaxSize(),
+ modifier = Modifier.element(sceneKey.rootElementKey).fillMaxSize()
)
}
}
@@ -152,7 +153,7 @@
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 @@
}
BottomRightCornerRibbon(
- content = {
- Text(
- text = "flexi\uD83E\uDD43",
- color = Color.White,
- )
- },
+ content = { Text(text = "flexi\uD83E\uDD43", color = Color.White) },
modifier = Modifier.align(Alignment.BottomEnd),
)
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index f38a310..9891025 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -123,6 +123,10 @@
overSlop: Float,
pointersDown: Int,
): DragController {
+ if (startedPosition != null && layoutImpl.gestureFilter(startedPosition)) {
+ return NoOpDragController
+ }
+
if (overSlop == 0f) {
val oldDragController = dragController
check(oldDragController != null && oldDragController.isDrivingTransition) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index cec8883..6e89814 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -47,6 +47,9 @@
* @param state the state of this layout.
* @param swipeSourceDetector the edge detector used to detect which edge a swipe is started from,
* if any.
+ * @param gestureFilter decides whether a drag gesture that started at the given start position
+ * should be filtered. If the lambda returns `true`, the drag gesture will be ignored. If it
+ * returns `false`, the drag gesture will be handled.
* @param transitionInterceptionThreshold used during a scene transition. For the scene to be
* intercepted, the progress value must be above the threshold, and below (1 - threshold).
* @param builder the configuration of the different scenes and overlays of this layout.
@@ -57,6 +60,7 @@
modifier: Modifier = Modifier,
swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
swipeDetector: SwipeDetector = DefaultSwipeDetector,
+ gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter,
@FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0.05f,
builder: SceneTransitionLayoutScope.() -> Unit,
) {
@@ -65,6 +69,7 @@
modifier,
swipeSourceDetector,
swipeDetector,
+ gestureFilter,
transitionInterceptionThreshold,
onLayoutImpl = null,
builder,
@@ -616,6 +621,7 @@
modifier: Modifier = Modifier,
swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
swipeDetector: SwipeDetector = DefaultSwipeDetector,
+ gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter,
transitionInterceptionThreshold: Float = 0f,
onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)? = null,
builder: SceneTransitionLayoutScope.() -> Unit,
@@ -632,6 +638,7 @@
transitionInterceptionThreshold = transitionInterceptionThreshold,
builder = builder,
animationScope = animationScope,
+ gestureFilter = gestureFilter,
)
.also { onLayoutImpl?.invoke(it) }
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 65c4043..9e7be37 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -31,6 +31,7 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.ApproachLayoutModifierNode
import androidx.compose.ui.layout.ApproachMeasureScope
import androidx.compose.ui.layout.LookaheadScope
@@ -70,6 +71,7 @@
* animations.
*/
internal val animationScope: CoroutineScope,
+ internal val gestureFilter: (startedPosition: Offset) -> Boolean,
) {
/**
* The map of [Scene]s.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt
index 54ee783..f758102 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt
@@ -17,6 +17,7 @@
package com.android.compose.animation.scene
import androidx.compose.runtime.Stable
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerInputChange
/** {@link SwipeDetector} helps determine whether a swipe gestured has occurred. */
@@ -31,6 +32,8 @@
val DefaultSwipeDetector = PassthroughSwipeDetector()
+val DefaultGestureFilter = { _: Offset -> false }
+
/** An {@link SwipeDetector} implementation that recognizes a swipe on any input. */
class PassthroughSwipeDetector : SwipeDetector {
override fun detectSwipe(change: PointerInputChange): Boolean {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index fca92ca..b64b8be 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -108,6 +108,8 @@
val transitionInterceptionThreshold = 0.05f
+ var gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter
+
private val layoutImpl =
SceneTransitionLayoutImpl(
state = layoutState,
@@ -120,6 +122,7 @@
// Use testScope and not backgroundScope here because backgroundScope does not
// work well with advanceUntilIdle(), which is used by some tests.
animationScope = testScope,
+ gestureFilter = { startedPosition -> gestureFilter.invoke(startedPosition) },
)
.apply { setContentsAndLayoutTargetSizeForTest(LAYOUT_SIZE) }
@@ -317,6 +320,13 @@
}
@Test
+ fun onDragStarted_doesNotStartTransition_whenGestureFiltered() = runGestureTest {
+ gestureFilter = { _ -> true }
+ onDragStarted(overSlop = down(fractionOfScreen = 0.1f), expectedConsumedOverSlop = 0f)
+ assertIdle(currentScene = SceneA)
+ }
+
+ @Test
fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest {
val dragController = onDragStarted(overSlop = down(fractionOfScreen = 0.1f))
assertTransition(currentScene = SceneA)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilterTest.kt
new file mode 100644
index 0000000..efde1ec
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilterTest.kt
@@ -0,0 +1,112 @@
+/*
+ * 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()
+ }
+}
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 a0bb017..e60e742 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
@@ -36,10 +36,12 @@
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 @@
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 @@
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(
diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepository.kt
new file mode 100644
index 0000000..a8d0777
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepository.kt
@@ -0,0 +1,56 @@
+/*
+ * 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)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractor.kt
new file mode 100644
index 0000000..4cee874
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractor.kt
@@ -0,0 +1,35 @@
+/*
+ * 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)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
index 7f35d73..a7e7d8b 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
@@ -107,7 +107,9 @@
view.viewModel(
traceName = "SceneWindowRootViewBinder",
minWindowLifecycleState = WindowLifecycleState.ATTACHED,
- factory = { viewModelFactory.create(motionEventHandlerReceiver) },
+ factory = {
+ viewModelFactory.create(view.context.displayId, motionEventHandlerReceiver)
+ },
) { viewModel ->
try {
view.setViewTreeOnBackPressedDispatcherOwner(
@@ -184,7 +186,7 @@
PlatformTheme {
ScreenDecorProvider(
displayCutout = displayCutoutFromWindowInsets(scope, context, windowInsets),
- screenCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
+ screenCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context),
) {
SceneContainer(
viewModel = viewModel,
@@ -205,9 +207,7 @@
): ComposeView {
return ComposeView(context).apply {
setContent {
- AlternateBouncer(
- alternateBouncerDependencies = alternateBouncerDependencies,
- )
+ AlternateBouncer(alternateBouncerDependencies = alternateBouncerDependencies)
}
}
}
@@ -234,14 +234,7 @@
else -> CutoutLocation.CENTER
}
val viewDisplayCutout = it?.displayCutout
- DisplayCutout(
- left,
- top,
- right,
- bottom,
- location,
- viewDisplayCutout,
- )
+ DisplayCutout(left, top, right, bottom, location, viewDisplayCutout)
}
.stateIn(scope, SharingStarted.WhileSubscribed(), DisplayCutout())
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilter.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilter.kt
new file mode 100644
index 0000000..a1d915a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilter.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.runtime.getValue
+import androidx.compose.ui.geometry.Offset
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.Hydrator
+import com.android.systemui.scene.domain.interactor.SystemGestureExclusionInteractor
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlin.math.roundToInt
+
+/** Decides whether drag gestures should be filtered out in the scene container framework. */
+class SceneContainerGestureFilter
+@AssistedInject
+constructor(interactor: SystemGestureExclusionInteractor, @Assisted displayId: Int) :
+ ExclusiveActivatable() {
+
+ private val hydrator = Hydrator("SceneContainerGestureFilter.hydrator")
+ private val exclusionRegion by
+ hydrator.hydratedStateOf(
+ traceName = "exclusionRegion",
+ initialValue = null,
+ source = interactor.exclusionRegion(displayId),
+ )
+
+ override suspend fun onActivated(): Nothing {
+ hydrator.activate()
+ }
+
+ /**
+ * Returns `true` if a drag gesture starting at [startPosition] should be filtered out (e.g.
+ * ignored, `false` otherwise.
+ *
+ * Invoke this and pass in the position of the `ACTION_DOWN` pointer event that began the
+ * gesture.
+ */
+ fun shouldFilterGesture(startPosition: Offset): Boolean {
+ check(isActive) { "Must be activated to use!" }
+
+ return exclusionRegion?.contains(startPosition.x.roundToInt(), startPosition.y.roundToInt())
+ ?: false
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(displayId: Int): SceneContainerGestureFilter
+ }
+}
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 af1f5a7..0bf2d49 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
@@ -18,6 +18,7 @@
import android.view.MotionEvent
import androidx.compose.runtime.getValue
+import androidx.compose.ui.geometry.Offset
import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.DefaultEdgeDetector
import com.android.compose.animation.scene.ObservableTransitionState
@@ -41,9 +42,12 @@
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
/** Models UI state for the scene container. */
class SceneContainerViewModel
@@ -55,6 +59,8 @@
shadeInteractor: ShadeInteractor,
private val splitEdgeDetector: SplitEdgeDetector,
private val logger: SceneLogger,
+ gestureFilterFactory: SceneContainerGestureFilter.Factory,
+ @Assisted displayId: Int,
@Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
) : ExclusiveActivatable() {
@@ -80,6 +86,8 @@
},
)
+ private val gestureFilter: SceneContainerGestureFilter = gestureFilterFactory.create(displayId)
+
override suspend fun onActivated(): Nothing {
try {
// Sends a MotionEventHandler to the owner of the view-model so they can report
@@ -96,7 +104,11 @@
}
)
- hydrator.activate()
+ coroutineScope {
+ launch { hydrator.activate() }
+ launch { gestureFilter.activate() }
+ }
+ awaitCancellation()
} finally {
// Clears the previously-sent MotionEventHandler so the owner of the view-model releases
// their reference to it.
@@ -243,6 +255,17 @@
}
}
+ /**
+ * Returns `true` if a drag gesture starting at [startPosition] should be filtered out (e.g.
+ * ignored, `false` otherwise.
+ *
+ * Invoke this and pass in the position of the `ACTION_DOWN` pointer event that began the
+ * gesture.
+ */
+ fun shouldFilterGesture(startPosition: Offset): Boolean {
+ return gestureFilter.shouldFilterGesture(startPosition)
+ }
+
/** Defines interface for classes that can handle externally-reported [MotionEvent]s. */
interface MotionEventHandler {
/** Notifies that a [MotionEvent] has occurred. */
@@ -258,7 +281,8 @@
@AssistedFactory
interface Factory {
fun create(
- motionEventHandlerReceiver: (MotionEventHandler?) -> Unit
+ displayId: Int,
+ motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
): SceneContainerViewModel
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
index 71afa62..5cc6454 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
@@ -62,6 +62,7 @@
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.domain.startable.sceneContainerStartable
+import com.android.systemui.scene.sceneContainerGestureFilterFactory
import com.android.systemui.scene.shared.logger.sceneLogger
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.Scenes
@@ -70,6 +71,7 @@
import com.android.systemui.scene.ui.composable.SceneContainer
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
+import com.android.systemui.settings.displayTracker
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.testKosmos
import kotlin.time.Duration.Companion.seconds
@@ -133,6 +135,8 @@
shadeInteractor = kosmos.shadeInteractor,
splitEdgeDetector = kosmos.splitEdgeDetector,
logger = kosmos.sceneLogger,
+ gestureFilterFactory = kosmos.sceneContainerGestureFilterFactory,
+ displayId = kosmos.displayTracker.defaultDisplayId,
motionEventHandlerReceiver = {},
)
.apply { setTransitionState(transitionState) }
diff --git a/packages/SystemUI/tests/utils/src/android/view/WindowManagerServiceKosmos.kt b/packages/SystemUI/tests/utils/src/android/view/WindowManagerServiceKosmos.kt
new file mode 100644
index 0000000..cd681a1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/android/view/WindowManagerServiceKosmos.kt
@@ -0,0 +1,58 @@
+/*
+ * 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 android.view
+
+import android.graphics.Region
+import com.android.systemui.kosmos.Kosmos
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.mock
+import org.mockito.kotlin.any
+import org.mockito.kotlin.whenever
+
+val Kosmos.mockWindowManagerService: IWindowManager by
+ Kosmos.Fixture {
+ mock(IWindowManager::class.java).apply {
+ whenever(registerSystemGestureExclusionListener(any(), anyInt())).then { answer ->
+ val listener = answer.arguments[0] as ISystemGestureExclusionListener
+ val displayId = answer.arguments[1] as Int
+ exclusionListeners.getOrPut(displayId) { mutableListOf() }.add(listener)
+ listener.onSystemGestureExclusionChanged(
+ displayId,
+ restrictedRegionByDisplayId[displayId],
+ null,
+ )
+ }
+
+ whenever(unregisterSystemGestureExclusionListener(any(), anyInt())).then { answer ->
+ val listener = answer.arguments[0] as ISystemGestureExclusionListener
+ val displayId = answer.arguments[1] as Int
+ exclusionListeners[displayId]?.remove(listener)
+ }
+ }
+ }
+
+var Kosmos.windowManagerService: IWindowManager by Kosmos.Fixture { mockWindowManagerService }
+
+private var restrictedRegionByDisplayId = mutableMapOf<Int, Region?>()
+private var exclusionListeners = mutableMapOf<Int, MutableList<ISystemGestureExclusionListener>>()
+
+fun setSystemGestureExclusionRegion(displayId: Int, restrictedRegion: Region?) {
+ restrictedRegionByDisplayId[displayId] = restrictedRegion
+ exclusionListeners[displayId]?.forEach { listener ->
+ listener.onSystemGestureExclusionChanged(displayId, restrictedRegion, null)
+ }
+}
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 8744638..737aaf2 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
@@ -6,13 +6,16 @@
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.domain.interactor.systemGestureExclusionInteractor
import com.android.systemui.scene.shared.logger.sceneLogger
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.FakeOverlay
+import com.android.systemui.scene.ui.viewmodel.SceneContainerGestureFilter
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
+import com.android.systemui.settings.displayTracker
import com.android.systemui.shade.domain.interactor.shadeInteractor
import kotlinx.coroutines.flow.MutableStateFlow
@@ -30,10 +33,7 @@
val Kosmos.initialSceneKey by Fixture { Scenes.Lockscreen }
var Kosmos.overlayKeys by Fixture {
- listOf(
- Overlays.NotificationsShade,
- Overlays.QuickSettingsShade,
- )
+ listOf(Overlays.NotificationsShade, Overlays.QuickSettingsShade)
}
val Kosmos.fakeOverlaysByKeys by Fixture { overlayKeys.associateWith { FakeOverlay(it) } }
@@ -74,8 +74,21 @@
powerInteractor = powerInteractor,
shadeInteractor = shadeInteractor,
splitEdgeDetector = splitEdgeDetector,
+ gestureFilterFactory = sceneContainerGestureFilterFactory,
+ displayId = displayTracker.defaultDisplayId,
motionEventHandlerReceiver = {},
- logger = sceneLogger
+ logger = sceneLogger,
)
.apply { setTransitionState(transitionState) }
}
+
+val Kosmos.sceneContainerGestureFilterFactory by Fixture {
+ object : SceneContainerGestureFilter.Factory {
+ override fun create(displayId: Int): SceneContainerGestureFilter {
+ return SceneContainerGestureFilter(
+ interactor = systemGestureExclusionInteractor,
+ displayId = displayId,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepositoryKosmos.kt
new file mode 100644
index 0000000..15ed1b3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepositoryKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.view.windowManagerService
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+
+val Kosmos.systemGestureExclusionRepository by Fixture {
+ SystemGestureExclusionRepository(windowManager = windowManagerService)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractorKosmos.kt
new file mode 100644
index 0000000..3e46c3f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractorKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * 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 com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.scene.data.repository.systemGestureExclusionRepository
+
+val Kosmos.systemGestureExclusionInteractor by Fixture {
+ SystemGestureExclusionInteractor(repository = systemGestureExclusionRepository)
+}