Implement stretch option for QS tiles

Rows with holes with stretch tiles to fill in empty spaces (except last row).
Edit mode reuses the same composable as InfiniteGridLayout, and the grid consistency interactor doesn't do any reordering.
This change also adds a layout selector to swap between prototypes.

Fix: 340246005
Flag: ACONFIG com.android.systemui.qs_ui_refactor DEVELOPMENT
Test: manually using layout selector on QSActivity

Change-Id: Ie4dbb81f99e3e6f0c00233974d518ec8b8612282
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
index 0696fbe..2cc3985 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
@@ -29,8 +29,10 @@
 import com.android.systemui.qs.panels.shared.model.GridConsistencyLog
 import com.android.systemui.qs.panels.shared.model.GridLayoutType
 import com.android.systemui.qs.panels.shared.model.InfiniteGridLayoutType
+import com.android.systemui.qs.panels.shared.model.StretchedGridLayoutType
 import com.android.systemui.qs.panels.ui.compose.GridLayout
 import com.android.systemui.qs.panels.ui.compose.InfiniteGridLayout
+import com.android.systemui.qs.panels.ui.compose.StretchedGridLayout
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
@@ -63,6 +65,14 @@
         }
 
         @Provides
+        @IntoSet
+        fun provideStretchedGridLayout(
+            gridLayout: StretchedGridLayout
+        ): Pair<GridLayoutType, GridLayout> {
+            return Pair(StretchedGridLayoutType, gridLayout)
+        }
+
+        @Provides
         fun provideGridLayoutMap(
             entries: Set<@JvmSuppressWildcards Pair<GridLayoutType, GridLayout>>
         ): Map<GridLayoutType, GridLayout> {
@@ -70,6 +80,13 @@
         }
 
         @Provides
+        fun provideGridLayoutTypes(
+            entries: Set<@JvmSuppressWildcards Pair<GridLayoutType, GridLayout>>
+        ): Set<GridLayoutType> {
+            return entries.map { it.first }.toSet()
+        }
+
+        @Provides
         @IntoSet
         fun provideGridConsistencyInteractor(
             consistencyInteractor: InfiniteGridConsistencyInteractor
@@ -78,6 +95,14 @@
         }
 
         @Provides
