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)
+}