Merge "[bc25] Handle transitions involving overlays." into main
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
index 671b012..a6d5c1c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
@@ -48,17 +48,13 @@
 import com.android.systemui.scene.shared.model.Scenes
 
 object QuickSettings {
-    private val SCENES =
-        setOf(
-            Scenes.QuickSettings,
-            Scenes.Shade,
-        )
+    private val SCENES = setOf(Scenes.QuickSettings, Scenes.Shade)
 
     object Elements {
         val Content =
             MovableElementKey(
                 "QuickSettingsContent",
-                contentPicker = MovableElementContentPicker(SCENES)
+                contentPicker = MovableElementContentPicker(SCENES),
             )
         val QuickQuickSettings = ElementKey("QuickQuickSettings")
         val SplitShadeQuickSettings = ElementKey("SplitShadeQuickSettings")
@@ -87,7 +83,7 @@
 
 private fun SceneScope.stateForQuickSettingsContent(
     isSplitShade: Boolean,
-    squishiness: () -> Float = { QuickSettings.SharedValues.SquishinessValues.Default }
+    squishiness: () -> Float = { QuickSettings.SharedValues.SquishinessValues.Default },
 ): QSSceneAdapter.State {
     return when (val transitionState = layoutState.transitionState) {
         is TransitionState.Idle -> {
@@ -122,7 +118,7 @@
                 }
             }
         is TransitionState.Transition.OverlayTransition ->
-            TODO("b/359173565: Handle overlay transitions")
+            error("Bad transition for QuickSettings scene: overlays not supported")
     }
 }
 
@@ -172,7 +168,7 @@
                 val height = heightProvider().coerceAtLeast(0)
 
                 layout(placeable.width, height) { placeable.placeRelative(0, 0) }
-            }
+            },
     ) {
         content { QuickSettingsContent(qsSceneAdapter = qsSceneAdapter, contentState) }
     }
@@ -225,7 +221,7 @@
                             it.addView(view)
                         }
                     },
-                    onRelease = { it.removeAllViews() }
+                    onRelease = { it.removeAllViews() },
                 )
             }
         }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorTest.kt
index edaa3d3..bf97afe 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorTest.kt
@@ -18,9 +18,12 @@
 
 package com.android.systemui.scene.domain.interactor
 
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 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.Transition.ShowOrHideOverlay
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
@@ -29,8 +32,10 @@
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.sceneDataSource
+import com.android.systemui.shade.shared.flag.DualShade
 import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
@@ -63,10 +68,11 @@
     private val sceneDataSource =
         kosmos.sceneDataSource.apply { changeScene(toScene = Scenes.Lockscreen) }
 
-    private val underTest = kosmos.sceneContainerOcclusionInteractor
+    private val underTest by lazy { kosmos.sceneContainerOcclusionInteractor }
 
     @Test
-    fun invisibleDueToOcclusion() =
+    @DisableFlags(DualShade.FLAG_NAME)
+    fun invisibleDueToOcclusion_dualShadeDisabled() =
         testScope.runTest {
             val invisibleDueToOcclusion by collectLastValue(underTest.invisibleDueToOcclusion)
             val keyguardState by collectLastValue(keyguardTransitionInteractor.currentKeyguardState)
@@ -126,6 +132,68 @@
                 .isFalse()
         }
 
