Stop the tile grid from automatically reflowing during a resizing movement.

This avoids the situation of a large tile moving up a row while being resized. The reflow will happen at the end of the resizing movement.

Flag: com.android.systemui.qs_ui_refactor_compose_fragment
Test: MutableSelectionStateTest
Test: ResizingTest
Bug: 350984160

Change-Id: I0108663c7f3e4b13b215a2f9afb264d66ccda922
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt
index e58cf15..79a303d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt
@@ -85,12 +85,12 @@
                 runCurrent()
 
                 // Assert that the tile is removed from the large tiles after resizing
-                underTest.resize(largeTile)
+                underTest.resize(largeTile, toIcon = true)
                 runCurrent()
                 assertThat(latest).doesNotContain(largeTile)
 
                 // Assert that the tile is added to the large tiles after resizing
-                underTest.resize(largeTile)
+                underTest.resize(largeTile, toIcon = false)
                 runCurrent()
                 assertThat(latest).contains(largeTile)
             }
@@ -122,7 +122,7 @@
                 val newTile = TileSpec.create("newTile")
 
                 // Remove the large tile from the current tiles
-                underTest.resize(newTile)
+                underTest.resize(newTile, toIcon = false)
                 runCurrent()
 
                 // Assert that it's still small
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt
index 484a8ff..3910903 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt
@@ -24,7 +24,6 @@
 import com.android.systemui.qs.panels.shared.model.SizedTile
 import com.android.systemui.qs.panels.shared.model.SizedTileImpl
 import com.android.systemui.qs.panels.ui.model.GridCell
-import com.android.systemui.qs.panels.ui.model.SpacerGridCell
 import com.android.systemui.qs.panels.ui.model.TileGridCell
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
 import com.android.systemui.qs.pipeline.shared.TileSpec
@@ -39,13 +38,6 @@
     private val underTest = EditTileListState(TestEditTiles, 4)
 
     @Test
-    fun noDrag_listUnchanged() {
-        underTest.tiles.forEach { assertThat(it).isNotInstanceOf(SpacerGridCell::class.java) }
-        assertThat(underTest.tiles.map { (it as TileGridCell).tile.tileSpec })
-            .containsExactly(*TestEditTiles.map { it.tile.tileSpec }.toTypedArray())
-    }
-
-    @Test
     fun startDrag_listHasSpacers() {
         underTest.onStarted(TestEditTiles[0])
 
@@ -109,16 +101,6 @@
     }
 
     @Test
-    fun droppedNewTile_spacersDisappear() {
-        underTest.onStarted(TestEditTiles[0])
-        underTest.onDrop()
-
-        assertThat(underTest.tiles.toStrings()).isEqualTo(listOf("a", "b", "c", "d", "e"))
-        assertThat(underTest.isMoving(TestEditTiles[0].tile.tileSpec)).isFalse()
-        assertThat(underTest.dragInProgress).isFalse()
-    }
-
-    @Test
     fun movedTileOutOfBounds_tileDisappears() {
         underTest.onStarted(TestEditTiles[0])
         underTest.movedOutOfBounds()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt
index fa72d74..4acf3ee 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt
@@ -27,23 +27,25 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class MutableSelectionStateTest : SysuiTestCase() {
-    private val underTest = MutableSelectionState()
+    private val underTest = MutableSelectionState({}, {})
 
     @Test
     fun selectTile_isCorrectlySelected() {
-        assertThat(underTest.isSelected(TEST_SPEC)).isFalse()
+        assertThat(underTest.selection?.tileSpec).isNotEqualTo(TEST_SPEC)
 
-        underTest.select(TEST_SPEC)
-        assertThat(underTest.isSelected(TEST_SPEC)).isTrue()
+        underTest.select(TEST_SPEC, manual = true)
+        assertThat(underTest.selection?.tileSpec).isEqualTo(TEST_SPEC)
+        assertThat(underTest.selection?.manual).isTrue()
 
         underTest.unSelect()
-        assertThat(underTest.isSelected(TEST_SPEC)).isFalse()
+        assertThat(underTest.selection).isNull()
 
         val newSpec = TileSpec.create("newSpec")
-        underTest.select(TEST_SPEC)
-        underTest.select(newSpec)
-        assertThat(underTest.isSelected(TEST_SPEC)).isFalse()
-        assertThat(underTest.isSelected(newSpec)).isTrue()
+        underTest.select(TEST_SPEC, manual = true)
+        underTest.select(newSpec, manual = false)
+        assertThat(underTest.selection?.tileSpec).isNotEqualTo(TEST_SPEC)
+        assertThat(underTest.selection?.tileSpec).isEqualTo(newSpec)
+        assertThat(underTest.selection?.manual).isFalse()
     }
 
     @Test
@@ -51,12 +53,12 @@
         assertThat(underTest.resizingState).isNull()
 
         // Resizing starts but no tile is selected
-        underTest.onResizingDragStart(TileWidths(0, 0, 1)) {}
+        underTest.onResizingDragStart(TileWidths(0, 0, 1))
         assertThat(underTest.resizingState).isNull()
 
         // Resizing starts with a selected tile
-        underTest.select(TEST_SPEC)
-        underTest.onResizingDragStart(TileWidths(0, 0, 1)) {}
+        underTest.select(TEST_SPEC, manual = true)
+        underTest.onResizingDragStart(TileWidths(0, 0, 1))
 
         assertThat(underTest.resizingState).isNotNull()
     }
@@ -66,8 +68,8 @@
         val spec = TileSpec.create("testSpec")
 
         // Resizing starts with a selected tile
-        underTest.select(spec)
-        underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) {}
+        underTest.select(spec, manual = true)
+        underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10))
         assertThat(underTest.resizingState).isNotNull()
 
         underTest.onResizingDragEnd()
