Merge "Fix AppFunctionRuntimeMedata.set/getEnabled" into main
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index bcad4633..b63f82b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -576,10 +576,6 @@
         mWindowDecorCaptionHandleRepository.notifyCaptionChanged(captionState);
     }
 
-    private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo) {
-        return taskInfo.isFreeform() && taskInfo.isResizeable;
-    }
-
     private void updateMaximizeMenu(SurfaceControl.Transaction startT) {
         if (!isDragResizable(mTaskInfo) || !isMaximizeMenuActive()) {
             return;
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
index f8d0588..8e6cb3f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
@@ -22,6 +22,7 @@
 import androidx.compose.animation.fadeOut
 import androidx.compose.animation.togetherWith
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
@@ -34,6 +35,7 @@
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.animation.scene.ContentScope
+import com.android.compose.animation.scene.SceneScope
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
 import com.android.systemui.battery.BatteryMeterViewController
@@ -41,6 +43,7 @@
 import com.android.systemui.compose.modifiers.sysuiResTag
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.qs.composefragment.ui.GridAnchor
 import com.android.systemui.qs.panels.ui.compose.EditMode
 import com.android.systemui.qs.panels.ui.compose.TileGrid
 import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel
@@ -79,16 +82,11 @@
     }
 
     @Composable
-    override fun ContentScope.Content(
-        modifier: Modifier,
-    ) {
+    override fun ContentScope.Content(modifier: Modifier) {
         val viewModel =
             rememberViewModel("QuickSettingsShadeOverlay") { contentViewModelFactory.create() }
 
-        OverlayShade(
-            modifier = modifier,
-            onScrimClicked = viewModel::onScrimClicked,
-        ) {
+        OverlayShade(modifier = modifier, onScrimClicked = viewModel::onScrimClicked) {
             Column {
                 ExpandedShadeHeader(
                     viewModelFactory = viewModel.shadeHeaderViewModelFactory,
@@ -98,40 +96,36 @@
                     modifier = Modifier.padding(QuickSettingsShade.Dimensions.Padding),
                 )
 
-                ShadeBody(
-                    viewModel = viewModel.quickSettingsContainerViewModel,
-                )
+                ShadeBody(viewModel = viewModel.quickSettingsContainerViewModel)
             }
         }
     }
 }
 
 @Composable
-fun ShadeBody(
-    viewModel: QuickSettingsContainerViewModel,
-) {
+fun SceneScope.ShadeBody(viewModel: QuickSettingsContainerViewModel) {
     val isEditing by viewModel.editModeViewModel.isEditing.collectAsStateWithLifecycle()
 
     AnimatedContent(
         targetState = isEditing,
-        transitionSpec = { fadeIn(tween(500)) togetherWith fadeOut(tween(500)) }
+        transitionSpec = { fadeIn(tween(500)) togetherWith fadeOut(tween(500)) },
     ) { editing ->
         if (editing) {
             EditMode(
                 viewModel = viewModel.editModeViewModel,
-                modifier = Modifier.fillMaxWidth().padding(QuickSettingsShade.Dimensions.Padding)
+                modifier = Modifier.fillMaxWidth().padding(QuickSettingsShade.Dimensions.Padding),
             )
         } else {
             QuickSettingsLayout(
                 viewModel = viewModel,
-                modifier = Modifier.sysuiResTag("quick_settings_panel")
+                modifier = Modifier.sysuiResTag("quick_settings_panel"),
             )
         }
     }
 }
 
 @Composable
-private fun QuickSettingsLayout(
+private fun SceneScope.QuickSettingsLayout(
     viewModel: QuickSettingsContainerViewModel,
     modifier: Modifier = Modifier,
 ) {
@@ -143,15 +137,18 @@
         BrightnessSliderContainer(
             viewModel = viewModel.brightnessSliderViewModel,
             modifier =
-                Modifier.fillMaxWidth()
-                    .height(QuickSettingsShade.Dimensions.BrightnessSliderHeight),
+                Modifier.fillMaxWidth().height(QuickSettingsShade.Dimensions.BrightnessSliderHeight),
         )
-        TileGrid(
-            viewModel = viewModel.tileGridViewModel,
-            modifier =
-                Modifier.fillMaxWidth().heightIn(max = QuickSettingsShade.Dimensions.GridMaxHeight),
-            viewModel.editModeViewModel::startEditing,
-        )
+        Box {
+            GridAnchor()
+            TileGrid(
+                viewModel = viewModel.tileGridViewModel,
+                modifier =
+                    Modifier.fillMaxWidth()
+                        .heightIn(max = QuickSettingsShade.Dimensions.GridMaxHeight),
+                viewModel.editModeViewModel::startEditing,
+            )
+        }
     }
 }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
index 7203b61..6f20e70 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
@@ -78,8 +78,6 @@
         Dispatchers.resetMain()
     }
 
-    // For now the state changes at 0.5f expansion. This will change once we implement animation
-    // (and this test will fail)
     @Test
     fun qsExpansionValueChanges_correctExpansionState() =
         with(kosmos) {
@@ -87,18 +85,27 @@
                 val expansionState by collectLastValue(underTest.expansionState)
 
                 underTest.qsExpansionValue = 0f
-                assertThat(expansionState)
-                    .isEqualTo(QSFragmentComposeViewModel.QSExpansionState.QQS)
+                assertThat(expansionState!!.progress).isEqualTo(0f)
 
                 underTest.qsExpansionValue = 0.3f
-                assertThat(expansionState)
-                    .isEqualTo(QSFragmentComposeViewModel.QSExpansionState.QQS)
-
-                underTest.qsExpansionValue = 0.7f
-                assertThat(expansionState).isEqualTo(QSFragmentComposeViewModel.QSExpansionState.QS)
+                assertThat(expansionState!!.progress).isEqualTo(0.3f)
 
                 underTest.qsExpansionValue = 1f
-                assertThat(expansionState).isEqualTo(QSFragmentComposeViewModel.QSExpansionState.QS)
+                assertThat(expansionState!!.progress).isEqualTo(1f)
+            }
+        }
+
+    @Test
+    fun qsExpansionValueChanges_clamped() =
+        with(kosmos) {
+            testScope.testWithinLifecycle {
+                val expansionState by collectLastValue(underTest.expansionState)
+
+                underTest.qsExpansionValue = -1f
+                assertThat(expansionState!!.progress).isEqualTo(0f)
+
+                underTest.qsExpansionValue = 2f
+                assertThat(expansionState!!.progress).isEqualTo(1f)
             }
         }
 
@@ -110,7 +117,7 @@
 
                 testableContext.orCreateTestableResources.addOverride(
                     R.bool.config_use_large_screen_shade_header,
-                    true
+                    true,
                 )
                 fakeConfigurationRepository.onConfigurationChange()
 
@@ -126,7 +133,7 @@
 
                 testableContext.orCreateTestableResources.addOverride(
                     R.bool.config_use_large_screen_shade_header,
-                    false
+                    false,
                 )
                 fakeConfigurationRepository.onConfigurationChange()
 
diff --git a/packages/SystemUI/src/com/android/systemui/grid/ui/compose/SpannedGrids.kt b/packages/SystemUI/src/com/android/systemui/grid/ui/compose/SpannedGrids.kt
new file mode 100644
index 0000000..62ab18b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/grid/ui/compose/SpannedGrids.kt
@@ -0,0 +1,321 @@
+/*
+ * 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.grid.ui.compose
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.semantics.CollectionInfo
+import androidx.compose.ui.semantics.CollectionItemInfo
+import androidx.compose.ui.semantics.collectionInfo
+import androidx.compose.ui.semantics.collectionItemInfo
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import kotlin.math.max
+
+/**
+ * Horizontal (non lazy) grid that supports [spans] for its elements.
+ *
+ * The elements will be laid down vertically first, and then by columns. So assuming LTR layout, it
+ * will be (for a span list `[2, 1, 2, 1, 1, 1, 1, 1]` and 4 rows):
+ * ```
+ * 0  2  5
+ * 0  2  6
+ * 1  3  7
+ *    4
+ * ```
+ *
+ * where repeated numbers show larger span. If an element doesn't fit in a column due to its span,
+ * it will start a new column.
+ *
+ * Elements in [spans] must be in the interval `[1, rows]` ([rows] > 0), and the composables are
+ * associated with the corresponding span based on their index.
+ *
+ * Due to the fact that elements are seen as a linear list that's laid out in a grid, the semantics
+ * represent the collection as a list of elements.
+ */
+@Composable
+fun HorizontalSpannedGrid(
+    rows: Int,
+    columnSpacing: Dp,
+    rowSpacing: Dp,
+    spans: List<Int>,
+    modifier: Modifier = Modifier,
+    composables: @Composable BoxScope.(spanIndex: Int) -> Unit,
+) {
+    SpannedGrid(
+        primarySpaces = rows,
+        crossAxisSpacing = rowSpacing,
+        mainAxisSpacing = columnSpacing,
+        spans = spans,
+        isVertical = false,
+        modifier = modifier,
+        composables = composables,
+    )
+}
+
+/**
+ * Horizontal (non lazy) grid that supports [spans] for its elements.
+ *
+ * The elements will be laid down horizontally first, and then by rows. So assuming LTR layout, it
+ * will be (for a span list `[2, 1, 2, 1, 1, 1, 1, 1]` and 4 columns):
+ * ```
+ * 0  0  1
+ * 2  2  3  4
+ * 5  6  7
+ * ```
+ *
+ * where repeated numbers show larger span. If an element doesn't fit in a row due to its span, it
+ * will start a new row.
+ *
+ * Elements in [spans] must be in the interval `[1, columns]` ([columns] > 0), and the composables
+ * are associated with the corresponding span based on their index.
+ *
+ * Due to the fact that elements are seen as a linear list that's laid out in a grid, the semantics
+ * represent the collection as a list of elements.
+ */
+@Composable
+fun VerticalSpannedGrid(
+    columns: Int,
+    columnSpacing: Dp,
+    rowSpacing: Dp,
+    spans: List<Int>,
+    modifier: Modifier = Modifier,
+    composables: @Composable BoxScope.(spanIndex: Int) -> Unit,
+) {
+    SpannedGrid(
+        primarySpaces = columns,
+        crossAxisSpacing = columnSpacing,
+        mainAxisSpacing = rowSpacing,
+        spans = spans,
+        isVertical = true,
+        modifier = modifier,
+        composables = composables,
+    )
+}
+
+@Composable
+private fun SpannedGrid(
+    primarySpaces: Int,
+    crossAxisSpacing: Dp,
+    mainAxisSpacing: Dp,
+    spans: List<Int>,
+    isVertical: Boolean,
+    modifier: Modifier = Modifier,
+    composables: @Composable BoxScope.(spanIndex: Int) -> Unit,
+) {
+    val crossAxisArrangement = Arrangement.spacedBy(crossAxisSpacing)
+    spans.forEachIndexed { index, span ->
+        check(span in 1..primarySpaces) {
+            "Span out of bounds. Span at index $index has value of $span which is outside of the " +
+                "expected rance of [1, $primarySpaces]"
+        }
+    }
+
+    if (isVertical) {
+        check(crossAxisSpacing >= 0.dp) { "Negative columnSpacing $crossAxisSpacing" }
+        check(mainAxisSpacing >= 0.dp) { "Negative rowSpacing $mainAxisSpacing" }
+    } else {
+        check(mainAxisSpacing >= 0.dp) { "Negative columnSpacing $mainAxisSpacing" }
+        check(crossAxisSpacing >= 0.dp) { "Negative rowSpacing $crossAxisSpacing" }
+    }
+
+    val totalMainAxisGroups: Int =
+        remember(primarySpaces, spans) {
+            var currentAccumulated = 0
+            var groups = 1
+            spans.forEach { span ->
+                if (currentAccumulated + span <= primarySpaces) {
+                    currentAccumulated += span
+                } else {
+                    groups += 1
+                    currentAccumulated = span
+                }
+            }
+            groups
+        }
+
+    val slotPositionsAndSizesCache = remember {
+        object {
+            var sizes = IntArray(0)
+            var positions = IntArray(0)
+        }
+    }
+
+    Layout(
+        {
+            (0 until spans.size).map { spanIndex ->
+                Box(
+                    Modifier.semantics {
+                        collectionItemInfo =
+                            if (isVertical) {
+                                CollectionItemInfo(spanIndex, 1, 0, 1)
+                            } else {
+                                CollectionItemInfo(0, 1, spanIndex, 1)
+                            }
+                    }
+                ) {
+                    composables(spanIndex)
+                }
+            }
+        },
+        modifier.semantics { collectionInfo = CollectionInfo(spans.size, 1) },
+    ) { measurables, constraints ->
+        check(measurables.size == spans.size)
+        val crossAxisSize = if (isVertical) constraints.maxWidth else constraints.maxHeight
+        check(crossAxisSize != Constraints.Infinity) { "Width must be constrained" }
+        if (slotPositionsAndSizesCache.sizes.size != primarySpaces) {
+            slotPositionsAndSizesCache.sizes = IntArray(primarySpaces)
+            slotPositionsAndSizesCache.positions = IntArray(primarySpaces)
+        }
+        calculateCellsCrossAxisSize(
+            crossAxisSize,
+            primarySpaces,
+            crossAxisSpacing.roundToPx(),
+            slotPositionsAndSizesCache.sizes,
+        )
+        val cellSizesInCrossAxis = slotPositionsAndSizesCache.sizes
+
+        // with is needed because of the double receiver (Density, Arrangement).
+        with(crossAxisArrangement) {
+            arrange(
+                crossAxisSize,
+                slotPositionsAndSizesCache.sizes,
+                LayoutDirection.Ltr,
+                slotPositionsAndSizesCache.positions,
+            )
+        }
+        val startPositions = slotPositionsAndSizesCache.positions
+
+        val mainAxisSpacingPx = mainAxisSpacing.roundToPx()
+        val mainAxisTotalGaps = (totalMainAxisGroups - 1) * mainAxisSpacingPx
+        val mainAxisSize = if (isVertical) constraints.maxHeight else constraints.maxWidth
+        val mainAxisElementConstraint =
+            if (mainAxisSize == Constraints.Infinity) {
+                Constraints.Infinity
+            } else {
+                max(0, (mainAxisSize - mainAxisTotalGaps) / totalMainAxisGroups)
+            }
+
+        val mainAxisSizes = IntArray(totalMainAxisGroups) { 0 }
+
+        var currentSlot = 0
+        var mainAxisGroup = 0
+        val placeables =
+            measurables.mapIndexed { index, measurable ->
+                val span = spans[index]
+                if (currentSlot + span > primarySpaces) {
+                    currentSlot = 0
+                    mainAxisGroup += 1
+                }
+                val crossAxisConstraint =
+                    calculateWidth(cellSizesInCrossAxis, startPositions, currentSlot, span)
+                PlaceResult(
+                        measurable.measure(
+                            makeConstraint(
+                                isVertical,
+                                mainAxisElementConstraint,
+                                crossAxisConstraint,
+                            )
+                        ),
+                        currentSlot,
+                        mainAxisGroup,
+                    )
+                    .also {
+                        currentSlot += span
+                        mainAxisSizes[mainAxisGroup] =
+                            max(
+                                mainAxisSizes[mainAxisGroup],
+                                if (isVertical) it.placeable.height else it.placeable.width,
+                            )
+                    }
+            }
+
+        val mainAxisTotalSize = mainAxisTotalGaps + mainAxisSizes.sum()
+        val mainAxisStartingPoints =
+            mainAxisSizes.runningFold(0) { acc, value -> acc + value + mainAxisSpacingPx }
+        val height = if (isVertical) mainAxisTotalSize else crossAxisSize
+        val width = if (isVertical) crossAxisSize else mainAxisTotalSize
+
+        layout(width, height) {
+            placeables.forEach { (placeable, slot, mainAxisGroup) ->
+                val x =
+                    if (isVertical) {
+                        startPositions[slot]
+                    } else {
+                        mainAxisStartingPoints[mainAxisGroup]
+                    }
+                val y =
+                    if (isVertical) {
+                        mainAxisStartingPoints[mainAxisGroup]
+                    } else {
+                        startPositions[slot]
+                    }
+                placeable.placeRelative(x, y)
+            }
+        }
+    }
+}
+
+fun makeConstraint(isVertical: Boolean, mainAxisSize: Int, crossAxisSize: Int): Constraints {
+    return if (isVertical) {
+        Constraints(maxHeight = mainAxisSize, minWidth = crossAxisSize, maxWidth = crossAxisSize)
+    } else {
+        Constraints(maxWidth = mainAxisSize, minHeight = crossAxisSize, maxHeight = crossAxisSize)
+    }
+}
+
+private fun calculateWidth(sizes: IntArray, positions: IntArray, startSlot: Int, span: Int): Int {
+    val crossAxisSize =
+        if (span == 1) {
+                sizes[startSlot]
+            } else {
+                val endSlot = startSlot + span - 1
+                positions[endSlot] + sizes[endSlot] - positions[startSlot]
+            }
+            .coerceAtLeast(0)
+    return crossAxisSize
+}
+
+private fun calculateCellsCrossAxisSize(
+    gridSize: Int,
+    slotCount: Int,
+    spacingPx: Int,
+    outArray: IntArray,
+) {
+    check(outArray.size == slotCount)
+    val gridSizeWithoutSpacing = gridSize - spacingPx * (slotCount - 1)
+    val slotSize = gridSizeWithoutSpacing / slotCount
+    val remainingPixels = gridSizeWithoutSpacing % slotCount
+    outArray.indices.forEach { index ->
+        outArray[index] = slotSize + if (index < remainingPixels) 1 else 0
+    }
+}
+
+private data class PlaceResult(
+    val placeable: Placeable,
+    val slotIndex: Int,
+    val mainAxisGroup: Int,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index af167d4..c174038 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -26,7 +26,6 @@
 import androidx.activity.OnBackPressedDispatcher
 import androidx.activity.OnBackPressedDispatcherOwner
 import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
-import androidx.compose.animation.AnimatedContent
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
@@ -38,10 +37,14 @@
 import androidx.compose.foundation.layout.windowInsetsPadding
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.layout.onPlaced
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.layout.positionInRoot
@@ -51,11 +54,18 @@
 import androidx.compose.ui.semantics.CustomAccessibilityAction
 import androidx.compose.ui.semantics.customActions
 import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.round
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.SceneTransitionLayout
+import com.android.compose.animation.scene.content.state.TransitionState
+import com.android.compose.animation.scene.transitions
 import com.android.compose.modifiers.height
 import com.android.compose.modifiers.padding
 import com.android.compose.modifiers.thenIf
@@ -70,11 +80,17 @@
 import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL
 import com.android.systemui.plugins.qs.QS
 import com.android.systemui.plugins.qs.QSContainerController
+import com.android.systemui.qs.composefragment.SceneKeys.QuickQuickSettings
+import com.android.systemui.qs.composefragment.SceneKeys.QuickSettings
+import com.android.systemui.qs.composefragment.SceneKeys.toIdleSceneKey
 import com.android.systemui.qs.composefragment.ui.notificationScrimClip
+import com.android.systemui.qs.composefragment.ui.quickQuickSettingsToQuickSettings
 import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel
 import com.android.systemui.qs.flags.QSComposeFragment
 import com.android.systemui.qs.footer.ui.compose.FooterActions
 import com.android.systemui.qs.panels.ui.compose.QuickQuickSettings
+import com.android.systemui.qs.shared.ui.ElementKeys
+import com.android.systemui.qs.ui.composable.QuickSettingsShade
 import com.android.systemui.qs.ui.composable.QuickSettingsTheme
 import com.android.systemui.qs.ui.composable.ShadeBody
 import com.android.systemui.res.R
@@ -86,11 +102,13 @@
 import java.util.function.Consumer
 import javax.inject.Inject
 import javax.inject.Named
+import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.awaitCancellation
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
 
 @SuppressLint("ValidFragment")
@@ -166,33 +184,48 @@
             setContent {
                 PlatformTheme {
                     val visible by viewModel.qsVisible.collectAsStateWithLifecycle()
-                    val qsState by viewModel.expansionState.collectAsStateWithLifecycle()
 
                     AnimatedVisibility(
                         visible = visible,
                         modifier =
-                            Modifier.windowInsetsPadding(WindowInsets.navigationBars).thenIf(
-                                notificationScrimClippingParams.isEnabled
-                            ) {
-                                Modifier.notificationScrimClip(
-                                    notificationScrimClippingParams.leftInset,
-                                    notificationScrimClippingParams.top,
-                                    notificationScrimClippingParams.rightInset,
-                                    notificationScrimClippingParams.bottom,
-                                    notificationScrimClippingParams.radius,
-                                )
-                            },
+                            Modifier.windowInsetsPadding(WindowInsets.navigationBars)
+                                .thenIf(notificationScrimClippingParams.isEnabled) {
+                                    Modifier.notificationScrimClip(
+                                        notificationScrimClippingParams.leftInset,
+                                        notificationScrimClippingParams.top,
+                                        notificationScrimClippingParams.rightInset,
+                                        notificationScrimClippingParams.bottom,
+                                        notificationScrimClippingParams.radius,
+                                    )
+                                }
+                                .graphicsLayer { elevation = 4.dp.toPx() },
                     ) {
-                        AnimatedContent(targetState = qsState) {
-                            when (it) {
-                                QSFragmentComposeViewModel.QSExpansionState.QQS -> {
-                                    QuickQuickSettingsElement()
-                                }
-                                QSFragmentComposeViewModel.QSExpansionState.QS -> {
-                                    QuickSettingsElement()
-                                }
-                                else -> {}
-                            }
+                        val sceneState = remember {
+                            MutableSceneTransitionLayoutState(
+                                viewModel.expansionState.value.toIdleSceneKey(),
+                                transitions =
+                                    transitions {
+                                        from(QuickQuickSettings, QuickSettings) {
+                                            quickQuickSettingsToQuickSettings()
+                                        }
+                                    },
+                            )
+                        }
+
+                        LaunchedEffect(Unit) {
+                            synchronizeQsState(
+                                sceneState,
+                                viewModel.expansionState.map { it.progress },
+                            )
+                        }
+
+                        SceneTransitionLayout(
+                            state = sceneState,
+                            modifier = Modifier.fillMaxSize(),
+                        ) {
+                            scene(QuickSettings) { QuickSettingsElement() }
+
+                            scene(QuickQuickSettings) { QuickQuickSettingsElement() }
                         }
                     }
                 }
@@ -420,7 +453,7 @@
     }
 
     @Composable
-    private fun QuickQuickSettingsElement() {
+    private fun SceneScope.QuickQuickSettingsElement() {
         val qqsPadding by viewModel.qqsHeaderHeight.collectAsStateWithLifecycle()
         val bottomPadding = dimensionResource(id = R.dimen.qqs_layout_padding_bottom)
         DisposableEffect(Unit) {
@@ -450,8 +483,15 @@
                         viewModel = viewModel.containerViewModel.quickQuickSettingsViewModel,
                         modifier =
                             Modifier.collapseExpandSemanticAction(
-                                stringResource(id = R.string.accessibility_quick_settings_expand)
-                            ),
+                                    stringResource(
+                                        id = R.string.accessibility_quick_settings_expand
+                                    )
+                                )
+                                .padding(
+                                    horizontal = {
+                                        QuickSettingsShade.Dimensions.Padding.roundToPx()
+                                    }
+                                ),
                     )
                 }
             }
@@ -460,7 +500,7 @@
     }
 
     @Composable
-    private fun QuickSettingsElement() {
+    private fun SceneScope.QuickSettingsElement() {
         val qqsPadding by viewModel.qqsHeaderHeight.collectAsStateWithLifecycle()
         val qsExtraPadding = dimensionResource(R.dimen.qs_panel_padding_top)
         Column(
@@ -471,7 +511,10 @@
         ) {
             val qsEnabled by viewModel.qsEnabled.collectAsStateWithLifecycle()
             if (qsEnabled) {
-                Box(modifier = Modifier.fillMaxSize().weight(1f)) {
+                Box(
+                    modifier =
+                        Modifier.element(ElementKeys.QuickSettingsContent).fillMaxSize().weight(1f)
+                ) {
                     Column {
                         Spacer(
                             modifier = Modifier.height { qqsPadding + qsExtraPadding.roundToPx() }
@@ -483,7 +526,9 @@
                     FooterActions(
                         viewModel = viewModel.footerActionsViewModel,
                         qsVisibilityLifecycleOwner = this@QSFragmentCompose,
-                        modifier = Modifier.sysuiResTag("qs_footer_actions"),
+                        modifier =
+                            Modifier.sysuiResTag("qs_footer_actions")
+                                .element(ElementKeys.FooterActions),
                     )
                 }
             }
@@ -590,3 +635,85 @@
             return currentId++
         }
     }
+
+object SceneKeys {
+    val QuickQuickSettings = SceneKey("QuickQuickSettingsScene")
+    val QuickSettings = SceneKey("QuickSettingsScene")
+
+    fun QSFragmentComposeViewModel.QSExpansionState.toIdleSceneKey(): SceneKey {
+        return when {
+            progress < 0.5f -> QuickQuickSettings
+            else -> QuickSettings
+        }
+    }
+}
+
+suspend fun synchronizeQsState(state: MutableSceneTransitionLayoutState, expansion: Flow<Float>) {
+    coroutineScope {
+        val animationScope = this
+
+        var currentTransition: ExpansionTransition? = null
+
+        fun snapTo(scene: SceneKey) {
+            state.snapToScene(scene)
+            currentTransition = null
+        }
+
+        expansion.collectLatest { progress ->
+            when (progress) {
+                0f -> snapTo(QuickQuickSettings)
+                1f -> snapTo(QuickSettings)
+                else -> {
+                    val transition = currentTransition
+                    if (transition != null) {
+                        transition.progress = progress
+                        return@collectLatest
+                    }
+
+                    val newTransition =
+                        ExpansionTransition(progress).also { currentTransition = it }
+                    state.startTransitionImmediately(
+                        animationScope = animationScope,
+                        transition = newTransition,
+                    )
+                }
+            }
+        }
+    }
+}
+
+private class ExpansionTransition(currentProgress: Float) :
+    TransitionState.Transition.ChangeScene(
+        fromScene = QuickQuickSettings,
+        toScene = QuickSettings,
+    ) {
+    override val currentScene: SceneKey
+        get() {
+            // This should return the logical scene. If the QS STLState is only driven by
+            // synchronizeQSState() then it probably does not matter which one we return, this is
+            // only used to compute the current user actions of a STL.
+            return QuickQuickSettings
+        }
+
+    override var progress: Float by mutableFloatStateOf(currentProgress)
+
+    override val progressVelocity: Float
+        get() = 0f
+
+    override val isInitiatedByUserInput: Boolean
+        get() = true
+
+    override val isUserInputOngoing: Boolean
+        get() = true
+
+    private val finishCompletable = CompletableDeferred<Unit>()
+
+    override suspend fun run() {
+        // This transition runs until it is interrupted by another one.
+        finishCompletable.await()
+    }
+
+    override fun freezeAndAnimateToCurrentState() {
+        finishCompletable.complete(Unit)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/FromQuickQuickSettingsToQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/FromQuickQuickSettingsToQuickSettings.kt
new file mode 100644
index 0000000..1514986
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/FromQuickQuickSettingsToQuickSettings.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.qs.composefragment.ui
+
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.systemui.qs.shared.ui.ElementKeys
+
+fun TransitionBuilder.quickQuickSettingsToQuickSettings() {
+
+    fractionRange(start = 0.5f) { fade(ElementKeys.QuickSettingsContent) }
+
+    fractionRange(start = 0.9f) { fade(ElementKeys.FooterActions) }
+
+    anchoredTranslate(ElementKeys.QuickSettingsContent, ElementKeys.GridAnchor)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/GridAnchor.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/GridAnchor.kt
new file mode 100644
index 0000000..f0f46d3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/GridAnchor.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.qs.composefragment.ui
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.android.compose.animation.scene.SceneScope
+import com.android.systemui.qs.shared.ui.ElementKeys
+
+/**
+ * This composable is used at the start of the tiles in QQS and QS to anchor the expansion and be
+ * able to have relative anchor translation of elements that appear in QS.
+ */
+@Composable
+fun SceneScope.GridAnchor(modifier: Modifier = Modifier) {
+    // The size of this anchor does not matter, as the tiles don't change size on expansion.
+    Spacer(modifier.element(ElementKeys.GridAnchor))
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
index 7ab11d2..7300ee1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
@@ -147,7 +147,7 @@
             .stateIn(
                 lifecycleScope,
                 SharingStarted.WhileSubscribed(),
-                disableFlagsRepository.disableFlags.value.isQuickSettingsEnabled()
+                disableFlagsRepository.disableFlags.value.isQuickSettingsEnabled(),
             )
 
     private val _showCollapsedOnKeyguard = MutableStateFlow(false)
@@ -213,19 +213,11 @@
         }
 
     val expansionState: StateFlow<QSExpansionState> =
-        combine(
-                _stackScrollerOverscrolling,
-                _qsExpanded,
-                _qsExpansion,
-            ) { args: Array<Any> ->
+        combine(_stackScrollerOverscrolling, _qsExpanded, _qsExpansion) { args: Array<Any> ->
                 val expansion = args[2] as Float
-                if (expansion > 0.5f) {
-                    QSExpansionState.QS
-                } else {
-                    QSExpansionState.QQS
-                }
+                QSExpansionState(expansion.coerceIn(0f, 1f))
             }
-            .stateIn(lifecycleScope, SharingStarted.WhileSubscribed(), QSExpansionState.QQS)
+            .stateIn(lifecycleScope, SharingStarted.WhileSubscribed(), QSExpansionState(0f))
 
     /**
      * Accessibility action for collapsing/expanding QS. The provided runnable is responsible for
@@ -262,13 +254,6 @@
         fun create(lifecycleScope: LifecycleCoroutineScope): QSFragmentComposeViewModel
     }
 
-    sealed interface QSExpansionState {
-        data object QQS : QSExpansionState
-
-        data object QS : QSExpansionState
-
-        @JvmInline value class Expanding(val progress: Float) : QSExpansionState
-
-        @JvmInline value class Collapsing(val progress: Float) : QSExpansionState
-    }
+    // In the future, this will have other relevant elements like squishiness.
+    data class QSExpansionState(@FloatRange(0.0, 1.0) val progress: Float)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
index fd276c2..0c02b40 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
+import com.android.compose.animation.scene.SceneScope
 import com.android.systemui.qs.panels.shared.model.SizedTile
 import com.android.systemui.qs.panels.shared.model.TileRow
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
@@ -27,7 +28,7 @@
 /** A layout of tiles, indicating how they should be composed when showing in QS or in edit mode. */
 interface GridLayout {
     @Composable
-    fun TileGrid(
+    fun SceneScope.TileGrid(
         tiles: List<TileViewModel>,
         modifier: Modifier,
         editModeStart: () -> Unit,
@@ -66,7 +67,7 @@
          */
         fun splitInRows(
             tiles: List<SizedTile<TileViewModel>>,
-            columns: Int
+            columns: Int,
         ): List<List<SizedTile<TileViewModel>>> {
             val row = TileRow<TileViewModel>(columns)
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
index 08a56bf..083f529 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
@@ -39,6 +39,7 @@
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.compose.animation.scene.SceneScope
 import com.android.systemui.compose.modifiers.sysuiResTag
 import com.android.systemui.qs.panels.dagger.PaginatedBaseLayoutType
 import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout.Dimensions.FooterHeight
@@ -55,7 +56,7 @@
     @PaginatedBaseLayoutType private val delegateGridLayout: PaginatableGridLayout,
 ) : GridLayout by delegateGridLayout {
     @Composable
-    override fun TileGrid(
+    override fun SceneScope.TileGrid(
         tiles: List<TileViewModel>,
         modifier: Modifier,
         editModeStart: () -> Unit,
@@ -85,16 +86,16 @@
             ) {
                 val page = pages[it]
 
-                delegateGridLayout.TileGrid(tiles = page, modifier = Modifier, editModeStart = {})
+                with(delegateGridLayout) {
+                    TileGrid(tiles = page, modifier = Modifier, editModeStart = {})
+                }
             }
-            Box(
-                modifier = Modifier.height(FooterHeight).fillMaxWidth(),
-            ) {
+            Box(modifier = Modifier.height(FooterHeight).fillMaxWidth()) {
                 PagerDots(
                     pagerState = pagerState,
                     activeColor = MaterialTheme.colorScheme.primary,
                     nonActiveColor = MaterialTheme.colorScheme.surfaceVariant,
-                    modifier = Modifier.align(Alignment.Center)
+                    modifier = Modifier.align(Alignment.Center),
                 )
                 CompositionLocalProvider(value = LocalContentColor provides Color.White) {
                     IconButton(
@@ -103,7 +104,7 @@
                     ) {
                         Icon(
                             imageVector = Icons.Default.Edit,
-                            contentDescription = stringResource(id = R.string.qs_edit)
+                            contentDescription = stringResource(id = R.string.qs_edit),
                         )
                     }
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
index f4acbec..8998a7f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt
@@ -16,21 +16,28 @@
 
 package com.android.systemui.qs.panels.ui.compose
 
-import androidx.compose.foundation.lazy.grid.GridCells
-import androidx.compose.foundation.lazy.grid.GridItemSpan
+import androidx.compose.foundation.layout.Box
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.dimensionResource
 import androidx.compose.ui.util.fastMap
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.compose.animation.scene.SceneScope
 import com.android.systemui.compose.modifiers.sysuiResTag
+import com.android.systemui.grid.ui.compose.VerticalSpannedGrid
+import com.android.systemui.qs.composefragment.ui.GridAnchor
 import com.android.systemui.qs.panels.ui.compose.infinitegrid.Tile
-import com.android.systemui.qs.panels.ui.compose.infinitegrid.TileLazyGrid
 import com.android.systemui.qs.panels.ui.viewmodel.QuickQuickSettingsViewModel
+import com.android.systemui.qs.shared.ui.ElementKeys.toElementKey
+import com.android.systemui.res.R
 
 @Composable
-fun QuickQuickSettings(viewModel: QuickQuickSettingsViewModel, modifier: Modifier = Modifier) {
+fun SceneScope.QuickQuickSettings(
+    viewModel: QuickQuickSettingsViewModel,
+    modifier: Modifier = Modifier,
+) {
     val sizedTiles by
         viewModel.tileViewModels.collectAsStateWithLifecycle(initialValue = emptyList())
     val tiles = sizedTiles.fastMap { it.tile }
@@ -41,20 +48,20 @@
         onDispose { tiles.forEach { it.stopListening(token) } }
     }
     val columns by viewModel.columns.collectAsStateWithLifecycle()
-
-    TileLazyGrid(
-        modifier = modifier.sysuiResTag("qqs_tile_layout"),
-        columns = GridCells.Fixed(columns),
-    ) {
-        items(
-            sizedTiles.size,
-            key = { index -> sizedTiles[index].tile.spec.spec },
-            span = { index -> GridItemSpan(sizedTiles[index].width) },
-        ) { index ->
+    Box(modifier = modifier) {
+        GridAnchor()
+        VerticalSpannedGrid(
+            columns = columns,
+            columnSpacing = dimensionResource(R.dimen.qs_tile_margin_horizontal),
+            rowSpacing = dimensionResource(R.dimen.qs_tile_margin_vertical),
+            spans = sizedTiles.fastMap { it.width },
+            modifier = Modifier.sysuiResTag("qqs_tile_layout"),
+        ) { spanIndex ->
+            val it = sizedTiles[spanIndex]
             Tile(
-                tile = sizedTiles[index].tile,
-                iconOnly = sizedTiles[index].isIcon,
-                modifier = Modifier,
+                tile = it.tile,
+                iconOnly = it.isIcon,
+                modifier = Modifier.element(it.tile.spec.toElementKey(spanIndex)),
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt
index 8c57d41..1a5297b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt
@@ -20,16 +20,17 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.compose.animation.scene.SceneScope
 import com.android.systemui.qs.panels.ui.viewmodel.TileGridViewModel
 
 @Composable
-fun TileGrid(
+fun SceneScope.TileGrid(
     viewModel: TileGridViewModel,
     modifier: Modifier = Modifier,
-    editModeStart: () -> Unit
+    editModeStart: () -> Unit,
 ) {
     val gridLayout by viewModel.gridLayout.collectAsStateWithLifecycle()
     val tiles by viewModel.tileViewModels.collectAsStateWithLifecycle(emptyList())
 
-    gridLayout.TileGrid(tiles, modifier, editModeStart)
+    with(gridLayout) { TileGrid(tiles, modifier, editModeStart) }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
index 4946c01..8a96065 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
@@ -16,15 +16,17 @@
 
 package com.android.systemui.qs.panels.ui.compose.infinitegrid
 
-import androidx.compose.foundation.lazy.grid.GridCells
-import androidx.compose.foundation.lazy.grid.GridItemSpan
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.util.fastMap
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.compose.animation.scene.SceneScope
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.grid.ui.compose.VerticalSpannedGrid
 import com.android.systemui.qs.panels.shared.model.SizedTileImpl
 import com.android.systemui.qs.panels.ui.compose.PaginatableGridLayout
 import com.android.systemui.qs.panels.ui.compose.rememberEditListState
@@ -33,6 +35,8 @@
 import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.ui.ElementKeys.toElementKey
+import com.android.systemui.res.R
 import javax.inject.Inject
 
 @SysUISingleton
@@ -44,7 +48,7 @@
 ) : PaginatableGridLayout {
 
     @Composable
-    override fun TileGrid(
+    override fun SceneScope.TileGrid(
         tiles: List<TileViewModel>,
         modifier: Modifier,
         editModeStart: () -> Unit,
@@ -57,15 +61,18 @@
         val columns by gridSizeViewModel.columns.collectAsStateWithLifecycle()
         val sizedTiles = tiles.map { SizedTileImpl(it, it.spec.width()) }
 
-        TileLazyGrid(modifier = modifier, columns = GridCells.Fixed(columns)) {
-            items(sizedTiles.size, span = { index -> GridItemSpan(sizedTiles[index].width) }) {
-                index ->
-                Tile(
-                    tile = sizedTiles[index].tile,
-                    iconOnly = iconTilesViewModel.isIconTile(sizedTiles[index].tile.spec),
-                    modifier = Modifier,
-                )
-            }
+        VerticalSpannedGrid(
+            columns = columns,
+            columnSpacing = dimensionResource(R.dimen.qs_tile_margin_horizontal),
+            rowSpacing = dimensionResource(R.dimen.qs_tile_margin_vertical),
+            spans = sizedTiles.fastMap { it.width },
+        ) { spanIndex ->
+            val it = sizedTiles[spanIndex]
+            Tile(
+                tile = it.tile,
+                iconOnly = iconTilesViewModel.isIconTile(it.tile.spec),
+                modifier = Modifier.element(it.tile.spec.toElementKey(spanIndex)),
+            )
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/shared/ui/ElementKeys.kt b/packages/SystemUI/src/com/android/systemui/qs/shared/ui/ElementKeys.kt
new file mode 100644
index 0000000..625459d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/shared/ui/ElementKeys.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.qs.shared.ui
+
+import com.android.compose.animation.scene.ElementKey
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+/** Element keys to be used by the compose implementation of QS for animations. */
+object ElementKeys {
+    val QuickSettingsContent = ElementKey("QuickSettingsContent")
+    val GridAnchor = ElementKey("QuickSettingsGridAnchor")
+    val FooterActions = ElementKey("FooterActions")
+
+    class TileElementKey(spec: TileSpec, val position: Int) : ElementKey(spec.spec, spec.spec)
+
+    fun TileSpec.toElementKey(positionInGrid: Int) = TileElementKey(this, positionInGrid)
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java b/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java
index 4104999..12b7445 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.wm;
 
+import static android.tools.traces.Utils.busyWaitForDataSourceRegistration;
+
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyZeroInteractions;
@@ -28,7 +30,6 @@
 import static java.io.File.createTempFile;
 import static java.nio.file.Files.createTempDirectory;
 
-import android.os.ParcelFileDescriptor;
 import android.platform.test.annotations.Presubmit;
 import android.tools.ScenarioBuilder;
 import android.tools.traces.io.ResultWriter;
@@ -36,9 +37,6 @@
 import android.view.Choreographer;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.google.protobuf.InvalidProtocolBufferException;
 
 import org.junit.After;
 import org.junit.Before;
@@ -46,12 +44,9 @@
 import org.junit.Test;
 import org.mockito.Mockito;
 
-import perfetto.protos.PerfettoConfig.TracingServiceState;
 import perfetto.protos.PerfettoConfig.WindowManagerConfig.LogFrequency;
 
-import java.io.FileInputStream;
 import java.io.IOException;
-import java.util.Optional;
 
 /**
  * Test class for {@link WindowTracingPerfetto}.
@@ -74,7 +69,7 @@
         sChoreographer = Mockito.mock(Choreographer.class);
         sWindowTracing = new WindowTracingPerfetto(sWmMock, sChoreographer,
                 new WindowManagerGlobalLock(), TEST_DATA_SOURCE_NAME);
-        waitDataSourceIsAvailable();
+        busyWaitForDataSourceRegistration(TEST_DATA_SOURCE_NAME);
     }
 
     @Before
@@ -156,67 +151,4 @@
 
         mTraceMonitor.stop(writer);
     }
-
-    private static void waitDataSourceIsAvailable() {
-        final int timeoutMs = 10000;
-        final int busyWaitIntervalMs = 100;
-
-        int elapsedMs = 0;
-
-        while (!isDataSourceAvailable()) {
-            try {
-                Thread.sleep(busyWaitIntervalMs);
-                elapsedMs += busyWaitIntervalMs;
-                if (elapsedMs >= timeoutMs) {
-                    throw new RuntimeException("Data source didn't become available."
-                            + " Waited for: " + timeoutMs + " ms");
-                }
-            } catch (InterruptedException e) {
-                throw new RuntimeException(e);
-            }
-        }
-    }
-
-    private static boolean isDataSourceAvailable() {
-        byte[] proto = executeShellCommand("perfetto --query-raw");
-
-        try {
-            TracingServiceState state = TracingServiceState.parseFrom(proto);
-
-            Optional<Integer> producerId = Optional.empty();
-
-            for (TracingServiceState.Producer producer : state.getProducersList()) {
-                if (producer.getPid() == android.os.Process.myPid()) {
-                    producerId = Optional.of(producer.getId());
-                    break;
-                }
-            }
-
-            if (producerId.isEmpty()) {
-                return false;
-            }
-
-            for (TracingServiceState.DataSource ds : state.getDataSourcesList()) {
-                if (ds.getDsDescriptor().getName().equals(TEST_DATA_SOURCE_NAME)
-                        && ds.getProducerId() == producerId.get()) {
-                    return true;
-                }
-            }
-        } catch (InvalidProtocolBufferException e) {
-            throw new RuntimeException(e);
-        }
-
-        return false;
-    }
-
-    private static byte[] executeShellCommand(String command) {
-        try {
-            ParcelFileDescriptor fd = InstrumentationRegistry.getInstrumentation().getUiAutomation()
-                    .executeShellCommand(command);
-            FileInputStream is = new ParcelFileDescriptor.AutoCloseInputStream(fd);
-            return is.readAllBytes();
-        } catch (IOException e) {
-            throw new RuntimeException(e);
-        }
-    }
 }
diff --git a/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
index cfb2645..6f3deab 100644
--- a/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
+++ b/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
@@ -16,7 +16,7 @@
 
 package com.android.internal.protolog;
 
-import static android.tools.traces.Utils.executeShellCommand;
+import static android.tools.traces.Utils.busyWaitForDataSourceRegistration;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertThrows;
@@ -50,7 +50,6 @@
 import com.android.internal.protolog.common.LogLevel;
 
 import com.google.common.truth.Truth;
-import com.google.protobuf.InvalidProtocolBufferException;
 
 import org.junit.After;
 import org.junit.Before;
@@ -60,14 +59,12 @@
 import org.junit.runners.JUnit4;
 import org.mockito.Mockito;
 
-import perfetto.protos.PerfettoConfig.TracingServiceState;
 import perfetto.protos.Protolog;
 import perfetto.protos.ProtologCommon;
 
 import java.io.File;
 import java.io.IOException;
 import java.util.List;
-import java.util.Optional;
 import java.util.Random;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -184,7 +181,7 @@
                     TestProtoLogGroup.values(), dataSourceBuilder, sProtoLogConfigurationService);
         }
 
-        waitDataSourceIsAvailable();
+        busyWaitForDataSourceRegistration(TEST_PROTOLOG_DATASOURCE_NAME);
     }
 
     @Before
@@ -870,54 +867,6 @@
                 .isEqualTo("This message should also be logged 567");
     }
 
-    private static void waitDataSourceIsAvailable() {
-        final int timeoutMs = 10000;
-        final int busyWaitIntervalMs = 100;
-
-        int elapsedMs = 0;
-
-        while (!isDataSourceAvailable()) {
-            SystemClock.sleep(busyWaitIntervalMs);
-            elapsedMs += busyWaitIntervalMs;
-            if (elapsedMs >= timeoutMs) {
-                throw new RuntimeException("Data source didn't become available."
-                        + " Waited for: " + timeoutMs + " ms");
-            }
-        }
-    }
-
-    private static boolean isDataSourceAvailable() {
-        byte[] proto = executeShellCommand("perfetto --query-raw");
-
-        try {
-            TracingServiceState state = TracingServiceState.parseFrom(proto);
-
-            Optional<Integer> producerId = Optional.empty();
-
-            for (TracingServiceState.Producer producer : state.getProducersList()) {
-                if (producer.getPid() == android.os.Process.myPid()) {
-                    producerId = Optional.of(producer.getId());
-                    break;
-                }
-            }
-
-            if (producerId.isEmpty()) {
-                return false;
-            }
-
-            for (TracingServiceState.DataSource ds : state.getDataSourcesList()) {
-                if (ds.getDsDescriptor().getName().equals(TEST_PROTOLOG_DATASOURCE_NAME)
-                        && ds.getProducerId() == producerId.get()) {
-                    return true;
-                }
-            }
-        } catch (InvalidProtocolBufferException e) {
-            throw new RuntimeException(e);
-        }
-
-        return false;
-    }
-
     private enum TestProtoLogGroup implements IProtoLogGroup {
         TEST_GROUP(true, true, false, "TEST_TAG");
 
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfaces.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfaces.kt
index 675a59e..caa018d 100644
--- a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfaces.kt
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfaces.kt
@@ -256,7 +256,7 @@
     "android.devicelock.IGetDeviceIdCallback",
     "android.devicelock.IGetKioskAppsCallback",
     "android.devicelock.IIsDeviceLockedCallback",
-    "android.devicelock.IVoidResultCallback",
+    "android.devicelock.ILockUnlockDeviceCallback",
     "android.federatedcompute.aidl.IExampleStoreCallback",
     "android.federatedcompute.aidl.IExampleStoreIterator",
     "android.federatedcompute.aidl.IExampleStoreIteratorCallback",
@@ -364,8 +364,6 @@
     "android.health.connect.aidl.IGetPriorityResponseCallback",
     "android.health.connect.aidl.IHealthConnectService",
     "android.health.connect.aidl.IInsertRecordsResponseCallback",
-    "android.health.connect.aidl.IMedicalDataSourceResponseCallback",
-    "android.health.connect.aidl.IMedicalResourcesResponseCallback",
     "android.health.connect.aidl.IMigrationCallback",
     "android.health.connect.aidl.IReadMedicalResourcesResponseCallback",
     "android.health.connect.aidl.IReadRecordsResponseCallback",
@@ -462,6 +460,7 @@
     "android.net.ipmemorystore.IOnBlobRetrievedListener",
     "android.net.ipmemorystore.IOnL2KeyResponseListener",
     "android.net.ipmemorystore.IOnNetworkAttributesRetrievedListener",
+    "android.net.ipmemorystore.IOnNetworkEventCountRetrievedListener",
     "android.net.ipmemorystore.IOnSameL3NetworkResponseListener",
     "android.net.ipmemorystore.IOnStatusAndCountListener",
     "android.net.ipmemorystore.IOnStatusListener",
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/PermissionAnnotationDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/PermissionAnnotationDetector.kt
index d44c271..8d6e320 100644
--- a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/PermissionAnnotationDetector.kt
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/PermissionAnnotationDetector.kt
@@ -57,7 +57,10 @@
             ISSUE_MISSING_PERMISSION_ANNOTATION,
             node,
             context.getLocation(node),
-            "The method ${node.name} is not permission-annotated."
+            """
+                ${node.name} should be annotated with either @EnforcePermission, \
+                @RequiresNoPermission or @PermissionManuallyEnforced.
+            """.trimMargin()
         )
     }
 
diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/PermissionAnnotationDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/PermissionAnnotationDetectorTest.kt
index 824be93..f985d02 100644
--- a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/PermissionAnnotationDetectorTest.kt
+++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/PermissionAnnotationDetectorTest.kt
@@ -73,7 +73,7 @@
             .run()
             .expect(
                 """
-                src/frameworks/base/services/java/com/android/server/Bar.java:3: Error: The method testMethod is not permission-annotated. [MissingPermissionAnnotation]
+                src/frameworks/base/services/java/com/android/server/Bar.java:3: Error: testMethod should be annotated with either @EnforcePermission, @RequiresNoPermission or @PermissionManuallyEnforced. [MissingPermissionAnnotation]
                     public void testMethod(int parameter1, int parameter2) { }
                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                 1 errors, 0 warnings