Merge "[bc25] Fix notifications shade rendering over lockscreen in Dual Shade." into main
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 2745f6e..4c6834c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -47,7 +47,6 @@
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.LaunchedEffect
@@ -60,7 +59,6 @@
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
 import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.graphics.BlendMode
 import androidx.compose.ui.graphics.Color
@@ -70,7 +68,6 @@
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.layout.boundsInWindow
 import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.layout.onPlaced
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.layout.positionInWindow
 import androidx.compose.ui.platform.LocalConfiguration
@@ -82,6 +79,7 @@
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.util.lerp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.LowestZIndexContentPicker
 import com.android.compose.animation.scene.NestedScrollBehavior
@@ -93,8 +91,9 @@
 import com.android.systemui.res.R
 import com.android.systemui.scene.session.ui.composable.SaveableSession
 import com.android.systemui.scene.session.ui.composable.rememberSession
+import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.shade.shared.flag.DualShade
 import com.android.systemui.shade.ui.composable.ShadeHeader
 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds
 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimRounding
@@ -112,18 +111,16 @@
         val NotificationStackPlaceholder = ElementKey("NotificationStackPlaceholder")
         val HeadsUpNotificationPlaceholder =
             ElementKey("HeadsUpNotificationPlaceholder", contentPicker = LowestZIndexContentPicker)
-        val ShelfSpace = ElementKey("ShelfSpace")
         val NotificationStackCutoffGuideline = ElementKey("NotificationStackCutoffGuideline")
     }
-
-    // Expansion fraction thresholds (between 0-1f) at which the corresponding value should be
-    // at its maximum, given they are at their minimum value at expansion = 0f.
-    object TransitionThresholds {
-        const val EXPANSION_FOR_MAX_CORNER_RADIUS = 0.1f
-        const val EXPANSION_FOR_MAX_SCRIM_ALPHA = 0.3f
-    }
 }
 
+private val notificationsShadeContentKey: ContentKey
+    get() = if (DualShade.isEnabled) Overlays.NotificationsShade else Scenes.Shade
+
+private val quickSettingsShadeContentKey: ContentKey
+    get() = if (DualShade.isEnabled) Overlays.QuickSettingsShade else Scenes.QuickSettings
+
 /**
  * Adds the space where heads up notifications can appear in the scene. This should generally be the
  * entire size of the scene.
@@ -146,7 +143,7 @@
                     // This element is sometimes opted out of the shared element system, so there
                     // can be multiple instances of it during a transition. Thus we need to
                     // determine which instance should feed its bounds to NSSL to avoid providing
-                    // conflicting values
+                    // conflicting values.
                     val useBounds = useHunBounds()
                     if (useBounds) {
                         val positionInWindow = coordinates.positionInWindow()
@@ -157,8 +154,8 @@
                                 " bounds=$boundsInWindow"
                         }
                         // Note: boundsInWindow doesn't scroll off the screen, so use
-                        // positionInWindow
-                        // for top bound, which can scroll off screen while snoozing
+                        // positionInWindow for top bound, which can scroll off screen while
+                        // snoozing.
                         stackScrollView.setHeadsUpTop(positionInWindow.y)
                         stackScrollView.setHeadsUpBottom(boundsInWindow.bottom)
                     }
@@ -285,7 +282,8 @@
     shouldFillMaxSize: Boolean = true,
     shouldReserveSpaceForNavBar: Boolean = true,
     shouldIncludeHeadsUpSpace: Boolean = true,
-    shadeMode: ShadeMode,
+    shouldShowScrim: Boolean = true,
+    supportNestedScrolling: Boolean,
     onEmptySpaceClick: (() -> Unit)? = null,
     modifier: Modifier = Modifier,
 ) {
@@ -293,6 +291,7 @@
     val density = LocalDensity.current
     val screenCornerRadius = LocalScreenCornerRadius.current
     val scrimCornerRadius = dimensionResource(R.dimen.notification_scrim_corner_radius)
+    val scrimBackgroundColor = MaterialTheme.colorScheme.surface
     val scrollState =
         shadeSession.rememberSaveableSession(saver = ScrollState.Saver, key = null) {
             ScrollState(initial = 0)
@@ -427,8 +426,14 @@
                     // completes.
                     if (
                         scrimOffset.value < 0 &&
-                            layoutState.isTransitioning(from = Scenes.Shade, to = Scenes.Gone) ||
-                            layoutState.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen)
+                            (layoutState.isTransitioning(
+                                from = notificationsShadeContentKey,
+                                to = Scenes.Gone,
+                            ) ||
+                                layoutState.isTransitioning(
+                                    from = notificationsShadeContentKey,
+                                    to = Scenes.Lockscreen,
+                                ))
                     ) {
                         IntOffset(x = 0, y = (scrimOffset.value * expansionFraction).roundToInt())
                     } else if (
@@ -498,7 +503,7 @@
                                 (expansionFraction / EXPANSION_FOR_MAX_SCRIM_ALPHA).coerceAtMost(1f)
                             } else 1f
                     }
-                    .background(MaterialTheme.colorScheme.surface)
+                    .thenIf(shouldShowScrim) { Modifier.background(scrimBackgroundColor) }
                     .thenIf(shouldFillMaxSize) { Modifier.fillMaxSize() }
                     .debugBackground(viewModel, DEBUG_BOX_COLOR)
         ) {
@@ -508,7 +513,7 @@
                             topBehavior = NestedScrollBehavior.EdgeWithPreview,
                             isExternalOverscrollGesture = { isCurrentGestureOverscroll.value },
                         )
-                        .thenIf(shadeMode == ShadeMode.Single) {
+                        .thenIf(supportNestedScrolling) {
                             Modifier.nestedScroll(scrimNestedScrollConnection)
                         }
                         .stackVerticalOverscroll(coroutineScope) { scrollState.canScrollForward }
@@ -550,38 +555,6 @@
 }
 
 /**
- * This may be added to the lockscreen to provide a space to the start of the lock icon where the
- * short shelf has room to flow vertically below the lock icon, but to its start, allowing more
- * notifications to fit in the stack itself. (see: b/213934746)
- *
- * NOTE: this is totally unused for now; it is here to clarify the future plan
- */
-@Composable
-fun SceneScope.NotificationShelfSpace(
-    viewModel: NotificationsPlaceholderViewModel,
-    modifier: Modifier = Modifier,
-) {
-    Text(
-        text = "Shelf Space",
-        modifier
-            .element(key = Notifications.Elements.ShelfSpace)
-            .fillMaxWidth()
-            .onPlaced { coordinates: LayoutCoordinates ->
-                debugLog(viewModel) {
-                    ("SHELF onPlaced:" +
-                        " size=${coordinates.size}" +
-                        " bounds=${coordinates.boundsInWindow()}")
-                }
-            }
-            .clip(RoundedCornerShape(24.dp))
-            .background(MaterialTheme.colorScheme.primaryContainer)
-            .padding(16.dp),
-        style = MaterialTheme.typography.titleLarge,
-        color = MaterialTheme.colorScheme.onPrimaryContainer,
-    )
-}
-
-/**
  * A 0 height horizontal spacer to be placed at the bottom-most position in the current scene, where
  * the notification contents (stack, footer, shelf) should be drawn.
  */