@@ -77,8 +79,8 @@
     @Test
     fun unselect_clearsResizingState() {
         // Resizing starts with a selected tile
-        underTest.select(TEST_SPEC)
-        underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) {}
+        underTest.select(TEST_SPEC, manual = true)
+        underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10))
         assertThat(underTest.resizingState).isNotNull()
 
         underTest.unSelect()
@@ -88,8 +90,8 @@
     @Test
     fun onResizingDrag_updatesResizingState() {
         // Resizing starts with a selected tile
-        underTest.select(TEST_SPEC)
-        underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) {}
+        underTest.select(TEST_SPEC, manual = true)
+        underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10))
         assertThat(underTest.resizingState).isNotNull()
 
         underTest.onResizingDrag(5f)
@@ -105,11 +107,15 @@
     @Test
     fun onResizingDrag_receivesResizeCallback() {
         var resized = false
-        val onResize: () -> Unit = { resized = !resized }
+        val onResize: (TileSpec) -> Unit = {
+            assertThat(it).isEqualTo(TEST_SPEC)
+            resized = !resized
+        }
+        val underTest = MutableSelectionState(onResize = onResize, {})
 
         // Resizing starts with a selected tile
-        underTest.select(TEST_SPEC)
-        underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10), onResize)
+        underTest.select(TEST_SPEC, true)
+        underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10))
         assertThat(underTest.resizingState).isNotNull()
 
         // Drag under the threshold
@@ -125,6 +131,37 @@
         assertThat(resized).isFalse()
     }
 