+        @IntoSet
+        fun provideStretchedGridConsistencyInteractor(
+            consistencyInteractor: NoopGridConsistencyInteractor
+        ): Pair<GridLayoutType, GridTypeConsistencyInteractor> {
+            return Pair(StretchedGridLayoutType, consistencyInteractor)
+        }
+
+        @Provides
         fun provideGridConsistencyInteractorMap(
             entries: Set<@JvmSuppressWildcards Pair<GridLayoutType, GridTypeConsistencyInteractor>>
         ): Map<GridLayoutType, GridTypeConsistencyInteractor> {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/GridLayoutTypeRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/GridLayoutTypeRepository.kt
index 542d0cb..31795d5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/GridLayoutTypeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/GridLayoutTypeRepository.kt
@@ -26,10 +26,17 @@
 
 interface GridLayoutTypeRepository {
     val layout: StateFlow<GridLayoutType>
+    fun setLayout(type: GridLayoutType)
 }
 
 @SysUISingleton
 class GridLayoutTypeRepositoryImpl @Inject constructor() : GridLayoutTypeRepository {
     private val _layout: MutableStateFlow<GridLayoutType> = MutableStateFlow(InfiniteGridLayoutType)
     override val layout = _layout.asStateFlow()
+
+    override fun setLayout(type: GridLayoutType) {
+        if (_layout.value != type) {
+            _layout.value = type
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractor.kt
index b6be578..4af1b22 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/GridLayoutTypeInteractor.kt
@@ -20,9 +20,13 @@
 import com.android.systemui.qs.panels.data.repository.GridLayoutTypeRepository
 import com.android.systemui.qs.panels.shared.model.GridLayoutType
 import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
 
 @SysUISingleton
-class GridLayoutTypeInteractor @Inject constructor(repo: GridLayoutTypeRepository) {
-    val layout: Flow<GridLayoutType> = repo.layout
+class GridLayoutTypeInteractor @Inject constructor(private val repo: GridLayoutTypeRepository) {
+    val layout: StateFlow<GridLayoutType> = repo.layout
+
+    fun setLayoutType(type: GridLayoutType) {
+        repo.setLayout(type)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt
index 74e906c..b437f64 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt
@@ -18,6 +18,8 @@
 
 import android.util.Log
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.panels.shared.model.SizedTile
+import com.android.systemui.qs.panels.shared.model.TileRow
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import javax.inject.Inject
 
@@ -35,7 +37,7 @@
      */
     override fun reconcileTiles(tiles: List<TileSpec>): List<TileSpec> {
         val newTiles: MutableList<TileSpec> = mutableListOf()
-        val row = TileRow(columns = gridSizeInteractor.columns.value)
+        val row = TileRow<TileSpec>(columns = gridSizeInteractor.columns.value)
         val iconTilesSet = iconTilesInteractor.iconTilesSpecs.value
         val tilesQueue =
             ArrayDeque(
@@ -54,7 +56,7 @@
 
         while (tilesQueue.isNotEmpty()) {
             if (row.isFull()) {
-                newTiles.addAll(row.tileSpecs())
+                newTiles.addAll(row.tiles.map { it.tile })
                 row.clear()
             }
 
@@ -66,13 +68,13 @@
                 // We'll try to either add an icon tile from the queue to complete the row, or
                 // remove an icon tile from the current row to free up space.
 
-                val iconTile: SizedTile? = tilesQueue.firstOrNull { it.width == 1 }
+                val iconTile: SizedTile<TileSpec>? = tilesQueue.firstOrNull { it.width == 1 }
                 if (iconTile != null) {
                     tilesQueue.remove(iconTile)
                     tilesQueue.addFirst(tile)
                     row.maybeAddTile(iconTile)
                 } else {
-                    val tileToRemove: SizedTile? = row.findLastIconTile()
+                    val tileToRemove: SizedTile<TileSpec>? = row.findLastIconTile()
                     if (tileToRemove != null) {
                         row.removeTile(tileToRemove)
                         row.maybeAddTile(tile)
@@ -84,7 +86,7 @@
                         // If the row does not have an icon tile, add the incomplete row.
                         // Note: this shouldn't happen because an icon tile is guaranteed to be in a
                         // row that doesn't have enough space for a large tile.
-                        val tileSpecs = row.tileSpecs()
+                        val tileSpecs = row.tiles.map { it.tile }
                         Log.wtf(TAG, "Uneven row does not have an icon tile to remove: $tileSpecs")
                         newTiles.addAll(tileSpecs)
                         row.clear()
@@ -95,48 +97,11 @@
         }
 
         // Add last row that might be incomplete
-        newTiles.addAll(row.tileSpecs())
+        newTiles.addAll(row.tiles.map { it.tile })
 
         return newTiles.toList()
     }
 
-    /** Tile with a width representing the number of columns it should take. */
-    private data class SizedTile(val spec: TileSpec, val width: Int)
-
-    private class TileRow(private val columns: Int) {
-        private var availableColumns = columns
-        private val tiles: MutableList<SizedTile> = mutableListOf()
-
-        fun tileSpecs(): List<TileSpec> {
-            return tiles.map { it.spec }
-        }
-
-        fun maybeAddTile(tile: SizedTile): Boolean {
-            if (availableColumns - tile.width >= 0) {
-                tiles.add(tile)
-                availableColumns -= tile.width
-                return true
-            }
-            return false
-        }
-
-        fun findLastIconTile(): SizedTile? {
-            return tiles.findLast { it.width == 1 }
-        }
-
-        fun removeTile(tile: SizedTile) {
-            tiles.remove(tile)
-            availableColumns += tile.width
-        }
-
-        fun clear() {
-            tiles.clear()
-            availableColumns = columns
-        }
-
-        fun isFull(): Boolean = availableColumns == 0
-    }
-
     private companion object {
         const val TAG = "InfiniteGridConsistencyInteractor"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/NoopConsistencyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/NoopConsistencyInteractor.kt
new file mode 100644
index 0000000..97ceacc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/NoopConsistencyInteractor.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.panels.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import javax.inject.Inject
+
+@SysUISingleton
+class NoopConsistencyInteractor @Inject constructor() : GridTypeConsistencyInteractor {
+    override fun reconcileTiles(tiles: List<TileSpec>): List<TileSpec> = tiles
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/GridLayoutType.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/GridLayoutType.kt
index 23110dc..501730a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/GridLayoutType.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/GridLayoutType.kt
@@ -25,3 +25,9 @@
 
 /** Grid type representing a scrollable vertical grid. */
 data object InfiniteGridLayoutType : GridLayoutType
+
+/**
+ * Grid type representing a scrollable vertical grid where tiles will stretch to fill in empty
+ * spaces.
+ */
+data object StretchedGridLayoutType : GridLayoutType
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/TileRow.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/TileRow.kt
new file mode 100644
index 0000000..7e4381b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/TileRow.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.panels.shared.model
+
+/** Represents a tile of type [T] associated with a width */
+data class SizedTile<T>(val tile: T, val width: Int)
+
+/** Represents a row of [SizedTile] with a maximum width of [columns] */
+class TileRow<T>(private val columns: Int) {
+    private var availableColumns = columns
+    private val _tiles: MutableList<SizedTile<T>> = mutableListOf()
+    val tiles: List<SizedTile<T>>
+        get() = _tiles.toList()
+
+    fun maybeAddTile(tile: SizedTile<T>): Boolean {
+        if (availableColumns - tile.width >= 0) {
+            _tiles.add(tile)
+            availableColumns -= tile.width
+            return true
+        }
+        return false
+    }
+
+    fun findLastIconTile(): SizedTile<T>? {
+        return _tiles.findLast { it.width == 1 }
+    }
+
+    fun removeTile(tile: SizedTile<T>) {
+        _tiles.remove(tile)
+        availableColumns += tile.width
+    }
+
+    fun clear() {
+        _tiles.clear()
+        availableColumns = columns
+    }
+
+    fun isFull(): Boolean = availableColumns == 0
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
index bac0f60..f5ee720 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
@@ -16,85 +16,23 @@
 
 package com.android.systemui.qs.panels.ui.compose
 
-import android.graphics.drawable.Animatable
-import android.text.TextUtils
-import androidx.appcompat.content.res.AppCompatResources
-import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
-import androidx.compose.animation.graphics.res.animatedVectorResource
-import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
-import androidx.compose.animation.graphics.vector.AnimatedImageVector
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.basicMarquee
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.combinedClickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Arrangement.spacedBy
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.lazy.grid.GridCells
 import androidx.compose.foundation.lazy.grid.GridItemSpan
-import androidx.compose.foundation.lazy.grid.LazyGridScope
-import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.Remove
-import androidx.compose.material3.Icon
-import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.ColorFilter
-import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.dimensionResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.contentDescription
-import androidx.compose.ui.semantics.onClick
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.stateDescription
-import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.android.compose.animation.Expandable
-import com.android.compose.theme.colorAttr
-import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.common.ui.compose.Icon
-import com.android.systemui.common.ui.compose.load
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.qs.panels.domain.interactor.IconTilesInteractor
 import com.android.systemui.qs.panels.domain.interactor.InfiniteGridSizeInteractor
-import com.android.systemui.qs.panels.ui.viewmodel.ActiveTileColorAttributes
-import com.android.systemui.qs.panels.ui.viewmodel.AvailableEditActions
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.TileColorAttributes
-import com.android.systemui.qs.panels.ui.viewmodel.TileUiState
 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.toUiState
-import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor.Companion.POSITION_AT_END
 import com.android.systemui.qs.pipeline.shared.TileSpec
-import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.res.R
 import javax.inject.Inject
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.mapLatest
 
 @SysUISingleton
 class InfiniteGridLayout
@@ -104,8 +42,6 @@
     private val gridSizeInteractor: InfiniteGridSizeInteractor
 ) : GridLayout {
 
-    private object TileType
-
     @Composable
     override fun TileGrid(
         tiles: List<TileViewModel>,
@@ -140,55 +76,6 @@
         }
     }
 
-    @OptIn(ExperimentalCoroutinesApi::class, ExperimentalFoundationApi::class)
-    @Composable
-    private fun Tile(
-        tile: TileViewModel,
-        iconOnly: Boolean,
-        modifier: Modifier,
-    ) {
-        val state: TileUiState by
-            tile.state
-                .mapLatest { it.toUiState() }
-                .collectAsStateWithLifecycle(initialValue = tile.currentState.toUiState())
-        val context = LocalContext.current
-
-        Expandable(
-            color = colorAttr(state.colors.background),
-            shape = RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius)),
-        ) {
-            Row(
-                modifier =
-                    modifier
-                        .combinedClickable(
-                            onClick = { tile.onClick(it) },
-                            onLongClick = { tile.onLongClick(it) }
-                        )
-                        .tileModifier(state.colors),
-                verticalAlignment = Alignment.CenterVertically,
-                horizontalArrangement = tileHorizontalArrangement(iconOnly),
-            ) {
-                val icon =
-                    remember(state.icon) {
-                        state.icon.get().let {
-                            if (it is QSTileImpl.ResourceIcon) {
-                                Icon.Resource(it.resId, null)
-                            } else {
-                                Icon.Loaded(it.getDrawable(context), null)
-                            }
-                        }
-                    }
-                TileContent(
-                    label = state.label.toString(),
-                    secondaryLabel = state.secondaryLabel?.toString(),
-                    icon = icon,
-                    colors = state.colors,
-                    iconOnly = iconOnly
-                )
-            }
-        }
-    }
-
     @Composable
     override fun EditTileGrid(
         tiles: List<EditTileViewModel>,
@@ -196,262 +83,16 @@
         onAddTile: (TileSpec, Int) -> Unit,
         onRemoveTile: (TileSpec) -> Unit,
     ) {
-        val (currentTiles, otherTiles) = tiles.partition { it.isCurrent }
-        val (otherTilesStock, otherTilesCustom) = otherTiles.partition { it.appName == null }
-        val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState {
-            onAddTile(it, POSITION_AT_END)
-        }
-        val iconOnlySpecs by
-            iconTilesInteractor.iconTilesSpecs.collectAsStateWithLifecycle(
-                initialValue = emptySet()
-            )
-        val isIconOnly: (TileSpec) -> Boolean =
-            remember(iconOnlySpecs) { { tileSpec: TileSpec -> tileSpec in iconOnlySpecs } }
+        val iconOnlySpecs by iconTilesInteractor.iconTilesSpecs.collectAsStateWithLifecycle()
         val columns by gridSizeInteractor.columns.collectAsStateWithLifecycle()
 
-        TileLazyGrid(modifier = modifier, columns = GridCells.Fixed(columns)) {
-            // These Text are just placeholders to see the different sections. Not final UI.
-            item(span = { GridItemSpan(maxLineSpan) }) {
-                Text("Current tiles", color = Color.White)
-            }
-
-            editTiles(
-                currentTiles,
-                ClickAction.REMOVE,
-                onRemoveTile,
-                isIconOnly,
-                indicatePosition = true,
-            )
-
-            item(span = { GridItemSpan(maxLineSpan) }) { Text("Tiles to add", color = Color.White) }
-
-            editTiles(
-                otherTilesStock,
-                ClickAction.ADD,
-                addTileToEnd,
-                isIconOnly,
-            )
-
-            item(span = { GridItemSpan(maxLineSpan) }) {
-                Text("Custom tiles to add", color = Color.White)
-            }
-
-            editTiles(
-                otherTilesCustom,
-                ClickAction.ADD,
-                addTileToEnd,
-                isIconOnly,
-            )
-        }
-    }
-
-    private fun LazyGridScope.editTiles(
-        tiles: List<EditTileViewModel>,
-        clickAction: ClickAction,
-        onClick: (TileSpec) -> Unit,
-        isIconOnly: (TileSpec) -> Boolean,
-        indicatePosition: Boolean = false,
-    ) {
-        items(
-            count = tiles.size,
-            key = { tiles[it].tileSpec.spec },
-            span = { GridItemSpan(if (isIconOnly(tiles[it].tileSpec)) 1 else 2) },
-            contentType = { TileType }
-        ) {
-            val viewModel = tiles[it]
-            val canClick =
-                when (clickAction) {
-                    ClickAction.ADD -> AvailableEditActions.ADD in viewModel.availableEditActions
-                    ClickAction.REMOVE ->
-                        AvailableEditActions.REMOVE in viewModel.availableEditActions
-                }
-            val onClickActionName =
-                when (clickAction) {
-                    ClickAction.ADD ->
-                        stringResource(id = R.string.accessibility_qs_edit_tile_add_action)
-                    ClickAction.REMOVE ->
-                        stringResource(id = R.string.accessibility_qs_edit_remove_tile_action)
-                }
-            val stateDescription =
-                if (indicatePosition) {
-                    stringResource(id = R.string.accessibility_qs_edit_position, it + 1)
-                } else {
-                    ""
-                }
-
-            Box(
-                modifier =
-                    Modifier.clickable(enabled = canClick) { onClick.invoke(viewModel.tileSpec) }
-                        .animateItem()
-                        .semantics {
-                            onClick(onClickActionName) { false }
-                            this.stateDescription = stateDescription
-                        }
-            ) {
-                EditTile(
-                    tileViewModel = viewModel,
-                    isIconOnly(viewModel.tileSpec),
-                    modifier = Modifier.height(dimensionResource(id = R.dimen.qs_tile_height))
-                )
-                if (canClick) {
-                    Badge(clickAction, Modifier.align(Alignment.TopEnd))
-                }
-            }
-        }
-    }
-
-    @Composable
-    private fun Badge(action: ClickAction, modifier: Modifier = Modifier) {
-        Box(modifier = modifier.size(16.dp).background(Color.Cyan, shape = CircleShape)) {
-            Icon(
-                imageVector =
-                    when (action) {
-                        ClickAction.ADD -> Icons.Filled.Add
-                        ClickAction.REMOVE -> Icons.Filled.Remove
-                    },
-                "",
-                tint = Color.Black,
-            )
-        }
-    }
-
-    @Composable
-    private fun EditTile(
-        tileViewModel: EditTileViewModel,
-        iconOnly: Boolean,
-        modifier: Modifier = Modifier,
-    ) {
-        val label = tileViewModel.label.load() ?: tileViewModel.tileSpec.spec
-        val colors = ActiveTileColorAttributes
-
-        Row(
-            modifier = modifier.tileModifier(colors).semantics { this.contentDescription = label },
-            verticalAlignment = Alignment.CenterVertically,
-            horizontalArrangement = tileHorizontalArrangement(iconOnly)
-        ) {
-            TileContent(
-                label = label,
-                secondaryLabel = tileViewModel.appName?.load(),
-                colors = colors,
-                icon = tileViewModel.icon,
-                iconOnly = iconOnly,
-                animateIconToEnd = true,
-            )
-        }
-    }
-
-    private enum class ClickAction {
-        ADD,
-        REMOVE,
-    }
-}
-
-@OptIn(ExperimentalAnimationGraphicsApi::class)
-@Composable
-private fun TileIcon(
-    icon: Icon,
-    color: Color,
-    animateToEnd: Boolean = false,
-) {
-    val modifier = Modifier.size(dimensionResource(id = R.dimen.qs_icon_size))
-    val context = LocalContext.current
-    val loadedDrawable =
-        remember(icon, context) {
-            when (icon) {
-                is Icon.Loaded -> icon.drawable
-                is Icon.Resource -> AppCompatResources.getDrawable(context, icon.res)
-            }
-        }
-    if (loadedDrawable !is Animatable) {
-        Icon(
-            icon = icon,
-            tint = color,
+        DefaultEditTileGrid(
+            tiles = tiles,
+            iconOnlySpecs = iconOnlySpecs,
+            columns = GridCells.Fixed(columns),
             modifier = modifier,
+            onAddTile = onAddTile,
+            onRemoveTile = onRemoveTile,
         )
-    } else if (icon is Icon.Resource) {
-        val image = AnimatedImageVector.animatedVectorResource(id = icon.res)
-        val painter =
-            if (animateToEnd) {
-                rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = true)
-            } else {
-                var atEnd by remember(icon.res) { mutableStateOf(false) }
-                LaunchedEffect(key1 = icon.res) {
-                    delay(350)
-                    atEnd = true
-                }
-                rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd)
-            }
-        Image(
-            painter = painter,
-            contentDescription = null,
-            colorFilter = ColorFilter.tint(color = color),
-            modifier = modifier
-        )
-    }
-}
-
-@Composable
-private fun TileLazyGrid(
-    modifier: Modifier = Modifier,
-    columns: GridCells,
-    content: LazyGridScope.() -> Unit,
-) {
-    LazyVerticalGrid(
-        columns = columns,
-        verticalArrangement = spacedBy(dimensionResource(R.dimen.qs_tile_margin_vertical)),
-        horizontalArrangement = spacedBy(dimensionResource(R.dimen.qs_tile_margin_horizontal)),
-        modifier = modifier,
-        content = content,
-    )
-}
-
-@Composable
-private fun Modifier.tileModifier(colors: TileColorAttributes): Modifier {
-    return fillMaxWidth()
-        .clip(RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius)))
-        .background(colorAttr(colors.background))
-        .padding(horizontal = dimensionResource(id = R.dimen.qs_label_container_margin))
-}
-
-@Composable
-private fun tileHorizontalArrangement(iconOnly: Boolean): Arrangement.Horizontal {
-    val horizontalAlignment =
-        if (iconOnly) {
-            Alignment.CenterHorizontally
-        } else {
-            Alignment.Start
-        }
-    return spacedBy(
-        space = dimensionResource(id = R.dimen.qs_label_container_margin),
-        alignment = horizontalAlignment
-    )
-}
-
-@Composable
-private fun TileContent(
-    label: String,
-    secondaryLabel: String?,
-    icon: Icon,
-    colors: TileColorAttributes,
-    iconOnly: Boolean,
-    animateIconToEnd: Boolean = false,
-) {
-    TileIcon(icon, colorAttr(colors.icon), animateIconToEnd)
-
-    if (!iconOnly) {
-        Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) {
-            Text(
-                label,
-                color = colorAttr(colors.label),
-                modifier = Modifier.basicMarquee(),
-            )
-            if (!TextUtils.isEmpty(secondaryLabel)) {
-                Text(
-                    secondaryLabel ?: "",
-                    color = colorAttr(colors.secondaryLabel),
-                    modifier = Modifier.basicMarquee(),
-                )
-            }
-        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt
new file mode 100644
index 0000000..ddd97c2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt
@@ -0,0 +1,139 @@
+/*
+ * 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.panels.ui.compose
+
+import androidx.compose.foundation.layout.height
+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.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.panels.domain.interactor.IconTilesInteractor
+import com.android.systemui.qs.panels.domain.interactor.InfiniteGridSizeInteractor
+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
+import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+@SysUISingleton
+class StretchedGridLayout
+@Inject
+constructor(
+    private val iconTilesInteractor: IconTilesInteractor,
+    private val gridSizeInteractor: InfiniteGridSizeInteractor,
+) : GridLayout {
+
+    @Composable
+    override fun TileGrid(
+        tiles: List<TileViewModel>,
+        modifier: Modifier,
+    ) {
+        DisposableEffect(tiles) {
+            val token = Any()
+            tiles.forEach { it.startListening(token) }
+            onDispose { tiles.forEach { it.stopListening(token) } }
+        }
+
+        // Tile widths [normal|stretched]
+        // Icon [3 | 4]
+        // Large [6 | 8]
+        val columns = 12
+        val iconTilesSpecs by iconTilesInteractor.iconTilesSpecs.collectAsStateWithLifecycle()
+        val stretchedTiles =
+            remember(tiles) {
+                val sizedTiles =
+                    tiles.map {
+                        SizedTile(
+                            it,
+                            if (iconTilesSpecs.contains(it.spec)) {
+                                3
+                            } else {
+                                6
+                            }
+                        )
+                    }
+                splitInRows(sizedTiles, columns)
+            }
+
+        TileLazyGrid(columns = GridCells.Fixed(columns), modifier = modifier) {
+            items(stretchedTiles.size, span = { GridItemSpan(stretchedTiles[it].width) }) { index ->
+                Tile(
+                    stretchedTiles[index].tile,
+                    iconTilesSpecs.contains(stretchedTiles[index].tile.spec),
+                    Modifier.height(dimensionResource(id = R.dimen.qs_tile_height))
+                )
+            }
+        }
+    }
+
+    @Composable
+    override fun EditTileGrid(
+        tiles: List<EditTileViewModel>,
+        modifier: Modifier,
+        onAddTile: (TileSpec, Int) -> Unit,
+        onRemoveTile: (TileSpec) -> Unit
+    ) {
+        val iconOnlySpecs by iconTilesInteractor.iconTilesSpecs.collectAsStateWithLifecycle()
+        val columns by gridSizeInteractor.columns.collectAsStateWithLifecycle()
+
+        DefaultEditTileGrid(
+            tiles = tiles,
+            iconOnlySpecs = iconOnlySpecs,
+            columns = GridCells.Fixed(columns),
+            modifier = modifier,
+            onAddTile = onAddTile,
+            onRemoveTile = onRemoveTile,
+        )
+    }
+
+    private fun splitInRows(
+        tiles: List<SizedTile<TileViewModel>>,
+        columns: Int
+    ): List<SizedTile<TileViewModel>> {
+        val row = TileRow<TileViewModel>(columns)
+
+        return buildList {
+            for (tile in tiles) {
+                if (row.maybeAddTile(tile)) {
+                    if (row.isFull()) {
+                        // Row is full, no need to stretch tiles
+                        addAll(row.tiles)
+                        row.clear()
+                    }
+                } else {
+                    if (row.isFull()) {
+                        addAll(row.tiles)
+                    } else {
+                        // Stretching tiles when row isn't full
+                        addAll(row.tiles.map { it.copy(width = it.width + (it.width / 3)) })
+                    }
+                    row.clear()
+                    row.maybeAddTile(tile)
+                }
+            }
+            addAll(row.tiles)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
new file mode 100644
index 0000000..eb45110
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
@@ -0,0 +1,403 @@
+/*
+ * 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.panels.ui.compose
+
+import android.graphics.drawable.Animatable
+import android.text.TextUtils
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
+import androidx.compose.animation.graphics.res.animatedVectorResource
+import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
+import androidx.compose.animation.graphics.vector.AnimatedImageVector
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.basicMarquee
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Arrangement.spacedBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.GridItemSpan
+import androidx.compose.foundation.lazy.grid.LazyGridScope
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Remove
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.onClick
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.compose.animation.Expandable
+import com.android.compose.theme.colorAttr
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.ui.compose.Icon
+import com.android.systemui.common.ui.compose.load
+import com.android.systemui.qs.panels.ui.viewmodel.ActiveTileColorAttributes
+import com.android.systemui.qs.panels.ui.viewmodel.AvailableEditActions
+import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.TileColorAttributes
+import com.android.systemui.qs.panels.ui.viewmodel.TileUiState
+import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.toUiState
+import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tileimpl.QSTileImpl
+import com.android.systemui.res.R
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.mapLatest
+
+object TileType
+
+@OptIn(ExperimentalCoroutinesApi::class, ExperimentalFoundationApi::class)
+@Composable
+fun Tile(
+    tile: TileViewModel,
+    iconOnly: Boolean,
+    modifier: Modifier,
+) {
+    val state: TileUiState by
+        tile.state
+            .mapLatest { it.toUiState() }
+            .collectAsStateWithLifecycle(tile.currentState.toUiState())
+    val context = LocalContext.current
+
+    Expandable(
+        color = colorAttr(state.colors.background),
+        shape = RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius)),
+    ) {
+        Row(
+            modifier =
+                modifier
+                    .combinedClickable(
+                        onClick = { tile.onClick(it) },
+                        onLongClick = { tile.onLongClick(it) }
+                    )
+                    .tileModifier(state.colors),
+            verticalAlignment = Alignment.CenterVertically,
+            horizontalArrangement = tileHorizontalArrangement(iconOnly),
+        ) {
+            val icon =
+                remember(state.icon) {
+                    state.icon.get().let {
+                        if (it is QSTileImpl.ResourceIcon) {
+                            Icon.Resource(it.resId, null)
+                        } else {
+                            Icon.Loaded(it.getDrawable(context), null)
+                        }
+                    }
+                }
+            TileContent(
+                label = state.label.toString(),
+                secondaryLabel = state.secondaryLabel.toString(),
+                icon = icon,
+                colors = state.colors,
+                iconOnly = iconOnly
+            )
+        }
+    }
+}
+
+@Composable
+fun TileLazyGrid(
+    modifier: Modifier = Modifier,
+    columns: GridCells,
+    content: LazyGridScope.() -> Unit,
+) {
+    LazyVerticalGrid(
+        columns = columns,
+        verticalArrangement = spacedBy(dimensionResource(R.dimen.qs_tile_margin_vertical)),
+        horizontalArrangement = spacedBy(dimensionResource(R.dimen.qs_tile_margin_horizontal)),
+        modifier = modifier,
+        content = content,
+    )
+}
+
+@Composable
+fun DefaultEditTileGrid(
+    tiles: List<EditTileViewModel>,
+    iconOnlySpecs: Set<TileSpec>,
+    columns: GridCells,
+    modifier: Modifier,
+    onAddTile: (TileSpec, Int) -> Unit,
+    onRemoveTile: (TileSpec) -> Unit,
+) {
+    val (currentTiles, otherTiles) = tiles.partition { it.isCurrent }
+    val (otherTilesStock, otherTilesCustom) = otherTiles.partition { it.appName == null }
+    val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState {
+        onAddTile(it, CurrentTilesInteractor.POSITION_AT_END)
+    }
+    val isIconOnly: (TileSpec) -> Boolean =
+        remember(iconOnlySpecs) { { tileSpec: TileSpec -> tileSpec in iconOnlySpecs } }
+
+    TileLazyGrid(modifier = modifier, columns = columns) {
+        // These Text are just placeholders to see the different sections. Not final UI.
+        item(span = { GridItemSpan(maxLineSpan) }) { Text("Current tiles", color = Color.White) }
+
+        editTiles(
+            currentTiles,
+            ClickAction.REMOVE,
+            onRemoveTile,
+            isIconOnly,
+            indicatePosition = true,
+        )
+
+        item(span = { GridItemSpan(maxLineSpan) }) { Text("Tiles to add", color = Color.White) }
+
+        editTiles(
+            otherTilesStock,
+            ClickAction.ADD,
+            addTileToEnd,
+            isIconOnly,
+        )
+
+        item(span = { GridItemSpan(maxLineSpan) }) {
+            Text("Custom tiles to add", color = Color.White)
+        }
+
+        editTiles(
+            otherTilesCustom,
+            ClickAction.ADD,
+            addTileToEnd,
+            isIconOnly,
+        )
+    }
+}
+
+private fun LazyGridScope.editTiles(
+    tiles: List<EditTileViewModel>,
+    clickAction: ClickAction,
+    onClick: (TileSpec) -> Unit,
+    isIconOnly: (TileSpec) -> Boolean,
+    indicatePosition: Boolean = false,
+) {
+    items(
+        count = tiles.size,
+        key = { tiles[it].tileSpec.spec },
+        span = { GridItemSpan(if (isIconOnly(tiles[it].tileSpec)) 1 else 2) },
+        contentType = { TileType }
+    ) {
+        val viewModel = tiles[it]
+        val canClick =
+            when (clickAction) {
+                ClickAction.ADD -> AvailableEditActions.ADD in viewModel.availableEditActions
+                ClickAction.REMOVE -> AvailableEditActions.REMOVE in viewModel.availableEditActions
+            }
+        val onClickActionName =
+            when (clickAction) {
+                ClickAction.ADD ->
+                    stringResource(id = R.string.accessibility_qs_edit_tile_add_action)
+                ClickAction.REMOVE ->
+                    stringResource(id = R.string.accessibility_qs_edit_remove_tile_action)
+            }
+        val stateDescription =
+            if (indicatePosition) {
+                stringResource(id = R.string.accessibility_qs_edit_position, it + 1)
+            } else {
+                ""
+            }
+
+        Box(
+            modifier =
+                Modifier.clickable(enabled = canClick) { onClick.invoke(viewModel.tileSpec) }
+                    .animateItem()
+                    .semantics {
+                        onClick(onClickActionName) { false }
+                        this.stateDescription = stateDescription
+                    }
+        ) {
+            EditTile(
+                tileViewModel = viewModel,
+                isIconOnly(viewModel.tileSpec),
+                modifier = Modifier.height(dimensionResource(id = R.dimen.qs_tile_height))
+            )
+            if (canClick) {
+                Badge(clickAction, Modifier.align(Alignment.TopEnd))
+            }
+        }
+    }
+}
+
+@Composable
+fun Badge(action: ClickAction, modifier: Modifier = Modifier) {
+    Box(modifier = modifier.size(16.dp).background(Color.Cyan, shape = CircleShape)) {
+        Icon(
+            imageVector =
+                when (action) {
+                    ClickAction.ADD -> Icons.Filled.Add
+                    ClickAction.REMOVE -> Icons.Filled.Remove
+                },
+            "",
+            tint = Color.Black,
+        )
+    }
+}
+
+@Composable
+fun EditTile(
+    tileViewModel: EditTileViewModel,
+    iconOnly: Boolean,
+    modifier: Modifier = Modifier,
+) {
+    val label = tileViewModel.label.load() ?: tileViewModel.tileSpec.spec
+    val colors = ActiveTileColorAttributes
+
+    Row(
+        modifier = modifier.tileModifier(colors).semantics { this.contentDescription = label },
+        verticalAlignment = Alignment.CenterVertically,
+        horizontalArrangement = tileHorizontalArrangement(iconOnly)
+    ) {
+        TileContent(
+            label = label,
+            secondaryLabel = tileViewModel.appName?.load(),
+            colors = colors,
+            icon = tileViewModel.icon,
+            iconOnly = iconOnly,
+            animateIconToEnd = true,
+        )
+    }
+}
+
+enum class ClickAction {
+    ADD,
+    REMOVE,
+}
+
+@OptIn(ExperimentalAnimationGraphicsApi::class)
+@Composable
+private fun TileIcon(
+    icon: Icon,
+    color: Color,
+    animateToEnd: Boolean = false,
+) {
+    val modifier = Modifier.size(dimensionResource(id = R.dimen.qs_icon_size))
+    val context = LocalContext.current
+    val loadedDrawable =
+        remember(icon, context) {
+            when (icon) {
+                is Icon.Loaded -> icon.drawable
+                is Icon.Resource -> AppCompatResources.getDrawable(context, icon.res)
+            }
+        }
+    if (loadedDrawable !is Animatable) {
+        Icon(
+            icon = icon,
+            tint = color,
+            modifier = modifier,
+        )
+    } else if (icon is Icon.Resource) {
+        val image = AnimatedImageVector.animatedVectorResource(id = icon.res)
+        val painter =
+            if (animateToEnd) {
+                rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = true)
+            } else {
+                var atEnd by remember(icon.res) { mutableStateOf(false) }
+                LaunchedEffect(key1 = icon.res) {
+                    delay(350)
+                    atEnd = true
+                }
+                rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd)
+            }
+        Image(
+            painter = painter,
+            contentDescription = null,
+            colorFilter = ColorFilter.tint(color = color),
+            modifier = modifier
+        )
+    }
+}
+
+@Composable
+private fun Modifier.tileModifier(colors: TileColorAttributes): Modifier {
+    return fillMaxWidth()
+        .clip(RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius)))
+        .background(colorAttr(colors.background))
+        .padding(horizontal = dimensionResource(id = R.dimen.qs_label_container_margin))
+}
+
+@Composable
+private fun tileHorizontalArrangement(iconOnly: Boolean): Arrangement.Horizontal {
+    val horizontalAlignment =
+        if (iconOnly) {
+            Alignment.CenterHorizontally
+        } else {
+            Alignment.Start
+        }
+    return spacedBy(
+        space = dimensionResource(id = R.dimen.qs_label_container_margin),
+        alignment = horizontalAlignment
+    )
+}
+
+@Composable
+private fun TileContent(
+    label: String,
+    secondaryLabel: String?,
+    icon: Icon,
+    colors: TileColorAttributes,
+    iconOnly: Boolean,
+    animateIconToEnd: Boolean = false,
+) {
+    TileIcon(icon, colorAttr(colors.icon), animateIconToEnd)
+
+    if (!iconOnly) {
+        Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) {
+            Text(
+                label,
+                color = colorAttr(colors.label),
+                modifier = Modifier.basicMarquee(),
+            )
+            if (!TextUtils.isEmpty(secondaryLabel)) {
+                Text(
+                    secondaryLabel ?: "",
+                    color = colorAttr(colors.secondaryLabel),
+                    modifier = Modifier.basicMarquee(),
+                )
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorTest.kt
index db752dd..d15cfbf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorTest.kt
@@ -20,7 +20,6 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.qs.panels.data.repository.GridLayoutTypeRepository
 import com.android.systemui.qs.panels.data.repository.IconTilesRepository
 import com.android.systemui.qs.panels.data.repository.gridLayoutTypeRepository
 import com.android.systemui.qs.panels.data.repository.iconTilesRepository
@@ -48,9 +47,6 @@
 
     data object TestGridLayoutType : GridLayoutType
 
-    private val gridLayout: MutableStateFlow<GridLayoutType> =
-        MutableStateFlow(InfiniteGridLayoutType)
-
     private val iconOnlyTiles =
         MutableStateFlow(
             setOf(
@@ -74,17 +70,13 @@
                     Pair(InfiniteGridLayoutType, infiniteGridConsistencyInteractor),
                     Pair(TestGridLayoutType, noopGridConsistencyInteractor)
                 )
-            gridLayoutTypeRepository =
-                object : GridLayoutTypeRepository {
-                    override val layout: StateFlow<GridLayoutType> = gridLayout.asStateFlow()
-                }
         }
 
     private val underTest = with(kosmos) { gridConsistencyInteractor }
 
     @Before
     fun setUp() {
-        gridLayout.value = InfiniteGridLayoutType
+        with(kosmos) { gridLayoutTypeRepository.setLayout(InfiniteGridLayoutType) }
         underTest.start()
     }
 
@@ -94,7 +86,7 @@
         with(kosmos) {
             testScope.runTest {
                 // Using the no-op grid consistency interactor
-                gridLayout.value = TestGridLayoutType
+                gridLayoutTypeRepository.setLayout(TestGridLayoutType)
 
                 // Setting an invalid layout with holes
                 // [ Large A ] [ sa ]