+    @Test
+    @EnableFlags(DualShade.FLAG_NAME)
+    fun invisibleDueToOcclusion_dualShadeEnabled() =
+        testScope.runTest {
+            val invisibleDueToOcclusion by collectLastValue(underTest.invisibleDueToOcclusion)
+            val keyguardState by collectLastValue(keyguardTransitionInteractor.currentKeyguardState)
+
+            // Assert that we have the desired preconditions:
+            assertThat(keyguardState).isEqualTo(KeyguardState.LOCKSCREEN)
+            assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Lockscreen)
+            assertThat(sceneInteractor.transitionState.value)
+                .isEqualTo(ObservableTransitionState.Idle(Scenes.Lockscreen))
+            assertWithMessage("Should start unoccluded").that(invisibleDueToOcclusion).isFalse()
+
+            // Actual testing starts here:
+            showOccludingActivity()
+            assertWithMessage("Should become occluded when occluding activity is shown")
+                .that(invisibleDueToOcclusion)
+                .isTrue()
+
+            transitionIntoAod {
+                assertWithMessage("Should become unoccluded when transitioning into AOD")
+                    .that(invisibleDueToOcclusion)
+                    .isFalse()
+            }
+            assertWithMessage("Should stay unoccluded when in AOD")
+                .that(invisibleDueToOcclusion)
+                .isFalse()
+
+            transitionOutOfAod {
+                assertWithMessage("Should remain unoccluded while transitioning away from AOD")
+                    .that(invisibleDueToOcclusion)
+                    .isFalse()
+            }
+            assertWithMessage("Should become occluded now that no longer in AOD")
+                .that(invisibleDueToOcclusion)
+                .isTrue()
+
+            expandDualShade {
+                assertWithMessage("Should become unoccluded once shade begins to expand")
+                    .that(invisibleDueToOcclusion)
+                    .isFalse()
+            }
+            assertWithMessage("Should be unoccluded when shade is fully expanded")
+                .that(invisibleDueToOcclusion)
+                .isFalse()
+
+            collapseDualShade {
+                assertWithMessage("Should remain unoccluded while shade is collapsing")
+                    .that(invisibleDueToOcclusion)
+                    .isFalse()
+            }
+            assertWithMessage("Should become occluded now that shade is fully collapsed")
+                .that(invisibleDueToOcclusion)
+                .isTrue()
+
+            hideOccludingActivity()
+            assertWithMessage("Should become unoccluded once the occluding activity is hidden")
+                .that(invisibleDueToOcclusion)
+                .isFalse()
+        }
+
     /** Simulates the appearance of a show-when-locked `Activity` in the foreground. */
     private fun TestScope.showOccludingActivity() {
         keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(
@@ -138,15 +206,13 @@
     /** Simulates the disappearance of a show-when-locked `Activity` from the foreground. */
     private fun TestScope.hideOccludingActivity() {
         keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(
-            showWhenLockedActivityOnTop = false,
+            showWhenLockedActivityOnTop = false
         )
         runCurrent()
     }
 
     /** Simulates a user-driven gradual expansion of the shade. */
-    private fun TestScope.expandShade(
-        assertMidTransition: () -> Unit = {},
-    ) {
+    private fun TestScope.expandShade(assertMidTransition: () -> Unit = {}) {
         val progress = MutableStateFlow(0f)
         mutableTransitionState.value =
             ObservableTransitionState.Transition(
@@ -170,10 +236,41 @@
         runCurrent()
     }
 
+    /** Simulates a user-driven gradual expansion of the dual shade (notifications). */
+    private fun TestScope.expandDualShade(assertMidTransition: () -> Unit = {}) {
+        val progress = MutableStateFlow(0f)
+        mutableTransitionState.value =
+            ShowOrHideOverlay(
+                overlay = Overlays.NotificationsShade,
+                fromContent = sceneDataSource.currentScene.value,
+                toContent = Overlays.NotificationsShade,
+                currentScene = sceneDataSource.currentScene.value,
+                currentOverlays = sceneDataSource.currentOverlays,
+                progress = progress,
+                isInitiatedByUserInput = true,
+                isUserInputOngoing = flowOf(true),
+                previewProgress = flowOf(0f),
+                isInPreviewStage = flowOf(false),
+            )
+        runCurrent()
+
+        progress.value = 0.5f
+        runCurrent()
+        assertMidTransition()
+
+        progress.value = 1f
+        runCurrent()
+
+        mutableTransitionState.value =
+            ObservableTransitionState.Idle(
+                sceneDataSource.currentScene.value,
+                setOf(Overlays.NotificationsShade),
+            )
+        runCurrent()
+    }
+
     /** Simulates a user-driven gradual collapse of the shade. */
-    private fun TestScope.collapseShade(
-        assertMidTransition: () -> Unit = {},
-    ) {
+    private fun TestScope.collapseShade(assertMidTransition: () -> Unit = {}) {
         val progress = MutableStateFlow(0f)
         mutableTransitionState.value =
             ObservableTransitionState.Transition(
@@ -197,10 +294,37 @@
         runCurrent()
     }
 
+    /** Simulates a user-driven gradual collapse of the dual shade (notifications). */
+    private fun TestScope.collapseDualShade(assertMidTransition: () -> Unit = {}) {
+        val progress = MutableStateFlow(0f)
+        mutableTransitionState.value =
+            ShowOrHideOverlay(
+                overlay = Overlays.NotificationsShade,
+                fromContent = Overlays.NotificationsShade,
+                toContent = Scenes.Lockscreen,
+                currentScene = Scenes.Lockscreen,
+                currentOverlays = flowOf(setOf(Overlays.NotificationsShade)),
+                progress = progress,
+                isInitiatedByUserInput = true,
+                isUserInputOngoing = flowOf(true),
+                previewProgress = flowOf(0f),
+                isInPreviewStage = flowOf(false),
+            )
+        runCurrent()
+
+        progress.value = 0.5f
+        runCurrent()
+        assertMidTransition()
+
+        progress.value = 1f
+        runCurrent()
+
+        mutableTransitionState.value = ObservableTransitionState.Idle(Scenes.Lockscreen)
+        runCurrent()
+    }
+
     /** Simulates a transition into AOD. */
-    private suspend fun TestScope.transitionIntoAod(
-        assertMidTransition: () -> Unit = {},
-    ) {
+    private suspend fun TestScope.transitionIntoAod(assertMidTransition: () -> Unit = {}) {
         val currentKeyguardState = keyguardTransitionInteractor.getCurrentState()
         keyguardTransitionRepository.sendTransitionStep(
             TransitionStep(
@@ -235,9 +359,7 @@
     }
 
     /** Simulates a transition away from AOD. */
-    private suspend fun TestScope.transitionOutOfAod(
-        assertMidTransition: () -> Unit = {},
-    ) {
+    private suspend fun TestScope.transitionOutOfAod(assertMidTransition: () -> Unit = {}) {
         keyguardTransitionRepository.sendTransitionStep(
             TransitionStep(
                 from = KeyguardState.AOD,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index 4a7d8b0..7fe3d8d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -21,6 +21,7 @@
 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.Transition.ShowOrHideOverlay
 import com.android.compose.animation.scene.SceneKey
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
@@ -38,6 +39,7 @@
 import com.android.systemui.scene.overlayKeys
 import com.android.systemui.scene.sceneContainerConfig
 import com.android.systemui.scene.sceneKeys
+import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.SceneFamilies
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.fakeSceneDataSource
@@ -256,7 +258,7 @@
         }
 
     @Test
-    fun transitioningTo() =
+    fun transitioningTo_sceneChange() =
         testScope.runTest {
             val transitionState =
                 MutableStateFlow<ObservableTransitionState>(
@@ -293,6 +295,51 @@
         }
 
     @Test
+    fun transitioningTo_overlayChange() =
+        testScope.runTest {
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Idle(underTest.currentScene.value)
+                )
+            underTest.setTransitionState(transitionState)
+
+            val transitionTo by collectLastValue(underTest.transitioningTo)
+            assertThat(transitionTo).isNull()
+
+            underTest.showOverlay(Overlays.NotificationsShade, "reason")
+            assertThat(transitionTo).isNull()
+
+            val progress = MutableStateFlow(0f)
+            transitionState.value =
+                ShowOrHideOverlay(
+                    overlay = Overlays.NotificationsShade,
+                    fromContent = underTest.currentScene.value,
+                    toContent = Overlays.NotificationsShade,
+                    currentScene = underTest.currentScene.value,
+                    currentOverlays = underTest.currentOverlays,
+                    progress = progress,
+                    isInitiatedByUserInput = true,
+                    isUserInputOngoing = flowOf(true),
+                    previewProgress = flowOf(0f),
+                    isInPreviewStage = flowOf(false),
+                )
+            assertThat(transitionTo).isEqualTo(Overlays.NotificationsShade)
+
+            progress.value = 0.5f
+            assertThat(transitionTo).isEqualTo(Overlays.NotificationsShade)
+
+            progress.value = 1f
+            assertThat(transitionTo).isEqualTo(Overlays.NotificationsShade)
+
+            transitionState.value =
+                ObservableTransitionState.Idle(
+                    currentScene = underTest.currentScene.value,
+                    currentOverlays = setOf(Overlays.NotificationsShade),
+                )
+            assertThat(transitionTo).isNull()
+        }
+
+    @Test
     fun isTransitionUserInputOngoing_idle_false() =
         testScope.runTest {
             val transitionState =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImplTest.kt
index 851b7b9..ba559b5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImplTest.kt
@@ -21,6 +21,8 @@
 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.Transition.ShowOrHideOverlay
+import com.android.compose.animation.scene.OverlayKey
 import com.android.compose.animation.scene.SceneKey
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
@@ -32,6 +34,7 @@
 import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.fakeSceneDataSource
 import com.android.systemui.testKosmos
@@ -125,6 +128,63 @@
 
     @Test
     @EnableSceneContainer
+    fun legacyPanelExpansion_dualShade_whenIdle_whenLocked() =
+        testScope.runTest {
+            underTest = kosmos.panelExpansionInteractorImpl
+            val panelExpansion by collectLastValue(underTest.legacyPanelExpansion)
+
+            changeScene(Scenes.Lockscreen) { assertThat(panelExpansion).isEqualTo(1f) }
+            assertThat(panelExpansion).isEqualTo(1f)
+
+            changeScene(Scenes.Bouncer) { assertThat(panelExpansion).isEqualTo(1f) }
+            assertThat(panelExpansion).isEqualTo(1f)
+
+            showOverlay(Overlays.NotificationsShade) { assertThat(panelExpansion).isEqualTo(1f) }
+            assertThat(panelExpansion).isEqualTo(1f)
+
+            showOverlay(Overlays.QuickSettingsShade) { assertThat(panelExpansion).isEqualTo(1f) }
+            assertThat(panelExpansion).isEqualTo(1f)
+
+            changeScene(Scenes.Communal) { assertThat(panelExpansion).isEqualTo(1f) }
+            assertThat(panelExpansion).isEqualTo(1f)
+        }
+
+    @Test
+    @EnableSceneContainer
+    fun legacyPanelExpansion_dualShade_whenIdle_whenUnlocked() =
+        testScope.runTest {
+            underTest = kosmos.panelExpansionInteractorImpl
+            val unlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus)
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                SuccessFingerprintAuthenticationStatus(0, true)
+            )
+            runCurrent()
+
+            assertThat(unlockStatus)
+                .isEqualTo(DeviceUnlockStatus(true, DeviceUnlockSource.Fingerprint))
+
+            val panelExpansion by collectLastValue(underTest.legacyPanelExpansion)
+
+            changeScene(Scenes.Gone) { assertThat(panelExpansion).isEqualTo(0f) }
+            assertThat(panelExpansion).isEqualTo(0f)
+
+            showOverlay(Overlays.NotificationsShade) { progress ->
+                assertThat(panelExpansion).isEqualTo(progress)
+            }
+            assertThat(panelExpansion).isEqualTo(1f)
+
+            showOverlay(Overlays.QuickSettingsShade) {
+                // Notification shade is already expanded, so moving to QS shade should also be 1f.
+                assertThat(panelExpansion).isEqualTo(1f)
+            }
+            assertThat(panelExpansion).isEqualTo(1f)
+
+            changeScene(Scenes.Communal) { assertThat(panelExpansion).isEqualTo(1f) }
+            assertThat(panelExpansion).isEqualTo(1f)
+        }
+
+    @Test
+    @EnableSceneContainer
     fun shouldHideStatusBarIconsWhenExpanded_goneScene() =
         testScope.runTest {
             underTest = kosmos.panelExpansionInteractorImpl
@@ -193,4 +253,72 @@
 
         assertThat(currentScene).isEqualTo(toScene)
     }
+
+    private fun TestScope.showOverlay(
+        toOverlay: OverlayKey,
+        assertDuringProgress: ((progress: Float) -> Unit) = {},
+    ) {
+        val currentScene by collectLastValue(sceneInteractor.currentScene)
+        val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+        val progressFlow = MutableStateFlow(0f)
+        transitionState.value =
+            if (checkNotNull(currentOverlays).isEmpty()) {
+                ShowOrHideOverlay(
+                    overlay = toOverlay,
+                    fromContent = checkNotNull(currentScene),
+                    toContent = toOverlay,
+                    currentScene = checkNotNull(currentScene),
+                    currentOverlays = flowOf(emptySet()),
+                    progress = progressFlow,
+                    isInitiatedByUserInput = true,
+                    isUserInputOngoing = flowOf(true),
+                    previewProgress = flowOf(0f),
+                    isInPreviewStage = flowOf(false),
+                )
+            } else {
+                ObservableTransitionState.Transition.ReplaceOverlay(
+                    fromOverlay = checkNotNull(currentOverlays).first(),
+                    toOverlay = toOverlay,
+                    currentScene = checkNotNull(currentScene),
+                    currentOverlays = flowOf(emptySet()),
+                    progress = progressFlow,
+                    isInitiatedByUserInput = true,
+                    isUserInputOngoing = flowOf(true),
+                    previewProgress = flowOf(0f),
+                    isInPreviewStage = flowOf(false),
+                )
+            }
+        runCurrent()
+        assertDuringProgress(progressFlow.value)
+
+        progressFlow.value = 0.2f
+        runCurrent()
+        assertDuringProgress(progressFlow.value)
+
+        progressFlow.value = 0.6f
+        runCurrent()
+        assertDuringProgress(progressFlow.value)
+
+        progressFlow.value = 1f
+        runCurrent()
+        assertDuringProgress(progressFlow.value)
+
+        transitionState.value =
+            ObservableTransitionState.Idle(
+                currentScene = checkNotNull(currentScene),
+                currentOverlays = setOf(toOverlay),
+            )
+        if (checkNotNull(currentOverlays).isEmpty()) {
+            fakeSceneDataSource.showOverlay(toOverlay)
+        } else {
+            fakeSceneDataSource.replaceOverlay(
+                from = checkNotNull(currentOverlays).first(),
+                to = toOverlay,
+            )
+        }
+        runCurrent()
+        assertDuringProgress(progressFlow.value)
+
+        assertThat(currentOverlays).containsExactly(toOverlay)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
index 429b47b..667827a 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
@@ -16,13 +16,14 @@
 
 package com.android.systemui.scene.domain.interactor
 
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.ObservableTransitionState
-import com.android.compose.animation.scene.SceneKey
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.domain.interactor.KeyguardOcclusionInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.Scenes
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -117,28 +118,30 @@
     private val ObservableTransitionState.canBeOccluded: Boolean
         get() =
             when (this) {
-                is ObservableTransitionState.Idle -> currentScene.canBeOccluded
-                is ObservableTransitionState.Transition.ChangeScene ->
-                    fromScene.canBeOccluded && toScene.canBeOccluded
-                is ObservableTransitionState.Transition.ReplaceOverlay,
-                is ObservableTransitionState.Transition.ShowOrHideOverlay ->
-                    TODO("b/359173565: Handle overlay transitions")
+                is ObservableTransitionState.Idle ->
+                    currentOverlays.all { it.canBeOccluded } && currentScene.canBeOccluded
+                is ObservableTransitionState.Transition ->
+                    // TODO(b/356596436): Should also verify currentOverlays.isEmpty(), but
+                    //  currentOverlays is a Flow and we need a state.
+                    fromContent.canBeOccluded && toContent.canBeOccluded
             }
 
     /**
-     * Whether the scene can be occluded by a "show when locked" activity. Some scenes should, on
+     * Whether the content can be occluded by a "show when locked" activity. Some content should, on
      * principle not be occlude-able because they render as if they are expanding on top of the
      * occluding activity.
      */
-    private val SceneKey.canBeOccluded: Boolean
+    private val ContentKey.canBeOccluded: Boolean
         get() =
             when (this) {
+                Overlays.NotificationsShade -> false
+                Overlays.QuickSettingsShade -> false
                 Scenes.Bouncer -> false
                 Scenes.Communal -> true
                 Scenes.Gone -> true
                 Scenes.Lockscreen -> true
                 Scenes.QuickSettings -> false
                 Scenes.Shade -> false
-                else -> error("SceneKey \"$this\" doesn't have a mapping for canBeOccluded!")
+                else -> error("ContentKey \"$this\" doesn't have a mapping for canBeOccluded!")
             }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index 0d24adc..f20e5a5 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -120,21 +120,18 @@
             )
 
     /**
-     * The key of the scene that the UI is currently transitioning to or `null` if there is no
+     * The key of the content that the UI is currently transitioning to or `null` if there is no
      * active transition at the moment.
      *
      * This is a convenience wrapper around [transitionState], meant for flow-challenged consumers
      * like Java code.
      */
-    val transitioningTo: StateFlow<SceneKey?> =
+    val transitioningTo: StateFlow<ContentKey?> =
         transitionState
             .map { state ->
                 when (state) {
                     is ObservableTransitionState.Idle -> null
-                    is ObservableTransitionState.Transition.ChangeScene -> state.toScene
-                    is ObservableTransitionState.Transition.ShowOrHideOverlay,
-                    is ObservableTransitionState.Transition.ReplaceOverlay ->
-                        TODO("b/359173565: Handle overlay transitions")
+                    is ObservableTransitionState.Transition -> state.toContent
                 }
             }
             .stateIn(
@@ -160,15 +157,14 @@
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.WhileSubscribed(),
-                initialValue = false
+                initialValue = false,
             )
 
     /** Whether the scene container is visible. */
     val isVisible: StateFlow<Boolean> =
-        combine(
-                repository.isVisible,
-                repository.isRemoteUserInputOngoing,
-            ) { isVisible, isRemoteUserInteractionOngoing ->
+        combine(repository.isVisible, repository.isRemoteUserInputOngoing) {
+                isVisible,
+                isRemoteUserInteractionOngoing ->
                 isVisibleInternal(
                     raw = isVisible,
                     isRemoteUserInputOngoing = isRemoteUserInteractionOngoing,
@@ -177,7 +173,7 @@
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.WhileSubscribed(),
-                initialValue = isVisibleInternal()
+                initialValue = isVisibleInternal(),
             )
 
     /** Whether there's an ongoing remotely-initiated user interaction. */
@@ -259,10 +255,7 @@
      * The change is instantaneous and not animated; it will be observable in the next frame and
      * there will be no transition animation.
      */
-    fun snapToScene(
-        toScene: SceneKey,
-        loggingReason: String,
-    ) {
+    fun snapToScene(toScene: SceneKey, loggingReason: String) {
         val currentSceneKey = currentScene.value
         val resolvedScene =
             sceneFamilyResolvers.get()[toScene]?.let { familyResolver ->
@@ -313,15 +306,9 @@
             return
         }
 
-        logger.logOverlayChangeRequested(
-            to = overlay,
-            reason = loggingReason,
-        )
+        logger.logOverlayChangeRequested(to = overlay, reason = loggingReason)
 
-        repository.showOverlay(
-            overlay = overlay,
-            transitionKey = transitionKey,
-        )
+        repository.showOverlay(overlay = overlay, transitionKey = transitionKey)
     }
 
     /**
@@ -345,15 +332,9 @@
             return
         }
 
-        logger.logOverlayChangeRequested(
-            from = overlay,
-            reason = loggingReason,
-        )
+        logger.logOverlayChangeRequested(from = overlay, reason = loggingReason)
 
-        repository.hideOverlay(
-            overlay = overlay,
-            transitionKey = transitionKey,
-        )
+        repository.hideOverlay(overlay = overlay, transitionKey = transitionKey)
     }
 
     /**
@@ -378,17 +359,9 @@
             return
         }
 
-        logger.logOverlayChangeRequested(
-            from = from,
-            to = to,
-            reason = loggingReason,
-        )
+        logger.logOverlayChangeRequested(from = from, to = to, reason = loggingReason)
 
-        repository.replaceOverlay(
-            from = from,
-            to = to,
-            transitionKey = transitionKey,
-        )
+        repository.replaceOverlay(from = from, to = to, transitionKey = transitionKey)
     }
 
     /**
@@ -405,11 +378,7 @@
             return
         }
 
-        logger.logVisibilityChange(
-            from = wasVisible,
-            to = isVisible,
-            reason = loggingReason,
-        )
+        logger.logVisibilityChange(from = wasVisible, to = isVisible, reason = loggingReason)
         return repository.setVisible(isVisible)
     }
 
@@ -491,11 +460,7 @@
      * @param loggingReason The reason why the transition is requested, for logging purposes
      * @return `true` if the scene change is valid; `false` if it shouldn't happen
      */
-    private fun validateSceneChange(
-        from: SceneKey,
-        to: SceneKey,
-        loggingReason: String,
-    ): Boolean {
+    private fun validateSceneChange(from: SceneKey, to: SceneKey, loggingReason: String): Boolean {
         if (to !in repository.allContentKeys) {
             return false
         }
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 c451704..af1f5a7 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
@@ -52,7 +52,7 @@
     private val sceneInteractor: SceneInteractor,
     private val falsingInteractor: FalsingInteractor,
     private val powerInteractor: PowerInteractor,
-    private val shadeInteractor: ShadeInteractor,
+    shadeInteractor: ShadeInteractor,
     private val splitEdgeDetector: SplitEdgeDetector,
     private val logger: SceneLogger,
     @Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
@@ -212,9 +212,10 @@
                         )
                     }
                 }
+                // Overlay transitions don't use scene families, nothing to resolve.
                 is UserActionResult.ShowOverlay,
                 is UserActionResult.HideOverlay,
-                is UserActionResult.ReplaceByOverlay -> TODO("b/353679003: Support overlays")
+                is UserActionResult.ReplaceByOverlay -> null
             } ?: actionResult
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt
index e276f88..cea521f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PanelExpansionInteractorImpl.kt
@@ -18,10 +18,11 @@
 
 package com.android.systemui.shade.domain.interactor
 
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.ObservableTransitionState
-import com.android.compose.animation.scene.SceneKey
 import com.android.systemui.dagger.SysUISingleton
 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.statusbar.SysuiStatusBarStateController
 import javax.inject.Inject
@@ -55,7 +56,9 @@
             when (state) {
                 is ObservableTransitionState.Idle ->
                     flowOf(
-                        if (state.currentScene != Scenes.Gone) {
+                        if (
+                            state.currentScene != Scenes.Gone || state.currentOverlays.isNotEmpty()
+                        ) {
                             // When resting on a non-Gone scene, the panel is fully expanded.
                             1f
                         } else {
@@ -64,10 +67,10 @@
                             0f
                         }
                     )
-                is ObservableTransitionState.Transition.ChangeScene ->
+                is ObservableTransitionState.Transition ->
                     when {
-                        state.fromScene == Scenes.Gone ->
-                            if (state.toScene.isExpandable()) {
+                        state.fromContent == Scenes.Gone ->
+                            if (state.toContent.isExpandable()) {
                                 // Moving from Gone to a scene that can animate-expand has a
                                 // panel expansion that tracks with the transition.
                                 state.progress
@@ -76,8 +79,8 @@
                                 // immediately makes the panel fully expanded.
                                 flowOf(1f)
                             }
-                        state.toScene == Scenes.Gone ->
-                            if (state.fromScene.isExpandable()) {
+                        state.toContent == Scenes.Gone ->
+                            if (state.fromContent.isExpandable()) {
                                 // Moving to Gone from a scene that can animate-expand has a
                                 // panel expansion that tracks with the transition.
                                 state.progress.map { 1 - it }
@@ -88,9 +91,6 @@
                             }
                         else -> flowOf(1f)
                     }
-                is ObservableTransitionState.Transition.ShowOrHideOverlay,
-                is ObservableTransitionState.Transition.ReplaceOverlay ->
-                    TODO("b/359173565: Handle overlay transitions")
             }
         }
 
@@ -132,7 +132,13 @@
         return sceneInteractor.currentScene.value == Scenes.Lockscreen
     }
 
-    private fun SceneKey.isExpandable(): Boolean {
-        return this == Scenes.Shade || this == Scenes.QuickSettings
+    private fun ContentKey.isExpandable(): Boolean {
+        return when (this) {
+            Scenes.Shade,
+            Scenes.QuickSettings,
+            Overlays.NotificationsShade,
+            Overlays.QuickSettingsShade -> true
+            else -> false
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index 3e42413..8d7007b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
 
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.ObservableTransitionState.Idle
 import com.android.compose.animation.scene.ObservableTransitionState.Transition
 import com.android.compose.animation.scene.ObservableTransitionState.Transition.ChangeScene
@@ -26,7 +27,7 @@
 import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
-import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.shared.model.ShadeMode
@@ -46,7 +47,6 @@
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.mapNotNull
 
 /** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */
@@ -91,7 +91,7 @@
     ): Float {
         return if (fullyExpandedDuringSceneChange(change)) {
             1f
-        } else if (change.isBetween({ it == Scenes.Gone }, { it in SceneFamilies.NotifShade })) {
+        } else if (change.isBetween({ it == Scenes.Gone }, { it == Scenes.Shade })) {
             shadeExpansion
         } else if (change.isBetween({ it == Scenes.Gone }, { it == Scenes.QuickSettings })) {
             // during QS expansion, increase fraction at same rate as scrim alpha,
@@ -99,6 +99,22 @@
             (qsExpansion / EXPANSION_FOR_MAX_SCRIM_ALPHA - EXPANSION_FOR_DELAYED_STACK_FADE_IN)
                 .coerceIn(0f, 1f)
         } else {
+            // TODO(b/356596436): If notification shade overlay is open, we'll reach this point and
+            //  the expansion fraction in that case should be `shadeExpansion`.
+            0f
+        }
+    }
+
+    private fun expandFractionDuringOverlayTransition(
+        transition: Transition,
+        currentScene: SceneKey,
+        shadeExpansion: Float,
+    ): Float {
+        return if (currentScene == Scenes.Lockscreen) {
+            1f
+        } else if (transition.isTransitioningFromOrTo(Overlays.NotificationsShade)) {
+            shadeExpansion
+        } else {
             0f
         }
     }
@@ -114,18 +130,35 @@
                 shadeInteractor.shadeMode,
                 shadeInteractor.qsExpansion,
                 sceneInteractor.transitionState,
-                sceneInteractor.resolveSceneFamily(SceneFamilies.QuickSettings),
-            ) { shadeExpansion, _, qsExpansion, transitionState, _ ->
+            ) { shadeExpansion, _, qsExpansion, transitionState ->
                 when (transitionState) {
-                    is Idle -> if (expandedInScene(transitionState.currentScene)) 1f else 0f
+                    is Idle ->
+                        if (
+                            expandedInScene(transitionState.currentScene) ||
+                                Overlays.NotificationsShade in transitionState.currentOverlays
+                        ) {
+                            1f
+                        } else {
+                            0f
+                        }
                     is ChangeScene ->
                         expandFractionDuringSceneChange(
-                            transitionState,
-                            shadeExpansion,
-                            qsExpansion,
+                            change = transitionState,
+                            shadeExpansion = shadeExpansion,
+                            qsExpansion = qsExpansion,
                         )
-                    is Transition.ShowOrHideOverlay,
-                    is Transition.ReplaceOverlay -> TODO("b/359173565: Handle overlay transitions")
+                    is Transition.ShowOrHideOverlay ->
+                        expandFractionDuringOverlayTransition(
+                            transition = transitionState,
+                            currentScene = transitionState.currentScene,
+                            shadeExpansion = shadeExpansion,
+                        )
+                    is Transition.ReplaceOverlay ->
+                        expandFractionDuringOverlayTransition(
+                            transition = transitionState,
+                            currentScene = transitionState.currentScene,
+                            shadeExpansion = shadeExpansion,
+                        )
                 }
             }
             .distinctUntilChanged()
@@ -166,14 +199,14 @@
 
     fun shadeScrimShape(
         cornerRadius: Flow<Int>,
-        viewLeftOffset: Flow<Int>
+        viewLeftOffset: Flow<Int>,
     ): Flow<ShadeScrimShape?> =
         combine(shadeScrimClipping, cornerRadius, viewLeftOffset) { clipping, radius, leftOffset ->
                 if (clipping == null) return@combine null
                 ShadeScrimShape(
                     bounds = clipping.bounds.minus(leftOffset = leftOffset),
                     topRadius = radius.takeIf { clipping.rounding.isTopRounded } ?: 0,
-                    bottomRadius = radius.takeIf { clipping.rounding.isBottomRounded } ?: 0
+                    bottomRadius = radius.takeIf { clipping.rounding.isBottomRounded } ?: 0,
                 )
             }
             .dumpWhileCollecting("shadeScrimShape")
@@ -209,10 +242,10 @@
 
     /** Whether the notification stack is scrollable or not. */
     val isScrollable: Flow<Boolean> =
-        sceneInteractor.currentScene
-            .map {
-                sceneInteractor.isSceneInFamily(it, SceneFamilies.NotifShade) ||
-                    it == Scenes.Lockscreen
+        combine(sceneInteractor.currentScene, sceneInteractor.currentOverlays) {
+                currentScene,
+                currentOverlays ->
+                currentScene.showsNotifications() || currentOverlays.any { it.showsNotifications() }
             }
             .dumpWhileCollecting("isScrollable")
 
@@ -242,13 +275,20 @@
         }
     }
 
+    private fun ContentKey.showsNotifications(): Boolean {
+        return when (this) {
+            Overlays.NotificationsShade,
+            Scenes.Lockscreen,
+            Scenes.Shade -> true
+            else -> false
+        }
+    }
+
     @AssistedFactory
     interface Factory {
         fun create(): NotificationScrollViewModel
     }
 }
 
-private fun ChangeScene.isBetween(
-    a: (SceneKey) -> Boolean,
-    b: (SceneKey) -> Boolean,
-): Boolean = (a(fromScene) && b(toScene)) || (b(fromScene) && a(toScene))
+private fun ChangeScene.isBetween(a: (SceneKey) -> Boolean, b: (SceneKey) -> Boolean): Boolean =
+    (a(fromScene) && b(toScene)) || (b(fromScene) && a(toScene))
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
index 45aee5b..a658115 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
@@ -30,6 +30,7 @@
 import android.view.WindowInsets;
 
 import androidx.annotation.VisibleForTesting;
+
 import com.android.compose.animation.scene.ObservableTransitionState;
 import com.android.internal.policy.SystemBarUtils;
 import com.android.systemui.Dumpable;
@@ -69,7 +70,9 @@
     private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
 
     private boolean mIsStatusBarExpanded = false;
-    private boolean mIsIdleOnGone = true;
+    // Whether the scene container has no UI to render, i.e. is in idle state on the Gone scene and
+    // without any overlays to display.
+    private boolean mIsSceneContainerUiEmpty = true;
     private boolean mIsRemoteUserInteractionOngoing = false;
     private boolean mShouldAdjustInsets = false;
     private View mNotificationShadeWindowView;
@@ -134,7 +137,7 @@
         if (SceneContainerFlag.isEnabled()) {
             javaAdapter.alwaysCollectFlow(
                     sceneInteractor.get().getTransitionState(),
-                    this::onSceneChanged);
+                    this::onSceneContainerTransition);
             javaAdapter.alwaysCollectFlow(
                     sceneInteractor.get().isRemoteUserInteractionOngoing(),
                     this::onRemoteUserInteractionOngoingChanged);
@@ -172,11 +175,13 @@
         }
     }
 
-    private void onSceneChanged(ObservableTransitionState transitionState) {
-        boolean isIdleOnGone = transitionState.isIdle(Scenes.Gone);
-        if (isIdleOnGone != mIsIdleOnGone) {
-            mIsIdleOnGone = isIdleOnGone;
-            if (!isIdleOnGone) {
+    private void onSceneContainerTransition(ObservableTransitionState transitionState) {
+        boolean isSceneContainerUiEmpty = transitionState.isIdle(Scenes.Gone)
+                && ((ObservableTransitionState.Idle) transitionState).getCurrentOverlays()
+                .isEmpty();
+        if (isSceneContainerUiEmpty != mIsSceneContainerUiEmpty) {
+            mIsSceneContainerUiEmpty = isSceneContainerUiEmpty;
+            if (!isSceneContainerUiEmpty) {
                 // make sure our state is sensible
                 mForceCollapsedUntilLayout = false;
             }
@@ -296,7 +301,7 @@
         // underneath.
         return mIsStatusBarExpanded
                 || (SceneContainerFlag.isEnabled()
-                && (!mIsIdleOnGone || mIsRemoteUserInteractionOngoing))
+                && (!mIsSceneContainerUiEmpty || mIsRemoteUserInteractionOngoing))
                 || mPrimaryBouncerInteractor.isShowing().getValue()
                 || mAlternateBouncerInteractor.isVisibleState()
                 || mUnlockedScreenOffAnimationController.isAnimationPlaying();