+    @Test
+    fun onResizingEnded_receivesResizeEndCallback() {
+        var resizeEnded = false
+        val onResizeEnd: (TileSpec) -> Unit = { resizeEnded = true }
+        val underTest = MutableSelectionState({}, onResizeEnd = onResizeEnd)
+
+        // Resizing starts with a selected tile
+        underTest.select(TEST_SPEC, true)
+        underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10))
+
+        underTest.onResizingDragEnd()
+        assertThat(resizeEnded).isTrue()
+    }
+
+    @Test
+    fun onResizingEnded_setsSelectionAutomatically() {
+        val underTest = MutableSelectionState({}, {})
+
+        // Resizing starts with a selected tile
+        underTest.select(TEST_SPEC, manual = true)
+        underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10))
+
+        // Assert the selection was manual
+        assertThat(underTest.selection?.manual).isTrue()
+
+        underTest.onResizingDragEnd()
+
+        // Assert the selection is no longer manual due to the resizing
+        assertThat(underTest.selection?.manual).isFalse()
+    }
+
     companion object {
         private val TEST_SPEC = TileSpec.create("testSpec")
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt
index 02a607d..fc59a50 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt
@@ -40,7 +40,7 @@
     private val currentTilesInteractor: CurrentTilesInteractor,
     private val preferencesInteractor: QSPreferencesInteractor,
     @PanelsLog private val logBuffer: LogBuffer,
-    @Application private val applicationScope: CoroutineScope
+    @Application private val applicationScope: CoroutineScope,
 ) {
 
     val largeTilesSpecs =
@@ -64,14 +64,15 @@
 
     fun isIconTile(spec: TileSpec): Boolean = !largeTilesSpecs.value.contains(spec)
 
-    fun resize(spec: TileSpec) {
+    fun resize(spec: TileSpec, toIcon: Boolean) {
         if (!isCurrent(spec)) {
             return
         }
 
-        if (largeTilesSpecs.value.contains(spec)) {
+        val isIcon = !largeTilesSpecs.value.contains(spec)
+        if (toIcon && !isIcon) {
             preferencesInteractor.setLargeTilesSpecs(largeTilesSpecs.value - spec)
-        } else {
+        } else if (!toIcon && isIcon) {
             preferencesInteractor.setLargeTilesSpecs(largeTilesSpecs.value + spec)
         }
     }
@@ -85,7 +86,7 @@
             LOG_BUFFER_LARGE_TILES_SPECS_CHANGE_TAG,
             LogLevel.DEBUG,
             { str1 = specs.toString() },
-            { "Large tiles change: $str1" }
+            { "Large tiles change: $str1" },
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
index a4f977b..770fd78 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
@@ -60,10 +60,37 @@
         return _tiles.filterIsInstance<TileGridCell>().map { it.tile.tileSpec }
     }
 
-    fun indexOf(tileSpec: TileSpec): Int {
+    private fun indexOf(tileSpec: TileSpec): Int {
         return _tiles.indexOfFirst { it is TileGridCell && it.tile.tileSpec == tileSpec }
     }
 
+    /**
+     * Whether the tile with this [TileSpec] is currently an icon in the [EditTileListState]
+     *
+     * @return true if the tile is an icon, false if it's large, null if the tile isn't in the list
+     */
+    fun isIcon(tileSpec: TileSpec): Boolean? {
+        val index = indexOf(tileSpec)
+        return if (index != -1) {
+            val cell = _tiles[index]
+            cell as TileGridCell
+            return cell.isIcon
+        } else {
+            null
+        }
+    }
+
+    /** Toggle the size of the tile corresponding to the [TileSpec] */
+    fun toggleSize(tileSpec: TileSpec) {
+        val fromIndex = indexOf(tileSpec)
+        if (fromIndex != -1) {
+            val cell = _tiles.removeAt(fromIndex)
+            cell as TileGridCell
+            _tiles.add(fromIndex, cell.copy(width = if (cell.isIcon) 2 else 1))
+            regenerateGrid(fromIndex)
+        }
+    }
+
     override fun isMoving(tileSpec: TileSpec): Boolean {
         return _draggedCell.value?.let { it.tile.tileSpec == tileSpec } ?: false
     }
@@ -71,8 +98,8 @@
     override fun onStarted(cell: SizedTile<EditTileViewModel>) {
         _draggedCell.value = cell
 
-        // Add visible spacers to the grid to indicate where the user can move a tile
-        regenerateGrid(includeSpacers = true)
+        // Add spacers to the grid to indicate where the user can move a tile
+        regenerateGrid()
     }
 
     override fun onMoved(target: Int, insertAfter: Boolean) {
@@ -86,7 +113,7 @@
         val insertionIndex = if (insertAfter) target + 1 else target
         if (fromIndex != -1) {
             val cell = _tiles.removeAt(fromIndex)
-            regenerateGrid(includeSpacers = true)
+            regenerateGrid()
             _tiles.add(insertionIndex.coerceIn(0, _tiles.size), cell)
         } else {
             // Add the tile with a temporary row which will get reassigned when
@@ -94,7 +121,7 @@
             _tiles.add(insertionIndex.coerceIn(0, _tiles.size), TileGridCell(draggedTile, 0))
         }
 
-        regenerateGrid(includeSpacers = true)
+        regenerateGrid()
     }
 
     override fun movedOutOfBounds() {
@@ -109,13 +136,28 @@
         _draggedCell.value = null
 
         // Remove the spacers
-        regenerateGrid(includeSpacers = false)
+        regenerateGrid()
     }
 
-    private fun regenerateGrid(includeSpacers: Boolean) {
-        _tiles.filterIsInstance<TileGridCell>().toGridCells(columns, includeSpacers).let {
+    /** Regenerate the list of [GridCell] with their new potential rows */
+    private fun regenerateGrid() {
+        _tiles.filterIsInstance<TileGridCell>().toGridCells(columns).let {
             _tiles.clear()
             _tiles.addAll(it)
         }
     }
+
+    /**
+     * Regenerate the list of [GridCell] with their new potential rows from [fromIndex], leaving
+     * cells before that untouched.
+     */
+    private fun regenerateGrid(fromIndex: Int) {
+        val fromRow = _tiles[fromIndex].row
+        val (pre, post) = _tiles.partition { it.row < fromRow }
+        post.filterIsInstance<TileGridCell>().toGridCells(columns, startingRow = fromRow).let {
+            _tiles.clear()
+            _tiles.addAll(pre)
+            _tiles.addAll(it)
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
index 0e76e18..30bafae 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
@@ -132,15 +132,23 @@
 
 @Composable
 fun DefaultEditTileGrid(
-    currentListState: EditTileListState,
+    listState: EditTileListState,
     otherTiles: List<SizedTile<EditTileViewModel>>,
     columns: Int,
     modifier: Modifier,
     onRemoveTile: (TileSpec) -> Unit,
     onSetTiles: (List<TileSpec>) -> Unit,
-    onResize: (TileSpec) -> Unit,
+    onResize: (TileSpec, toIcon: Boolean) -> Unit,
 ) {
-    val selectionState = rememberSelectionState()
+    val currentListState by rememberUpdatedState(listState)
+    val selectionState =
+        rememberSelectionState(
+            onResize = { currentListState.toggleSize(it) },
+            onResizeEnd = { spec ->
+                // Commit the size currently in the list
+                currentListState.isIcon(spec)?.let { onResize(spec, it) }
+            },
+        )
 
     CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
         Column(
@@ -149,11 +157,11 @@
             modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()),
         ) {
             AnimatedContent(
-                targetState = currentListState.dragInProgress,
+                targetState = listState.dragInProgress,
                 modifier = Modifier.wrapContentSize(),
                 label = "",
             ) { dragIsInProgress ->
-                EditGridHeader(Modifier.dragAndDropRemoveZone(currentListState, onRemoveTile)) {
+                EditGridHeader(Modifier.dragAndDropRemoveZone(listState, onRemoveTile)) {
                     if (dragIsInProgress) {
                         RemoveTileTarget()
                     } else {
@@ -162,11 +170,11 @@
                 }
             }
 
-            CurrentTilesGrid(currentListState, selectionState, columns, onResize, onSetTiles)
+            CurrentTilesGrid(listState, selectionState, columns, onResize, onSetTiles)
 
             // Hide available tiles when dragging
             AnimatedVisibility(
-                visible = !currentListState.dragInProgress,
+                visible = !listState.dragInProgress,
                 enter = fadeIn(),
                 exit = fadeOut(),
             ) {
@@ -177,7 +185,7 @@
                 ) {
                     EditGridHeader { Text(text = "Hold and drag to add tiles.") }
 
-                    AvailableTileGrid(otherTiles, selectionState, columns, currentListState)
+                    AvailableTileGrid(otherTiles, selectionState, columns, listState)
                 }
             }
 
@@ -186,7 +194,7 @@
                 modifier =
                     Modifier.fillMaxWidth()
                         .weight(1f)
-                        .dragAndDropRemoveZone(currentListState, onRemoveTile)
+                        .dragAndDropRemoveZone(listState, onRemoveTile)
             )
         }
     }
@@ -229,7 +237,7 @@
     listState: EditTileListState,
     selectionState: MutableSelectionState,
     columns: Int,
-    onResize: (TileSpec) -> Unit,
+    onResize: (TileSpec, toIcon: Boolean) -> Unit,
     onSetTiles: (List<TileSpec>) -> Unit,
 ) {
     val currentListState by rememberUpdatedState(listState)
@@ -242,19 +250,6 @@
         )
     val gridState = rememberLazyGridState()
     var gridContentOffset by remember { mutableStateOf(Offset(0f, 0f)) }
-    var droppedSpec by remember { mutableStateOf<TileSpec?>(null) }
-
-    // Select the tile that was dropped. A delay is introduced to avoid clipping issues on the
-    // selected border and resizing handle, as well as letting the selection animation play.
-    LaunchedEffect(droppedSpec) {
-        droppedSpec?.let {
-            delay(200)
-            selectionState.select(it)
-
-            // Reset droppedSpec in case a tile is dropped twice in a row
-            droppedSpec = null
-        }
-    }
 
     TileLazyGrid(
         state = gridState,
@@ -270,14 +265,17 @@
                 )
                 .dragAndDropTileList(gridState, { gridContentOffset }, listState) { spec ->
                     onSetTiles(currentListState.tileSpecs())
-                    droppedSpec = spec
+                    selectionState.select(spec, manual = false)
                 }
                 .onGloballyPositioned { coordinates ->
                     gridContentOffset = coordinates.positionInRoot()
                 }
                 .testTag(CURRENT_TILES_GRID_TEST_TAG),
     ) {
-        EditTiles(listState.tiles, listState, selectionState, onResize)
+        EditTiles(listState.tiles, listState, selectionState) { spec ->
+            // Toggle the current size of the tile
+            currentListState.isIcon(spec)?.let { onResize(spec, !it) }
+        }
     }
 }
 
@@ -348,11 +346,19 @@
     }
 }
 
+/**
+ * Adds a list of [GridCell] to the lazy grid
+ *
+ * @param cells the list of [GridCell]
+ * @param dragAndDropState the [DragAndDropState] for this grid
+ * @param selectionState the [MutableSelectionState] for this grid
+ * @param onToggleSize the callback when a tile's size is toggled
+ */
 fun LazyGridScope.EditTiles(
     cells: List<GridCell>,
     dragAndDropState: DragAndDropState,
     selectionState: MutableSelectionState,
-    onResize: (TileSpec) -> Unit,
+    onToggleSize: (spec: TileSpec) -> Unit,
 ) {
     items(
         count = cells.size,
@@ -378,7 +384,7 @@
                         index = index,
                         dragAndDropState = dragAndDropState,
                         selectionState = selectionState,
-                        onResize = onResize,
+                        onToggleSize = onToggleSize,
                     )
                 }
             is SpacerGridCell -> SpacerGridCell()
@@ -392,16 +398,28 @@
     index: Int,
     dragAndDropState: DragAndDropState,
     selectionState: MutableSelectionState,
-    onResize: (TileSpec) -> Unit,
+    onToggleSize: (spec: TileSpec) -> Unit,
 ) {
-    val selected = selectionState.isSelected(cell.tile.tileSpec)
     val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1)