@@ -673,15 +646,19 @@
     }
 }
 
+private fun TransitionState.isOnLockscreen(): Boolean {
+    return currentScene == Scenes.Lockscreen && currentOverlays.isEmpty()
+}
+
 private fun shouldUseLockscreenStackBounds(state: TransitionState): Boolean {
-    return state is TransitionState.Idle && state.currentScene == Scenes.Lockscreen
+    return state is TransitionState.Idle && state.isOnLockscreen()
 }
 
 private fun shouldUseLockscreenHunBounds(state: TransitionState): Boolean {
     return when (state) {
-        is TransitionState.Idle -> state.currentScene == Scenes.Lockscreen
+        is TransitionState.Idle -> state.isOnLockscreen()
         is TransitionState.Transition ->
-            state.isTransitioning(from = Scenes.QuickSettings, to = Scenes.Lockscreen)
+            state.isTransitioning(from = quickSettingsShadeContentKey, to = Scenes.Lockscreen)
     }
 }
 
@@ -690,7 +667,7 @@
     shouldPunchHoleBehindScrim: Boolean,
 ): Boolean {
     return shouldPunchHoleBehindScrim ||
-        state.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen)
+        state.isTransitioning(from = notificationsShadeContentKey, to = Scenes.Lockscreen)
 }
 
 private fun calculateCornerRadius(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
index a22becc..5b99670 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
@@ -33,7 +33,6 @@
 import com.android.systemui.scene.session.ui.composable.SaveableSession
 import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.ui.composable.Overlay
-import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.shade.ui.composable.ExpandedShadeHeader
 import com.android.systemui.shade.ui.composable.OverlayShade
 import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
@@ -69,9 +68,7 @@
     }
 
     @Composable
