Merge "[Flexiglass] Set the top HUN height on the HeadsUpPlaceholder" into main
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationHeadsUpHeight.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationHeadsUpHeight.kt
new file mode 100644
index 0000000..75a565b
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationHeadsUpHeight.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.notifications.ui.composable
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.invalidateMeasurement
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
+
+/**
+ * Modify element, which updates the height to the height of current top heads up notification, or
+ * to 0 if there is none.
+ *
+ * @param view Notification stack scroll view
+ */
+fun Modifier.notificationHeadsUpHeight(view: NotificationScrollView) =
+    this then HeadsUpLayoutElement(view)
+
+private data class HeadsUpLayoutElement(
+    val view: NotificationScrollView,
+) : ModifierNodeElement<HeadsUpLayoutNode>() {
+
+    override fun create(): HeadsUpLayoutNode = HeadsUpLayoutNode(view)
+
+    override fun update(node: HeadsUpLayoutNode) {
+        check(view == node.view) { "Trying to reuse the node with a new View." }
+    }
+}
+
+private class HeadsUpLayoutNode(val view: NotificationScrollView) :
+    LayoutModifierNode, Modifier.Node() {
+
+    private val headsUpHeightChangedListener = Runnable { invalidateMeasureIfAttached() }
+
+    override fun onAttach() {
+        super.onAttach()
+        view.addHeadsUpHeightChangedListener(headsUpHeightChangedListener)
+    }
+
+    override fun onDetach() {
+        super.onDetach()
+        view.removeHeadsUpHeightChangedListener(headsUpHeightChangedListener)
+    }
+
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        // TODO(b/339181697) make sure, that the row is already measured.
+        val contentHeight = view.topHeadsUpHeight
+        val placeable =
+            measurable.measure(
+                constraints.copy(minHeight = contentHeight, maxHeight = contentHeight)
+            )
+        return layout(placeable.width, placeable.height) { placeable.place(IntOffset.Zero) }
+    }
+
+    override fun toString(): String {
+        return "HeadsUpLayoutNode(view=$view)"
+    }
+
+    fun invalidateMeasureIfAttached() {
+        if (isAttached) {
+            this.invalidateMeasurement()
+        }
+    }
+}
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 e6132c6..c26259f 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
@@ -67,7 +67,6 @@
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.NestedScrollBehavior
 import com.android.compose.animation.scene.SceneScope
-import com.android.compose.modifiers.height
 import com.android.compose.modifiers.thenIf
 import com.android.systemui.common.ui.compose.windowinsets.LocalRawScreenHeight
 import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
@@ -108,18 +107,17 @@
  */
 @Composable
 fun SceneScope.HeadsUpNotificationSpace(
+    stackScrollView: NotificationScrollView,
     viewModel: NotificationsPlaceholderViewModel,
     modifier: Modifier = Modifier,
     isPeekFromBottom: Boolean = false,
 ) {
-    val headsUpHeight = viewModel.headsUpHeight.collectAsStateWithLifecycle()
-
     Element(
         Notifications.Elements.HeadsUpNotificationPlaceholder,
         modifier =
             modifier
-                .height { headsUpHeight.value.roundToInt() }
                 .fillMaxWidth()
+                .notificationHeadsUpHeight(stackScrollView)
                 .debugBackground(viewModel, DEBUG_HUN_COLOR)
                 .onGloballyPositioned { coordinates: LayoutCoordinates ->
                     val boundsInWindow = coordinates.boundsInWindow()
@@ -152,6 +150,7 @@
             modifier = Modifier.fillMaxSize(),
         )
         HeadsUpNotificationSpace(
+            stackScrollView = stackScrollView,
             viewModel = viewModel,
             modifier = Modifier.align(Alignment.TopCenter),
         )
@@ -358,7 +357,7 @@
                         .onSizeChanged { size -> stackHeight.intValue = size.height },
             )
         }
-        HeadsUpNotificationSpace(viewModel = viewModel)
+        HeadsUpNotificationSpace(stackScrollView = stackScrollView, viewModel = viewModel)
     }
 }
 
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 d76b19f..0ee485c 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
@@ -42,7 +42,6 @@
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.navigationBars
-import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.foundation.rememberScrollState
@@ -60,7 +59,6 @@
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.res.colorResource
-import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.animation.scene.SceneScope
@@ -79,14 +77,13 @@
 import com.android.systemui.media.controls.ui.controller.MediaCarouselController
 import com.android.systemui.media.controls.ui.view.MediaHost
 import com.android.systemui.media.dagger.MediaModule
-import com.android.systemui.notifications.ui.composable.NotificationScrollingStack
+import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace
 import com.android.systemui.qs.footer.ui.compose.FooterActionsWithAnimatedVisibility
 import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel
 import com.android.systemui.res.R
 import com.android.systemui.scene.session.ui.composable.SaveableSession
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.ui.composable.ComposableScene
-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
@@ -99,7 +96,6 @@
 import dagger.Lazy
 import javax.inject.Inject
 import javax.inject.Named