+    var selected by remember { mutableStateOf(false) }
     val selectionAlpha by
         animateFloatAsState(
             targetValue = if (selected) 1f else 0f,
             label = "QSEditTileSelectionAlpha",
         )
 
+    LaunchedEffect(selectionState.selection?.tileSpec) {
+        selectionState.selection?.let {
+            // A delay is introduced on automatic selections such as dragged tiles or reflow caused
+            // by resizing. This avoids clipping issues on the border and resizing handle, as well
+            // as letting the selection animation play correctly.
+            if (!it.manual) {
+                delay(250)
+            }
+        }
+        selected = selectionState.selection?.tileSpec == cell.tile.tileSpec
+    }
+
     val modifier =
         Modifier.animateItem()
             .semantics(mergeDescendants = true) {
@@ -411,7 +429,7 @@
                     listOf(
                         // TODO(b/367748260): Add final accessibility actions
                         CustomAccessibilityAction("Toggle size") {
-                            onResize(cell.tile.tileSpec)
+                            onToggleSize(cell.tile.tileSpec)
                             true
                         }
                     )
@@ -438,11 +456,9 @@
 
     if (selected) {
         SelectedTile(
-            tileSpec = cell.tile.tileSpec,
             isIcon = cell.isIcon,
             selectionAlpha = { selectionAlpha },
             selectionState = selectionState,
-            onResize = onResize,
             modifier = modifier.zIndex(2f), // 2f to display this tile over neighbors when dragged
             content = content,
         )
@@ -458,11 +474,9 @@
 
 @Composable
 private fun SelectedTile(
-    tileSpec: TileSpec,
     isIcon: Boolean,
     selectionAlpha: () -> Float,
     selectionState: MutableSelectionState,
-    onResize: (TileSpec) -> Unit,
     modifier: Modifier = Modifier,
     content: @Composable () -> Unit,
 ) {
@@ -492,9 +506,7 @@
                     selectionState = selectionState,
                     transition = selectionAlpha,
                     tileWidths = { tileWidths },
-                ) {
-                    onResize(tileSpec)
-                }
+                )
             }
 
         Layout(contents = listOf(content, handle)) {
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..542d0ef 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
@@ -94,7 +94,7 @@
         val (currentTiles, otherTiles) = sizedTiles.partition { it.tile.isCurrent }
         val currentListState = rememberEditListState(currentTiles, columns)
         DefaultEditTileGrid(
-            currentListState = currentListState,
+            listState = currentListState,
             otherTiles = otherTiles,
             columns = columns,
             modifier = modifier,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt
index 2ea32e6..441d962 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt
@@ -27,28 +27,39 @@
 
 /** Creates the state of the current selected tile that is remembered across compositions. */
 @Composable
-fun rememberSelectionState(): MutableSelectionState {
-    return remember { MutableSelectionState() }
+fun rememberSelectionState(
+    onResize: (TileSpec) -> Unit,
+    onResizeEnd: (TileSpec) -> Unit,
+): MutableSelectionState {
+    return remember { MutableSelectionState(onResize, onResizeEnd) }
 }
 
+/**
+ * Holds the selected [TileSpec] and whether the selection was manual, i.e. caused by a tap from the
+ * user.
+ */
+data class Selection(val tileSpec: TileSpec, val manual: Boolean)
+
 /** Holds the state of the current selection. */
-class MutableSelectionState {
-    private var _selectedTile = mutableStateOf<TileSpec?>(null)
+class MutableSelectionState(
+    val onResize: (TileSpec) -> Unit,
+    private val onResizeEnd: (TileSpec) -> Unit,
+) {
+    private var _selection = mutableStateOf<Selection?>(null)
     private var _resizingState = mutableStateOf<ResizingState?>(null)
 
+    /** The [Selection] if a tile is selected, null if not. */
+    val selection by _selection
+
     /** The [ResizingState] of the selected tile is currently being resized, null if not. */
     val resizingState by _resizingState
 
-    fun isSelected(tileSpec: TileSpec): Boolean {
-        return _selectedTile.value?.let { it == tileSpec } ?: false
-    }
-
-    fun select(tileSpec: TileSpec) {
-        _selectedTile.value = tileSpec
+    fun select(tileSpec: TileSpec, manual: Boolean) {
+        _selection.value = Selection(tileSpec, manual)
     }
 
     fun unSelect() {
-        _selectedTile.value = null
+        _selection.value = null
         onResizingDragEnd()
     }
 
@@ -56,14 +67,21 @@
         _resizingState.value?.onDrag(offset)
     }
 
-    fun onResizingDragStart(tileWidths: TileWidths, onResize: () -> Unit) {
-        if (_selectedTile.value == null) return
-
-        _resizingState.value = ResizingState(tileWidths, onResize)
+    fun onResizingDragStart(tileWidths: TileWidths) {
+        _selection.value?.let {
+            _resizingState.value = ResizingState(tileWidths) { onResize(it.tileSpec) }
+        }
     }
 
     fun onResizingDragEnd() {
         _resizingState.value = null
+        _selection.value?.let {
+            onResizeEnd(it.tileSpec)
+
+            // Mark the selection as automatic in case the tile ends up moving to a different
+            // row with its new size.
+            _selection.value = it.copy(manual = false)
+        }
     }
 }
 
@@ -76,10 +94,10 @@
     return pointerInput(Unit) {
         detectTapGestures(
             onTap = {
-                if (selectionState.isSelected(tileSpec)) {
+                if (selectionState.selection?.tileSpec == tileSpec) {
                     selectionState.unSelect()
                 } else {
-                    selectionState.select(tileSpec)
+                    selectionState.select(tileSpec, manual = true)
                 }
             }
         )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
index e3acf38..7c62e59 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
@@ -45,7 +45,6 @@
     selectionState: MutableSelectionState,
     transition: () -> Float,
     tileWidths: () -> TileWidths? = { null },
-    onResize: () -> Unit = {},
 ) {
     if (enabled) {
         // Manually creating the touch target around the resizing dot to ensure that the next tile
@@ -56,9 +55,7 @@
             Modifier.size(minTouchTargetSize).pointerInput(Unit) {
                 detectHorizontalDragGestures(
                     onHorizontalDrag = { _, offset -> selectionState.onResizingDrag(offset) },
-                    onDragStart = {
-                        tileWidths()?.let { selectionState.onResizingDragStart(it, onResize) }
-                    },
+                    onDragStart = { tileWidths()?.let { selectionState.onResizingDragStart(it) } },
                     onDragEnd = selectionState::onResizingDragEnd,
                     onDragCancel = selectionState::onResizingDragEnd,
                 )
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt
index b16a707..b1841c4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt
@@ -27,6 +27,7 @@
 sealed interface GridCell {
     val row: Int
     val span: GridItemSpan
+    val s: String
 }
 
 /**
@@ -39,6 +40,7 @@
     override val row: Int,
     override val width: Int,
     override val span: GridItemSpan = GridItemSpan(width),
+    override val s: String = "${tile.tileSpec.spec}-$row-$width",
 ) : GridCell, SizedTile<EditTileViewModel>, CategoryAndName by tile {
     val key: String = "${tile.tileSpec.spec}-$row"
 
@@ -53,22 +55,30 @@
 data class SpacerGridCell(
     override val row: Int,
     override val span: GridItemSpan = GridItemSpan(1),
+    override val s: String = "spacer",
 ) : GridCell
 
+/**
+ * Generates a list of [GridCell] from a list of [SizedTile]
+ *
+ * Builds rows based on the tiles' widths, and fill each hole with a [SpacerGridCell]
+ *
+ * @param startingRow The row index the grid is built from, used in cases where only end rows need
+ *   to be regenerated
+ */
 fun List<SizedTile<EditTileViewModel>>.toGridCells(
     columns: Int,
-    includeSpacers: Boolean = false,
+    startingRow: Int = 0,
 ): List<GridCell> {
     return splitInRowsSequence(this, columns)
         .flatMapIndexed { rowIndex, sizedTiles ->
-            val row: List<GridCell> = sizedTiles.map { TileGridCell(it, rowIndex) }
+            val correctedRowIndex = rowIndex + startingRow
+            val row: List<GridCell> = sizedTiles.map { TileGridCell(it, correctedRowIndex) }
 
-            if (includeSpacers) {
-                // Fill the incomplete rows with spacers
-                val numSpacers = columns - sizedTiles.sumOf { it.width }
-                row.toMutableList().apply { repeat(numSpacers) { add(SpacerGridCell(rowIndex)) } }
-            } else {
-                row
+            // Fill the incomplete rows with spacers
+            val numSpacers = columns - sizedTiles.sumOf { it.width }
+            row.toMutableList().apply {
+                repeat(numSpacers) { add(SpacerGridCell(correctedRowIndex)) }
             }
         }
         .toList()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt
index b604e18..4e698ed 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt
@@ -27,7 +27,7 @@
 
     fun isIconTile(spec: TileSpec): Boolean
 
-    fun resize(spec: TileSpec)
+    fun resize(spec: TileSpec, toIcon: Boolean)
 }
 
 @SysUISingleton
@@ -37,5 +37,5 @@
 
     override fun isIconTile(spec: TileSpec): Boolean = interactor.isIconTile(spec)
 
-    override fun resize(spec: TileSpec) = interactor.resize(spec)
+    override fun resize(spec: TileSpec, toIcon: Boolean) = interactor.resize(spec, toIcon)
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
index 6423d25..8d060e9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
@@ -60,13 +60,13 @@
         onSetTiles: (List<TileSpec>) -> Unit,
     ) {
         DefaultEditTileGrid(
-            currentListState = listState,
+            listState = listState,
             otherTiles = listOf(),
             columns = 4,
             modifier = Modifier.fillMaxSize(),
             onRemoveTile = {},
             onSetTiles = onSetTiles,
-            onResize = {},
+            onResize = { _, _ -> },
         )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
index 682ed92..ee1c0e9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
@@ -25,7 +25,11 @@
 import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.performClick
 import androidx.compose.ui.test.performCustomAccessibilityActionWithLabel
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeLeft
+import androidx.compose.ui.test.swipeRight
 import androidx.compose.ui.text.AnnotatedString
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -43,15 +47,19 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@OptIn(ExperimentalTestApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class ResizingTest : SysuiTestCase() {
     @get:Rule val composeRule = createComposeRule()
 
     @Composable
-    private fun EditTileGridUnderTest(listState: EditTileListState, onResize: (TileSpec) -> Unit) {
+    private fun EditTileGridUnderTest(
+        listState: EditTileListState,
+        onResize: (TileSpec, Boolean) -> Unit,
+    ) {
         DefaultEditTileGrid(
-            currentListState = listState,
+            listState = listState,
             otherTiles = listOf(),
             columns = 4,
             modifier = Modifier.fillMaxSize(),
@@ -61,22 +69,12 @@
         )
     }
 
-    @OptIn(ExperimentalTestApi::class)
     @Test
-    fun resizedIcon_shouldBeLarge() {
+    fun toggleIconTile_shouldBeLarge() {
         var tiles by mutableStateOf(TestEditTiles)
         val listState = EditTileListState(tiles, 4)
         composeRule.setContent {
-            EditTileGridUnderTest(listState) { spec ->
-                tiles =
-                    tiles.map {
-                        if (it.tile.tileSpec == spec) {
-                            toggleWidth(it)
-                        } else {
-                            it
-                        }
-                    }
-            }
+            EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) }
         }
         composeRule.waitForIdle()
 
@@ -87,22 +85,12 @@
         assertThat(tiles.find { it.tile.tileSpec.spec == "tileA" }?.width).isEqualTo(2)
     }
 
-    @OptIn(ExperimentalTestApi::class)
     @Test
-    fun resizedLarge_shouldBeIcon() {
+    fun toggleLargeTile_shouldBeIcon() {
         var tiles by mutableStateOf(TestEditTiles)
         val listState = EditTileListState(tiles, 4)
         composeRule.setContent {
-            EditTileGridUnderTest(listState) { spec ->
-                tiles =
-                    tiles.map {
-                        if (it.tile.tileSpec == spec) {
-                            toggleWidth(it)
-                        } else {
-                            it
-                        }
-                    }
-            }
+            EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) }
         }
         composeRule.waitForIdle()
 
@@ -113,9 +101,58 @@
         assertThat(tiles.find { it.tile.tileSpec.spec == "tileD_large" }?.width).isEqualTo(1)
     }
 
+    @Test
+    fun resizedLarge_shouldBeIcon() {
+        var tiles by mutableStateOf(TestEditTiles)
+        val listState = EditTileListState(tiles, 4)
+        composeRule.setContent {
+            EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) }
+        }
+        composeRule.waitForIdle()
+
+        composeRule
+            .onNodeWithContentDescription("tileA")
+            .performClick() // Select
+            .performTouchInput { // Resize down
+                swipeRight()
+            }
+        composeRule.waitForIdle()
+
+        assertThat(tiles.find { it.tile.tileSpec.spec == "tileA" }?.width).isEqualTo(1)
+    }
+
+    @Test
+    fun resizedIcon_shouldBeLarge() {
+        var tiles by mutableStateOf(TestEditTiles)
+        val listState = EditTileListState(tiles, 4)
+        composeRule.setContent {
+            EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) }
+        }
+        composeRule.waitForIdle()
+
+        composeRule
+            .onNodeWithContentDescription("tileD_large")
+            .performClick() // Select
+            .performTouchInput { // Resize down
+                swipeLeft()
+            }
+        composeRule.waitForIdle()
+
+        assertThat(tiles.find { it.tile.tileSpec.spec == "tileD_large" }?.width).isEqualTo(1)
+    }
+
     companion object {
-        private fun toggleWidth(tile: SizedTile<EditTileViewModel>): SizedTile<EditTileViewModel> {
-            return SizedTileImpl(tile.tile, width = if (tile.isIcon) 2 else 1)
+        private fun List<SizedTile<EditTileViewModel>>.resize(
+            spec: TileSpec,
+            toIcon: Boolean,
+        ): List<SizedTile<EditTileViewModel>> {
+            return map {
+                if (it.tile.tileSpec == spec) {
+                    SizedTileImpl(it.tile, width = if (toIcon) 1 else 2)
+                } else {
+                    it
+                }
+            }
         }
 
         private fun createEditTile(tileSpec: String): SizedTile<EditTileViewModel> {