-    override fun ContentScope.Content(
-        modifier: Modifier,
-    ) {
+    override fun ContentScope.Content(modifier: Modifier) {
         val viewModel =
             rememberViewModel("NotificationsShadeOverlay-viewModel") {
                 contentViewModelFactory.create()
@@ -81,10 +78,7 @@
                 viewModel.notificationsPlaceholderViewModelFactory.create()
             }
 
-        OverlayShade(
-            modifier = modifier,
-            onScrimClicked = viewModel::onScrimClicked,
-        ) {
+        OverlayShade(modifier = modifier, onScrimClicked = viewModel::onScrimClicked) {
             Column {
                 ExpandedShadeHeader(
                     viewModelFactory = viewModel.shadeHeaderViewModelFactory,
@@ -102,7 +96,8 @@
                     shouldPunchHoleBehindScrim = false,
                     shouldFillMaxSize = false,
                     shouldReserveSpaceForNavBar = false,
-                    shadeMode = ShadeMode.Dual,
+                    shouldShowScrim = false,
+                    supportNestedScrolling = false,
                     modifier = Modifier.fillMaxWidth(),
                 )
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index 6304979..e27f46b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -100,7 +100,6 @@
 import com.android.systemui.scene.session.ui.composable.SaveableSession
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.ui.composable.Scene
-import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.shade.ui.composable.CollapsedShadeHeader
 import com.android.systemui.shade.ui.composable.ExpandedShadeHeader
 import com.android.systemui.shade.ui.composable.Shade
@@ -114,11 +113,9 @@
 import javax.inject.Inject
 import javax.inject.Named
 import kotlin.math.roundToInt
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 
 /** The Quick Settings (AKA "QS") scene shows the quick setting tiles. */
-@OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class QuickSettingsScene
 @Inject
@@ -427,7 +424,7 @@
             maxScrimTop = { screenHeight },
             shouldPunchHoleBehindScrim = shouldPunchHoleBehindScrim,
             shouldIncludeHeadsUpSpace = false,
-            shadeMode = ShadeMode.Single,
+            supportNestedScrolling = true,
             modifier =
                 Modifier.fillMaxWidth()
                     .offset { IntOffset(x = 0, y = screenHeight.roundToInt()) }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt
index 337f53a5..23c4f12 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt
@@ -21,26 +21,20 @@
 import androidx.compose.animation.core.tween
 import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.TransitionBuilder
-import com.android.compose.animation.scene.UserActionDistance
 import com.android.systemui.notifications.ui.composable.Notifications
 import com.android.systemui.shade.ui.composable.OverlayShade
 import com.android.systemui.shade.ui.composable.Shade
 import com.android.systemui.shade.ui.composable.ShadeHeader
 import kotlin.time.Duration.Companion.milliseconds
 
-fun TransitionBuilder.toNotificationsShadeTransition(
-    durationScale: Double = 1.0,
-) {
+fun TransitionBuilder.toNotificationsShadeTransition(durationScale: Double = 1.0) {
     spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
     swipeSpec =
         spring(
             stiffness = Spring.StiffnessMediumLow,
             visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
         )
-    distance = UserActionDistance { fromSceneSize, orientation ->
-        fromSceneSize.height.toFloat() * 2 / 3f
-    }
-
+    scaleSize(OverlayShade.Elements.Panel, height = 0f)
     translate(OverlayShade.Elements.Panel, Edge.Top)
 
     fractionRange(end = .5f) { fade(OverlayShade.Elements.Scrim) }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index c6c42fc..3ec057b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -40,7 +40,6 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.systemBars
 import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
@@ -89,7 +88,6 @@
 import com.android.systemui.media.controls.ui.controller.MediaCarouselController
 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
 import com.android.systemui.media.controls.ui.view.MediaHost
-import com.android.systemui.media.controls.ui.view.MediaHostState
 import com.android.systemui.media.controls.ui.view.MediaHostState.Companion.COLLAPSED
 import com.android.systemui.media.controls.ui.view.MediaHostState.Companion.EXPANDED
 import com.android.systemui.media.dagger.MediaModule.QS_PANEL
@@ -117,7 +115,6 @@
 import javax.inject.Inject
 import javax.inject.Named
 import kotlin.math.roundToInt
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 
 object Shade {
@@ -128,23 +125,13 @@
     }
 
     object Dimensions {
-        val ScrimCornerSize = 32.dp
         val HorizontalPadding = 16.dp
         val ScrimOverscrollLimit = 32.dp
         const val ScrimVisibilityThreshold = 5f
     }
-
-    object Shapes {
-        val Scrim =
-            RoundedCornerShape(
-                topStart = Dimensions.ScrimCornerSize,
-                topEnd = Dimensions.ScrimCornerSize,
-            )
-    }
 }
 
 /** The shade scene shows scrolling list of notifications and some of the quick setting tiles. */
-@OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class ShadeScene
 @Inject
@@ -197,11 +184,11 @@
         )
 
     init {
-        qqsMediaHost.expansion = MediaHostState.EXPANDED
+        qqsMediaHost.expansion = EXPANDED
         qqsMediaHost.showsOnlyActiveMedia = true
         qqsMediaHost.init(MediaHierarchyManager.LOCATION_QQS)
 
-        qsMediaHost.expansion = MediaHostState.EXPANDED
+        qsMediaHost.expansion = EXPANDED
         qsMediaHost.showsOnlyActiveMedia = false
         qsMediaHost.init(MediaHierarchyManager.LOCATION_QS)
     }
@@ -329,8 +316,7 @@
         modifier =
             modifier.thenIf(shouldPunchHoleBehindScrim) {
                 // Render the scene to an offscreen buffer so that BlendMode.DstOut only clears this
-                // scene
-                // (and not the one under it) during a scene transition.
+                // scene (and not the one under it) during a scene transition.
                 Modifier.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
             }
     ) {
@@ -382,8 +368,8 @@
                     stackScrollView = notificationStackScrollView,
                     viewModel = notificationsPlaceholderViewModel,
                     maxScrimTop = { maxNotifScrimTop.toFloat() },
-                    shadeMode = ShadeMode.Single,
                     shouldPunchHoleBehindScrim = shouldPunchHoleBehindScrim,
+                    supportNestedScrolling = true,
                     onEmptySpaceClick =
                         viewModel::onEmptySpaceClicked.takeIf { isEmptySpaceClickable },
                     modifier =
@@ -601,7 +587,7 @@
                     maxScrimTop = { 0f },
                     shouldPunchHoleBehindScrim = false,
                     shouldReserveSpaceForNavBar = false,
-                    shadeMode = ShadeMode.Split,
+                    supportNestedScrolling = false,
                     onEmptySpaceClick =
                         viewModel::onEmptySpaceClicked.takeIf { isEmptySpaceClickable },
                     modifier =
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 c9eaec7..aec81b0 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
@@ -84,9 +84,9 @@
     private fun fullyExpandedDuringSceneChange(change: ChangeScene): Boolean {
         // The lockscreen stack is visible during all transitions away from the lockscreen, so keep
         // the stack expanded until those transitions finish.
-        return if (change.isFrom({ it == Scenes.Lockscreen }, to = { true })) {
+        return if (change.isTransitioning(from = Scenes.Lockscreen)) {
             true
-        } else if (change.isFrom({ it == Scenes.Shade }, to = { it == Scenes.Lockscreen })) {
+        } else if (change.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen)) {
             false
         } else {
             (expandedInScene(change.fromScene) && expandedInScene(change.toScene))
@@ -101,11 +101,11 @@
         return if (fullyExpandedDuringSceneChange(change)) {
             1f
         } else if (
-            change.isBetween({ it == Scenes.Gone }, { it == Scenes.Shade }) ||
-                change.isFrom({ it == Scenes.Shade }, to = { it == Scenes.Lockscreen })
+            change.isTransitioningBetween(Scenes.Gone, Scenes.Shade) ||
+                change.isTransitioning(from = Scenes.Gone, to = Scenes.Lockscreen)
         ) {
             shadeExpansion
-        } else if (change.isBetween({ it == Scenes.Gone }, { it == Scenes.QuickSettings })) {
+        } else if (change.isTransitioningBetween(Scenes.Gone, Scenes.QuickSettings)) {
             // during QS expansion, increase fraction at same rate as scrim alpha,
             // but start when scrim alpha is at EXPANSION_FOR_DELAYED_STACK_FADE_IN.
             (qsExpansion / EXPANSION_FOR_MAX_SCRIM_ALPHA - EXPANSION_FOR_DELAYED_STACK_FADE_IN)
@@ -213,7 +213,11 @@
 
     private val qsAllowsClipping: Flow<Boolean> =
         combine(shadeInteractor.shadeMode, shadeInteractor.qsExpansion) { shadeMode, qsExpansion ->
-                qsExpansion < 0.5f || shadeMode != ShadeMode.Single
+                when (shadeMode) {
+                    is ShadeMode.Dual -> false
+                    is ShadeMode.Split -> true
+                    is ShadeMode.Single -> qsExpansion < 0.5f
+                }
             }
             .distinctUntilChanged()
 
@@ -325,9 +329,3 @@
         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.isFrom(from: (SceneKey) -> Boolean, to: (SceneKey) -> Boolean): Boolean =
-    from(fromScene) && to(toScene)