-import kotlin.math.roundToInt
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.stateIn
@@ -368,15 +364,11 @@
                     Modifier.align(Alignment.CenterHorizontally).sysuiResTag("qs_footer_actions"),
             )
         }
-        NotificationScrollingStack(
+        HeadsUpNotificationSpace(
             stackScrollView = notificationStackScrollView,
             viewModel = notificationsPlaceholderViewModel,
-            shadeSession = shadeSession,
-            maxScrimTop = { screenHeight },
-            shouldPunchHoleBehindScrim = shouldPunchHoleBehindScrim,
-            shadeMode = ShadeMode.Single,
-            modifier =
-                Modifier.fillMaxWidth().offset { IntOffset(x = 0, y = screenHeight.roundToInt()) },
+            modifier = Modifier.align(Alignment.BottomCenter),
+            isPeekFromBottom = true,
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index edd2961..99ce454 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -2843,14 +2843,10 @@
         mIsSystemChildExpanded = expanded;
     }
 
-    public void setLayoutListener(LayoutListener listener) {
+    public void setLayoutListener(@Nullable LayoutListener listener) {
         mLayoutListener = listener;
     }
 
-    public void removeListener() {
-        mLayoutListener = null;
-    }
-
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         Trace.beginSection(appendTraceStyleTag("ExpNotRow#onLayout"));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
index c10c09c..bdfbc4b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
@@ -242,7 +242,7 @@
     public void onLayout() {
         mIconsPlaced = false; // Force icons to be re-placed
         setMenuLocation();
-        mParent.removeListener();
+        mParent.setLayoutListener(null);
     }
 
     private void createMenuViews(boolean resetState) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index a9d7cc0..84b7478 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -255,7 +255,8 @@
      * The raw amount of the overScroll on the bottom, which is not rubber-banded.
      */
     private float mOverScrolledBottomPixels;
-    private ListenerSet<Runnable> mStackHeightChangedListeners = new ListenerSet<>();
+    private final ListenerSet<Runnable> mStackHeightChangedListeners = new ListenerSet<>();
+    private final ListenerSet<Runnable> mHeadsUpHeightChangedListeners = new ListenerSet<>();
     private NotificationLogger.OnChildLocationsChangedListener mListener;
     private OnNotificationLocationsChangedListener mLocationsChangedListener;
     private OnOverscrollTopChangedListener mOverscrollTopChangedListener;
@@ -1114,6 +1115,28 @@
         mStackHeightChangedListeners.remove(runnable);
     }
 
+    private void notifyHeadsUpHeightChangedForView(View view) {
+        if (mTopHeadsUpRow == view) {
+            notifyHeadsUpHeightChangedListeners();
+        }
+    }
+
+    private void notifyHeadsUpHeightChangedListeners() {
+        for (Runnable listener : mHeadsUpHeightChangedListeners) {
+            listener.run();
+        }
+    }
+
+    @Override
+    public void addHeadsUpHeightChangedListener(@NonNull Runnable runnable) {
+        mHeadsUpHeightChangedListeners.addIfAbsent(runnable);
+    }
+
+    @Override
+    public void removeHeadsUpHeightChangedListener(@NonNull Runnable runnable) {
+        mHeadsUpHeightChangedListeners.remove(runnable);
+    }
+
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
         if (!mSuppressChildrenMeasureAndLayout) {
@@ -2444,6 +2467,11 @@
         return mScrollViewFields.getIntrinsicStackHeight();
     }
 
+    @Override
+    public int getTopHeadsUpHeight() {
+        return getTopHeadsUpPinnedHeight();
+    }
+
     /**
      * Calculate the gap height between two different views
      *
@@ -4193,12 +4221,14 @@
             requestAnimationOnViewResize(row);
         }
         requestChildrenUpdate();
+        notifyHeadsUpHeightChangedForView(view);
         mAnimateStackYForContentHeightChange = previouslyNeededAnimation;
     }
 
     void onChildHeightReset(ExpandableView view) {
         updateAnimationState(view);
         updateChronometerForChild(view);
+        notifyHeadsUpHeightChangedForView(view);
     }
 
     private void updateScrollPositionOnExpandInBottom(ExpandableView view) {
@@ -5573,6 +5603,7 @@
      */
     public void setTopHeadsUpRow(@Nullable ExpandableNotificationRow topHeadsUpRow) {
         mTopHeadsUpRow = topHeadsUpRow;
+        notifyHeadsUpHeightChangedListeners();
     }
 
     public boolean getIsExpanded() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt
index 463c631..f6d9351 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt
@@ -27,9 +27,6 @@
 @SysUISingleton
 class NotificationViewHeightRepository @Inject constructor() {
 
-    /** The height in px of the current heads up notification. */
-    val headsUpHeight = MutableStateFlow(0f)
-
     /**
      * The amount in px that the notification stack should scroll due to internal expansion. This
      * should only happen when a notification expansion hits the bottom of the screen, so it is
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
index e7acbe3..afcf3ae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
@@ -65,9 +65,6 @@
             }
             .distinctUntilChanged()
 
-    /** The height in px of the contents of the HUN. */
-    val headsUpHeight: StateFlow<Float> = viewHeightRepository.headsUpHeight.asStateFlow()
-
     /** The alpha of the Notification Stack for the brightness mirror */
     val alphaForBrightnessMirror: StateFlow<Float> =
         placeholderRepository.alphaForBrightnessMirror.asStateFlow()
@@ -110,11 +107,6 @@
         placeholderRepository.shadeScrimBounds.value = bounds
     }
 
-    /** Sets the height of heads up notification. */
-    fun setHeadsUpHeight(height: Float) {
-        viewHeightRepository.headsUpHeight.value = height
-    }
-
     /** Sets whether the notification stack is scrolled to the top. */
     fun setScrolledToTop(scrolledToTop: Boolean) {
         placeholderRepository.scrolledToTop.value = scrolledToTop
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
index 14b882f..eaaa9a1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
@@ -32,6 +32,9 @@
      */
     val intrinsicStackHeight: Int
 
+    /** Height in pixels required to display the top HeadsUp Notification. */
+    val topHeadsUpHeight: Int
+
     /**
      * Since this is an interface rather than a literal View, this provides cast-like access to the
      * underlying view.
@@ -72,9 +75,18 @@
     /** Sets whether the view is displayed in doze mode. */
     fun setDozing(dozing: Boolean)
 
-    /** Sets a listener to be notified, when the stack height might have changed. */
+    /** Adds a listener to be notified, when the stack height might have changed. */
     fun addStackHeightChangedListener(runnable: Runnable)
 
     /** @see addStackHeightChangedListener */
     fun removeStackHeightChangedListener(runnable: Runnable)
+
+    /**
+     * Adds a listener to be notified, when the height of the top heads up notification might have
+     * changed.
+     */
+    fun addHeadsUpHeightChangedListener(runnable: Runnable)
+
+    /** @see addHeadsUpHeightChangedListener */
+    fun removeHeadsUpHeightChangedListener(runnable: Runnable)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
index 622d8e7..fd08e89 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
@@ -80,7 +80,6 @@
 
         launch { viewModel.maxAlpha.collect { view.setMaxAlpha(it) } }
         launch { viewModel.scrolledToTop.collect { view.setScrolledToTop(it) } }
-        launch { viewModel.headsUpTop.collect { view.setHeadsUpTop(it) } }
         launch { viewModel.expandFraction.collect { view.setExpandFraction(it.coerceIn(0f, 1f)) } }
         launch { viewModel.isScrollable.collect { view.setScrollingEnabled(it) } }
         launch { viewModel.isDozing.collect { isDozing -> view.setDozing(isDozing) } }
@@ -88,11 +87,9 @@
         launchAndDispose {
             view.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer)
             view.setCurrentGestureOverscrollConsumer(viewModel.currentGestureOverscrollConsumer)
-            view.setHeadsUpHeightConsumer(viewModel.headsUpHeightConsumer)
             DisposableHandle {
                 view.setSyntheticScrollConsumer(null)
                 view.setCurrentGestureOverscrollConsumer(null)
-                view.setHeadsUpHeightConsumer(null)
             }
         }
     }
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 c2ce114..a99fbfc 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
@@ -150,8 +150,6 @@
      */
     val currentGestureOverscrollConsumer: (Boolean) -> Unit =
         stackAppearanceInteractor::setCurrentGestureOverscroll
-    /** Receives the height of the heads up notification. */
-    val headsUpHeightConsumer: (Float) -> Unit = stackAppearanceInteractor::setHeadsUpHeight
 
     /** Whether the notification stack is scrollable or not. */
     val isScrollable: Flow<Boolean> =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
index 97b86e3..ea33be0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
@@ -29,7 +29,6 @@
 import com.android.systemui.util.kotlin.FlowDumperImpl
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.StateFlow
 
 /**
  * ViewModel used by the Notification placeholders inside the scene container to update the
@@ -74,9 +73,6 @@
     val shadeScrimRounding: Flow<ShadeScrimRounding> =
         interactor.shadeScrimRounding.dumpWhileCollecting("shadeScrimRounding")
 
-    /** The height in px of the contents of the HUN. */
-    val headsUpHeight: StateFlow<Float> = interactor.headsUpHeight.dumpValue("headsUpHeight")
-
     /**
      * The amount [0-1] that the shade or quick settings has been opened. At 0, the shade is closed;
      * at 1, either the shade or quick settings is open.