Merge "Implement top app bar and reset button for Edit mode" into main
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/DynamicIconTilesInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/DynamicIconTilesInteractorTest.kt
new file mode 100644
index 0000000..75d4b91
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/DynamicIconTilesInteractorTest.kt
@@ -0,0 +1,81 @@
+/*
+ * 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 androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepository
+import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository
+import com.android.systemui.qs.panels.data.repository.qsPreferencesRepository
+import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DynamicIconTilesInteractorTest : SysuiTestCase() {
+    private val kosmos =
+        testKosmos().apply {
+            defaultLargeTilesRepository =
+                object : DefaultLargeTilesRepository {
+                    override val defaultLargeTiles: Set<TileSpec> = setOf(largeTile)
+                }
+            currentTilesInteractor.setTiles(listOf(largeTile, smallTile))
+        }
+    private lateinit var underTest: DynamicIconTilesInteractor
+
+    @Before
+    fun setUp() {
+        with(kosmos) {
+            underTest = dynamicIconTilesInteractorFactory.create()
+            underTest.activateIn(testScope)
+        }
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun removingTile_updatesSharedPreferences() =
+        with(kosmos) {
+            testScope.runTest {
+                val latest by collectLastValue(qsPreferencesRepository.largeTilesSpecs)
+                runCurrent()
+
+                // Remove the large tile from the current tiles
+                currentTilesInteractor.removeTiles(listOf(largeTile))
+                runCurrent()
+
+                // Assert that it resized to small
+                assertThat(latest).doesNotContain(largeTile)
+            }
+        }
+
+    private companion object {
+        private val largeTile = TileSpec.create("large")
+        private val smallTile = TileSpec.create("small")
+    }
+}
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 79a303d..ed28dc8 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
@@ -98,23 +98,6 @@
 
     @OptIn(ExperimentalCoroutinesApi::class)
     @Test
-    fun removingTile_updatesSharedPreferences() =
-        with(kosmos) {
-            testScope.runTest {
-                val latest by collectLastValue(qsPreferencesRepository.largeTilesSpecs)
-                runCurrent()
-
-                // Remove the large tile from the current tiles
-                currentTilesInteractor.removeTiles(listOf(largeTile))
-                runCurrent()
-
-                // Assert that it resized to small
-                assertThat(latest).doesNotContain(largeTile)
-            }
-        }
-
-    @OptIn(ExperimentalCoroutinesApi::class)
-    @Test
     fun resizingNonCurrentTile_doesNothing() =
         with(kosmos) {
             testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/SizedTilesResetInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/SizedTilesResetInteractorTest.kt
new file mode 100644
index 0000000..ee7a15e
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/SizedTilesResetInteractorTest.kt
@@ -0,0 +1,82 @@
+/*
+ * 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 androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepository
+import com.android.systemui.qs.panels.data.repository.defaultLargeTilesRepository
+import com.android.systemui.qs.pipeline.data.repository.FakeDefaultTilesRepository
+import com.android.systemui.qs.pipeline.data.repository.fakeDefaultTilesRepository
+import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SizedTilesResetInteractorTest : SysuiTestCase() {
+    private val kosmos =
+        testKosmos().apply {
+            defaultLargeTilesRepository =
+                object : DefaultLargeTilesRepository {
+                    override val defaultLargeTiles: Set<TileSpec> = setOf(largeTile)
+                }
+            fakeDefaultTilesRepository = FakeDefaultTilesRepository(listOf(smallTile, largeTile))
+        }
+    private val underTest = with(kosmos) { sizedTilesResetInteractor }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun changeTiles_resetsCorrectly() {
+        with(kosmos) {
+            testScope.runTest {
+                // Change current tiles and large tiles
+                currentTilesInteractor.setTiles(listOf(largeTile, newTile))
+                iconTilesInteractor.setLargeTiles(setOf(newTile))
+                runCurrent()
+
+                // Assert both current tiles and large tiles changed
+                assertThat(currentTilesInteractor.currentTilesSpecs)
+                    .containsExactly(largeTile, newTile)
+                assertThat(iconTilesInteractor.largeTilesSpecs.value).containsExactly(newTile)
+
+                // Reset to default
+                underTest.reset()
+                runCurrent()
+
+                // Assert both current tiles and large tiles are back to the initial state
+                assertThat(currentTilesInteractor.currentTilesSpecs)
+                    .containsExactly(largeTile, smallTile)
+                assertThat(iconTilesInteractor.largeTilesSpecs.value).containsExactly(largeTile)
+            }
+        }
+    }
+
+    private companion object {
+        private val largeTile = TileSpec.create("large")
+        private val smallTile = TileSpec.create("small")
+        private val newTile = TileSpec.create("newTile")
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt
index 7ebebd7..23056b2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt
@@ -91,7 +91,7 @@
                 context.resources,
                 logger,
                 retailModeRepository,
-                userTileSpecRepositoryFactory
+                userTileSpecRepositoryFactory,
             )
     }
 
@@ -218,6 +218,21 @@
             assertThat(loadTilesForUser(user)).isEqualTo(startingTiles)
         }
 
+    @Test
+    fun resetsDefault() =
+        testScope.runTest {
+            val tiles by collectLastValue(underTest.tilesSpecs(0))
+
+            val startingTiles = listOf(TileSpec.create("e"), TileSpec.create("f"))
+
+            underTest.setTiles(0, startingTiles)
+            runCurrent()
+
+            underTest.resetToDefault(0)
+
+            assertThat(tiles!!).containsExactlyElementsIn(DEFAULT_TILES.toTileSpecs())
+        }
+
     private fun TestScope.storeTilesForUser(specs: String, forUser: Int) {
         secureSettings.putStringForUser(SETTING, specs, forUser)
         runCurrent()
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 2c5fb56..cdf15ca 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3895,4 +3895,12 @@
     <string name="qs_edit_mode_category_unknown">
         Unknown
     </string>
+    <!-- Title for the Reset Tiles dialog in QS Edit mode. [CHAR LIMIT=NONE] -->
+    <string name="qs_edit_mode_reset_dialog_title">
+        Reset tiles
+    </string>
+    <!-- Content of the Reset Tiles dialog in QS Edit mode. [CHAR LIMIT=NONE] -->
+    <string name="qs_edit_mode_reset_dialog_content">
+        Reset tiles to their original order and sizes?
+    </string>
 </resources>
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 31e867e..ef30cbf 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
@@ -23,6 +23,8 @@
 import com.android.systemui.qs.panels.data.repository.DefaultLargeTilesRepositoryImpl
 import com.android.systemui.qs.panels.data.repository.GridLayoutTypeRepository
 import com.android.systemui.qs.panels.data.repository.GridLayoutTypeRepositoryImpl
+import com.android.systemui.qs.panels.domain.interactor.EditTilesResetInteractor
+import com.android.systemui.qs.panels.domain.interactor.SizedTilesResetInteractor
 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.PaginatedGridLayoutType
@@ -53,6 +55,9 @@
     @Binds
     fun bindGridLayoutTypeRepository(impl: GridLayoutTypeRepositoryImpl): GridLayoutTypeRepository
 
+    @Binds
+    fun bindEditTilesResetInteractor(impl: SizedTilesResetInteractor): EditTilesResetInteractor
+
     @Binds fun bindIconTilesViewModel(impl: IconTilesViewModelImpl): IconTilesViewModel
 
     @Binds fun bindQSColumnsViewModel(impl: QSColumnsSizeViewModelImpl): QSColumnsViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/DynamicIconTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/DynamicIconTilesInteractor.kt
new file mode 100644
index 0000000..ee38dfb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/DynamicIconTilesInteractor.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.lifecycle.ExclusiveActivatable
+import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+/** Interactor to resize QS tiles down to icons when removed from the current tiles. */
+class DynamicIconTilesInteractor
+@AssistedInject
+constructor(
+    private val iconTilesInteractor: IconTilesInteractor,
+    private val currentTilesInteractor: CurrentTilesInteractor,
+) : ExclusiveActivatable() {
+
+    override suspend fun onActivated(): Nothing {
+        currentTilesInteractor.currentTiles.collect { currentTiles ->
+            // Only current tiles can be resized, so observe the current tiles and find the
+            // intersection between them and the large tiles.
+            val newLargeTiles =
+                iconTilesInteractor.largeTilesSpecs.value intersect
+                    currentTiles.map { it.spec }.toSet()
+            iconTilesInteractor.setLargeTiles(newLargeTiles)
+        }
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(): DynamicIconTilesInteractor
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/EditTilesResetInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/EditTilesResetInteractor.kt
new file mode 100644
index 0000000..b523897
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/EditTilesResetInteractor.kt
@@ -0,0 +1,22 @@
+/*
+ * 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
+
+/** Interactor for resetting QS tiles to the default state. */
+interface EditTilesResetInteractor {
+    fun reset()
+}
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 fc59a50..ec61a0d 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
@@ -27,7 +27,6 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.stateIn
 
@@ -36,34 +35,27 @@
 class IconTilesInteractor
 @Inject
 constructor(
-    repo: DefaultLargeTilesRepository,
+    private val repo: DefaultLargeTilesRepository,
     private val currentTilesInteractor: CurrentTilesInteractor,
     private val preferencesInteractor: QSPreferencesInteractor,
     @PanelsLog private val logBuffer: LogBuffer,
     @Application private val applicationScope: CoroutineScope,
 ) {
-
     val largeTilesSpecs =
-        combine(preferencesInteractor.largeTilesSpecs, currentTilesInteractor.currentTiles) {
-                largeTiles,
-                currentTiles ->
-                if (currentTiles.isEmpty()) {
-                    largeTiles
-                } else {
-                    // Only current tiles can be resized, so observe the current tiles and find the
-                    // intersection between them and the large tiles.
-                    val newLargeTiles = largeTiles intersect currentTiles.map { it.spec }.toSet()
-                    if (newLargeTiles != largeTiles) {
-                        preferencesInteractor.setLargeTilesSpecs(newLargeTiles)
-                    }
-                    newLargeTiles
-                }
-            }
+        preferencesInteractor.largeTilesSpecs
             .onEach { logChange(it) }
             .stateIn(applicationScope, SharingStarted.Eagerly, repo.defaultLargeTiles)
 
     fun isIconTile(spec: TileSpec): Boolean = !largeTilesSpecs.value.contains(spec)
 
+    fun setLargeTiles(specs: Set<TileSpec>) {
+        preferencesInteractor.setLargeTilesSpecs(specs)
+    }
+
+    fun resetToDefault() {
+        preferencesInteractor.setLargeTilesSpecs(repo.defaultLargeTiles)
+    }
+
     fun resize(spec: TileSpec, toIcon: Boolean) {
         if (!isCurrent(spec)) {
             return
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/SizedTilesResetInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/SizedTilesResetInteractor.kt
new file mode 100644
index 0000000..a402587
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/SizedTilesResetInteractor.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.internal.logging.UiEventLogger
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.QSEditEvent
+import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
+import javax.inject.Inject
+
+/**
+ * This implementation of [EditTilesResetInteractor] resets both the current tiles and the sizes to
+ * the default state.
+ */
+@SysUISingleton
+class SizedTilesResetInteractor
+@Inject
+constructor(
+    private val currentTilesInteractor: CurrentTilesInteractor,
+    private val iconTilesInteractor: IconTilesInteractor,
+    private val uiEventLogger: UiEventLogger,
+) : EditTilesResetInteractor {
+    override fun reset() {
+        uiEventLogger.log(QSEditEvent.QS_EDIT_RESET)
+        currentTilesInteractor.resetTiles()
+        iconTilesInteractor.resetToDefault()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditMode.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditMode.kt
index 1674865..e990d9d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditMode.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditMode.kt
@@ -26,10 +26,7 @@
 import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel
 
 @Composable
-fun EditMode(
-    viewModel: EditModeViewModel,
-    modifier: Modifier = Modifier,
-) {
+fun EditMode(viewModel: EditModeViewModel, modifier: Modifier = Modifier) {
     val gridLayout by viewModel.gridLayout.collectAsStateWithLifecycle()
     val tiles by viewModel.tiles.collectAsStateWithLifecycle(emptyList())
 
@@ -44,6 +41,7 @@
             viewModel::addTile,
             viewModel::removeTile,
             viewModel::setTiles,
+            viewModel::stopEditing,
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
index 0c02b40..0d37581 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
@@ -41,6 +41,7 @@
         onAddTile: (TileSpec, Int) -> Unit,
         onRemoveTile: (TileSpec) -> Unit,
         onSetTiles: (List<TileSpec>) -> Unit,
+        onStopEditing: () -> Unit,
     )
 }
 
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 5c2a2bd..d2ec958 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
@@ -53,11 +53,19 @@
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.ArrowBack
 import androidx.compose.material.icons.filled.Clear
+import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
 import androidx.compose.material3.LocalContentColor
 import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
 import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.LaunchedEffect
@@ -133,6 +141,31 @@
 
 object TileType
 
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun EditModeTopBar(onStopEditing: () -> Unit, onReset: (() -> Unit)?) {
+    TopAppBar(
+        colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Black),
+        title = { Text(text = stringResource(id = R.string.qs_edit)) },
+        navigationIcon = {
+            IconButton(onClick = onStopEditing) {
+                Icon(
+                    Icons.AutoMirrored.Filled.ArrowBack,
+                    contentDescription =
+                        stringResource(id = com.android.internal.R.string.action_bar_up_description),
+                )
+            }
+        },
+        actions = {
+            if (onReset != null) {
+                TextButton(onClick = onReset) {
+                    Text(stringResource(id = com.android.internal.R.string.reset))
+                }
+            }
+        },
+    )
+}
+
 @Composable
 fun DefaultEditTileGrid(
     listState: EditTileListState,
@@ -142,6 +175,8 @@
     onRemoveTile: (TileSpec) -> Unit,
     onSetTiles: (List<TileSpec>) -> Unit,
     onResize: (TileSpec, toIcon: Boolean) -> Unit,
+    onStopEditing: () -> Unit,
+    onReset: (() -> Unit)?,
 ) {
     val currentListState by rememberUpdatedState(listState)
     val selectionState =
@@ -152,53 +187,71 @@
                 currentListState.isIcon(spec)?.let { onResize(spec, it) }
             },
         )
+    val reset: (() -> Unit)? =
+        if (onReset != null) {
+            {
+                selectionState.unSelect()
+                onReset()
+            }
+        } else {
+            null
+        }
 
-    CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
-        Column(
-            verticalArrangement =
-                spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)),
-            modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()),
-        ) {
-            AnimatedContent(
-                targetState = listState.dragInProgress,
-                modifier = Modifier.wrapContentSize(),
-                label = "",
-            ) { dragIsInProgress ->
-                EditGridHeader(Modifier.dragAndDropRemoveZone(listState, onRemoveTile)) {
-                    if (dragIsInProgress) {
-                        RemoveTileTarget()
-                    } else {
-                        Text(text = "Hold and drag to rearrange tiles.")
+    Scaffold(
+        containerColor = Color.Transparent,
+        topBar = { EditModeTopBar(onStopEditing = onStopEditing, onReset = reset) },
+    ) { innerPadding ->
+        CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
+            Column(
+                verticalArrangement =
+                    spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)),
+                modifier =
+                    modifier
+                        .fillMaxSize()
+                        .verticalScroll(rememberScrollState())
+                        .padding(innerPadding),
+            ) {
+                AnimatedContent(
+                    targetState = listState.dragInProgress,
+                    modifier = Modifier.wrapContentSize(),
+                    label = "",
+                ) { dragIsInProgress ->
+                    EditGridHeader(Modifier.dragAndDropRemoveZone(listState, onRemoveTile)) {
+                        if (dragIsInProgress) {
+                            RemoveTileTarget()
+                        } else {
+                            Text(text = "Hold and drag to rearrange tiles.")
+                        }
                     }
                 }
-            }
 
-            CurrentTilesGrid(listState, selectionState, columns, onResize, onSetTiles)
+                CurrentTilesGrid(listState, selectionState, columns, onResize, onSetTiles)
 
-            // Hide available tiles when dragging
-            AnimatedVisibility(
-                visible = !listState.dragInProgress,
-                enter = fadeIn(),
-                exit = fadeOut(),
-            ) {
-                Column(
-                    verticalArrangement =
-                        spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)),
-                    modifier = modifier.fillMaxSize(),
+                // Hide available tiles when dragging
+                AnimatedVisibility(
+                    visible = !listState.dragInProgress,
+                    enter = fadeIn(),
+                    exit = fadeOut(),
                 ) {
-                    EditGridHeader { Text(text = "Hold and drag to add tiles.") }
+                    Column(
+                        verticalArrangement =
+                            spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)),
+                        modifier = modifier.fillMaxSize(),
+                    ) {
+                        EditGridHeader { Text(text = "Hold and drag to add tiles.") }
 
-                    AvailableTileGrid(otherTiles, selectionState, columns, listState)
+                        AvailableTileGrid(otherTiles, selectionState, columns, listState)
+                    }
                 }
-            }
 
-            // Drop zone to remove tiles dragged out of the tile grid
-            Spacer(
-                modifier =
-                    Modifier.fillMaxWidth()
-                        .weight(1f)
-                        .dragAndDropRemoveZone(listState, onRemoveTile)
-            )
+                // Drop zone to remove tiles dragged out of the tile grid
+                Spacer(
+                    modifier =
+                        Modifier.fillMaxWidth()
+                            .weight(1f)
+                            .dragAndDropRemoveZone(listState, onRemoveTile)
+                )
+            }
         }
     }
 }
@@ -269,7 +322,7 @@
                 .border(
                     width = 1.dp,
                     color = MaterialTheme.colorScheme.onBackground.copy(alpha = .5f),
-                    shape = RoundedCornerShape(48.dp),
+                    shape = RoundedCornerShape((TileHeight / 2) + CurrentTilesGridPadding),
                 )
                 .dragAndDropTileList(gridState, { gridContentOffset }, listState) { spec ->
                     onSetTiles(currentListState.tileSpecs())
@@ -313,10 +366,7 @@
                 text = category.label.load() ?: "",
                 fontSize = 20.sp,
                 color = labelColors.label,
-                modifier =
-                    Modifier.fillMaxWidth()
-                        .background(Color.Black)
-                        .padding(start = 16.dp, bottom = 8.dp, top = 8.dp),
+                modifier = Modifier.fillMaxWidth().padding(start = 16.dp, bottom = 8.dp, top = 8.dp),
             )
             tiles.chunked(columns).forEach { row ->
                 Row(
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 e5c2135..366bc9a 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
@@ -28,6 +28,7 @@
 import com.android.compose.animation.scene.SceneScope
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.grid.ui.compose.VerticalSpannedGrid
+import com.android.systemui.lifecycle.rememberViewModel
 import com.android.systemui.qs.panels.shared.model.SizedTileImpl
 import com.android.systemui.qs.panels.ui.compose.PaginatableGridLayout
 import com.android.systemui.qs.panels.ui.compose.bounceableInfo
@@ -35,8 +36,7 @@
 import com.android.systemui.qs.panels.ui.viewmodel.BounceableTileViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.QSColumnsViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.TileSquishinessViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.shared.ui.ElementKeys.toElementKey
@@ -48,8 +48,7 @@
 @Inject
 constructor(
     private val iconTilesViewModel: IconTilesViewModel,
-    private val gridSizeViewModel: QSColumnsViewModel,
-    private val squishinessViewModel: TileSquishinessViewModel,
+    private val viewModelFactory: InfiniteGridViewModel.Factory,
 ) : PaginatableGridLayout {
 
     @Composable
@@ -63,11 +62,19 @@
             tiles.forEach { it.startListening(token) }
             onDispose { tiles.forEach { it.stopListening(token) } }
         }
-        val columns by gridSizeViewModel.columns.collectAsStateWithLifecycle()
+        val viewModel =
+            rememberViewModel(traceName = "InfiniteGridLayout.TileGrid") {
+                viewModelFactory.create()
+            }
+        val iconTilesViewModel =
+            rememberViewModel(traceName = "InfiniteGridLayout.TileGrid") {
+                viewModel.dynamicIconTilesViewModelFactory.create()
+            }
+        val columns by viewModel.gridSizeViewModel.columns.collectAsStateWithLifecycle()
         val sizedTiles = tiles.map { SizedTileImpl(it, it.spec.width()) }
         val bounceables =
             remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } }
-        val squishiness by squishinessViewModel.squishiness.collectAsStateWithLifecycle()
+        val squishiness by viewModel.squishinessViewModel.squishiness.collectAsStateWithLifecycle()
         val scope = rememberCoroutineScope()
         var cellIndex = 0
 
@@ -98,8 +105,17 @@
         onAddTile: (TileSpec, Int) -> Unit,
         onRemoveTile: (TileSpec) -> Unit,
         onSetTiles: (List<TileSpec>) -> Unit,
+        onStopEditing: () -> Unit,
     ) {
-        val columns by gridSizeViewModel.columns.collectAsStateWithLifecycle()
+        val viewModel =
+            rememberViewModel(traceName = "InfiniteGridLayout.EditTileGrid") {
+                viewModelFactory.create()
+            }
+        val iconTilesViewModel =
+            rememberViewModel(traceName = "InfiniteGridLayout.EditTileGrid") {
+                viewModel.dynamicIconTilesViewModelFactory.create()
+            }
+        val columns by viewModel.gridSizeViewModel.columns.collectAsStateWithLifecycle()
         val largeTiles by iconTilesViewModel.largeTiles.collectAsStateWithLifecycle()
 
         // Non-current tiles should always be displayed as icon tiles.
@@ -123,6 +139,8 @@
             onRemoveTile = onRemoveTile,
             onSetTiles = onSetTiles,
             onResize = iconTilesViewModel::resize,
+            onStopEditing = onStopEditing,
+            onReset = viewModel::showResetDialog,
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/dialog/QSResetDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/dialog/QSResetDialogDelegate.kt
new file mode 100644
index 0000000..03fc425
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/dialog/QSResetDialogDelegate.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.dialog
+
+import android.util.Log
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import com.android.compose.PlatformButton
+import com.android.compose.PlatformOutlinedButton
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dialog.ui.composable.AlertDialogContent
+import com.android.systemui.qs.panels.domain.interactor.EditTilesResetInteractor
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.ComponentSystemUIDialog
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.phone.SystemUIDialogFactory
+import com.android.systemui.statusbar.phone.create
+import com.android.systemui.util.Assert
+import javax.inject.Inject
+
+@SysUISingleton
+class QSResetDialogDelegate
+@Inject
+constructor(
+    private val sysuiDialogFactory: SystemUIDialogFactory,
+    private val resetInteractor: EditTilesResetInteractor,
+) : SystemUIDialog.Delegate {
+    private var currentDialog: ComponentSystemUIDialog? = null
+
+    override fun createDialog(): SystemUIDialog {
+        Assert.isMainThread()
+        if (currentDialog != null) {
+            Log.d(TAG, "Dialog is already open, dismissing it and creating a new one.")
+            currentDialog?.dismiss()
+        }
+
+        currentDialog =
+            sysuiDialogFactory
+                .create { ResetConfirmationDialog(it) }
+                .also {
+                    it.lifecycle.addObserver(
+                        object : DefaultLifecycleObserver {
+                            override fun onStop(owner: LifecycleOwner) {
+                                Assert.isMainThread()
+                                currentDialog = null
+                            }
+                        }
+                    )
+                }
+        return currentDialog!!
+    }
+
+    @Composable
+    private fun ResetConfirmationDialog(dialog: SystemUIDialog) {
+        AlertDialogContent(
+            title = { Text(text = stringResource(id = R.string.qs_edit_mode_reset_dialog_title)) },
+            content = {
+                Text(text = stringResource(id = R.string.qs_edit_mode_reset_dialog_content))
+            },
+            positiveButton = {
+                PlatformButton(
+                    onClick = {
+                        dialog.dismiss()
+                        resetInteractor.reset()
+                    }
+                ) {
+                    Text(stringResource(id = android.R.string.ok))
+                }
+            },
+            neutralButton = {
+                PlatformOutlinedButton(onClick = { dialog.dismiss() }) {
+                    Text(stringResource(id = android.R.string.cancel))
+                }
+            },
+        )
+    }
+
+    fun showDialog() {
+        if (currentDialog == null) {
+            createDialog()
+        }
+        currentDialog?.show()
+    }
+
+    companion object {
+        private const val TAG = "ResetDialogDelegate"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DynamicIconTilesViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DynamicIconTilesViewModel.kt
new file mode 100644
index 0000000..9feaab8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DynamicIconTilesViewModel.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.viewmodel
+
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.qs.panels.domain.interactor.DynamicIconTilesInteractor
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+/** View model to resize QS tiles down to icons when removed from the current tiles. */
+class DynamicIconTilesViewModel
+@AssistedInject
+constructor(
+    interactorFactory: DynamicIconTilesInteractor.Factory,
+    iconTilesViewModel: IconTilesViewModel,
+) : IconTilesViewModel by iconTilesViewModel, ExclusiveActivatable() {
+    private val interactor = interactorFactory.create()
+
+    override suspend fun onActivated(): Nothing {
+        interactor.activate()
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(): DynamicIconTilesViewModel
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt
index 4a8aa83e..7fe856b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt
@@ -73,11 +73,7 @@
     val gridLayout: StateFlow<GridLayout> =
         gridLayoutTypeInteractor.layout
             .map { gridLayoutMap[it] ?: defaultGridLayout }
-            .stateIn(
-                applicationScope,
-                SharingStarted.WhileSubscribed(),
-                defaultGridLayout,
-            )
+            .stateIn(applicationScope, SharingStarted.WhileSubscribed(), defaultGridLayout)
 
     /**
      * Flow of view models for each tile that should be visible in edit mode (or empty flow when not
@@ -196,9 +192,4 @@
     fun setTiles(tileSpecs: List<TileSpec>) {
         currentTilesInteractor.setTiles(tileSpecs)
     }
-
-    /** Immediately resets the current tiles to the default list. */
-    fun resetCurrentTilesToDefault() {
-        throw NotImplementedError("This is not supported yet")
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridViewModel.kt
new file mode 100644
index 0000000..0d12067
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridViewModel.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.viewmodel
+
+import com.android.systemui.qs.panels.ui.dialog.QSResetDialogDelegate
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+class InfiniteGridViewModel
+@AssistedInject
+constructor(
+    val dynamicIconTilesViewModelFactory: DynamicIconTilesViewModel.Factory,
+    val gridSizeViewModel: QSColumnsViewModel,
+    val squishinessViewModel: TileSquishinessViewModel,
+    private val resetDialogDelegate: QSResetDialogDelegate,
+) {
+
+    fun showResetDialog() {
+        resetDialogDelegate.showDialog()
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(): InfiniteGridViewModel
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt
index 24b80b8..d94e7cf 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt
@@ -71,6 +71,9 @@
     /** Prepend the default list of tiles to the current set of tiles */
     suspend fun prependDefault(@UserIdInt userId: Int)
 
+    /** Reset the current set of tiles to the default list of tiles */
+    suspend fun resetToDefault(userId: Int)
+
     companion object {
         /** Position to indicate the end of the list */
         const val POSITION_AT_END = -1
@@ -148,22 +151,24 @@
 
     override suspend fun reconcileRestore(
         restoreData: RestoreData,
-        currentAutoAdded: Set<TileSpec>
+        currentAutoAdded: Set<TileSpec>,
     ) {
         userTileRepositories
             .get(restoreData.userId)
             ?.reconcileRestore(restoreData, currentAutoAdded)
     }
 
-    override suspend fun prependDefault(
-        userId: Int,
-    ) {
+    override suspend fun prependDefault(userId: Int) {
         if (retailModeRepository.inRetailMode) {
             return
         }
         userTileRepositories.get(userId)?.prependDefault()
     }
 
+    override suspend fun resetToDefault(userId: Int) {
+        userTileRepositories.get(userId)?.resetToDefault()
+    }
+
     companion object {
         private const val DELIMITER = TilesSettingConverter.DELIMITER
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt
index 1f9570a..b0ae1e1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt
@@ -119,14 +119,7 @@
                 .filter { it !is TileSpec.Invalid }
                 .joinToString(DELIMITER, transform = TileSpec::spec)
         withContext(backgroundDispatcher) {
-            secureSettings.putStringForUser(
-                SETTING,
-                toStore,
-                null,
-                false,
-                forUser,
-                true,
-            )
+            secureSettings.putStringForUser(SETTING, toStore, null, false, forUser, true)
         }
     }
 
@@ -172,13 +165,17 @@
         changeEvents.emit(PrependDefault(defaultTiles))
     }
 
+    suspend fun resetToDefault() {
+        changeEvents.emit(ResetToDefault(defaultTiles))
+    }
+
     sealed interface ChangeAction {
         fun apply(currentTiles: List<TileSpec>): List<TileSpec>
     }
 
     private data class AddTile(
         val tileSpec: TileSpec,
-        val position: Int = TileSpecRepository.POSITION_AT_END
+        val position: Int = TileSpecRepository.POSITION_AT_END,
     ) : ChangeAction {
         override fun apply(currentTiles: List<TileSpec>): List<TileSpec> {
             val tilesList = currentTiles.toMutableList()
@@ -199,9 +196,7 @@
         }
     }
 
-    private data class ChangeTiles(
-        val newTiles: List<TileSpec>,
-    ) : ChangeAction {
+    private data class ChangeTiles(val newTiles: List<TileSpec>) : ChangeAction {
         override fun apply(currentTiles: List<TileSpec>): List<TileSpec> {
             val new = newTiles.filter { it !is TileSpec.Invalid }
             return if (new.isNotEmpty()) new else currentTiles
@@ -214,6 +209,12 @@
         }
     }
 
+    private data class ResetToDefault(val defaultTiles: List<TileSpec>) : ChangeAction {
+        override fun apply(currentTiles: List<TileSpec>): List<TileSpec> {
+            return defaultTiles
+        }
+    }
+
     private data class RestoreTiles(
         val restoreData: RestoreData,
         val currentAutoAdded: Set<TileSpec>,
@@ -236,7 +237,7 @@
         fun reconcileTiles(
             currentTiles: List<TileSpec>,
             currentAutoAdded: Set<TileSpec>,
-            restoreData: RestoreData
+            restoreData: RestoreData,
         ): List<TileSpec> {
             val toRestore = restoreData.restoredTiles.toMutableList()
             val freshlyAutoAdded =
@@ -260,8 +261,6 @@
 
     @AssistedFactory
     interface Factory {
-        fun create(
-            userId: Int,
-        ): UserTileSpecRepository
+        fun create(userId: Int): UserTileSpecRepository
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
index 4a96710..10097d6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
@@ -116,6 +116,9 @@
      */
     fun setTiles(specs: List<TileSpec>)
 
+    /** Requests that the list of tiles for the current user is changed to the default list. */
+    fun resetTiles()
+
     fun createTileSync(spec: TileSpec): QSTile?
 
     companion object {
@@ -222,7 +225,7 @@
                                         .TILE_NOT_PRESENT_IN_NEW_USER
                                 } else {
                                     QSPipelineLogger.TileDestroyedReason.TILE_REMOVED
-                                }
+                                },
                             )
                             (entry.value as TileOrNotInstalled.Tile).tile.destroy()
                         }
@@ -245,7 +248,7 @@
                                             tileSpec,
                                             specsToTiles.getValue(tileSpec),
                                             userChanged,
-                                            newUser
+                                            newUser,
                                         ) ?: createTile(tileSpec)
                                     } else {
                                         createTile(tileSpec)
@@ -268,7 +271,7 @@
                     _currentSpecsAndTiles.value = newResolvedTiles
                     logger.logTilesNotInstalled(
                         newTileMap.filter { it.value is TileOrNotInstalled.NotInstalled }.keys,
-                        newUser
+                        newUser,
                     )
                     if (newResolvedTiles.size < minTiles) {
                         // We ended up with not enough tiles (some may be not installed).
@@ -317,6 +320,10 @@
         }
     }
 
+    override fun resetTiles() {
+        scope.launch { tileSpecRepository.resetToDefault(currentUser.value) }
+    }
+
     override fun dump(pw: PrintWriter, args: Array<out String>) {
         pw.println("CurrentTileInteractorImpl:")
         pw.println("User: ${userId.value}")
@@ -384,7 +391,7 @@
                     !qsTile.isAvailable -> {
                         logger.logTileDestroyed(
                             tileSpec,
-                            QSPipelineLogger.TileDestroyedReason.EXISTING_TILE_NOT_AVAILABLE
+                            QSPipelineLogger.TileDestroyedReason.EXISTING_TILE_NOT_AVAILABLE,
                         )
                         qsTile.destroy()
                         null
@@ -409,7 +416,7 @@
                         qsTile.destroy()
                         logger.logTileDestroyed(
                             tileSpec,
-                            QSPipelineLogger.TileDestroyedReason.CUSTOM_TILE_USER_CHANGED
+                            QSPipelineLogger.TileDestroyedReason.CUSTOM_TILE_USER_CHANGED,
                         )
                         null
                     }
@@ -428,7 +435,7 @@
 private data class UserTilesAndComponents(
     val userId: Int,
     val tiles: List<TileSpec>,
-    val installedComponents: Set<ComponentName>
+    val installedComponents: Set<ComponentName>,
 )
 
 private data class DataWithUserChange(
@@ -439,9 +446,4 @@
 )
 
 private fun DataWithUserChange(data: UserTilesAndComponents, userChange: Boolean) =
-    DataWithUserChange(
-        data.userId,
-        data.tiles,
-        data.installedComponents,
-        userChange,
-    )
+    DataWithUserChange(data.userId, data.tiles, data.installedComponents, userChange)
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 8d060e9..8a6df1c 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
@@ -67,6 +67,8 @@
             onRemoveTile = {},
             onSetTiles = onSetTiles,
             onResize = { _, _ -> },
+            onStopEditing = {},
+            onReset = null,
         )
     }
 
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 ee1c0e9..d9c1d99 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
@@ -66,6 +66,8 @@
             onRemoveTile = {},
             onSetTiles = {},
             onResize = onResize,
+            onStopEditing = {},
+            onReset = null,
         )
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/DynamicIconTilesInteractorFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/DynamicIconTilesInteractorFactoryKosmos.kt
new file mode 100644
index 0000000..a5fe8cf
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/DynamicIconTilesInteractorFactoryKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
+
+val Kosmos.dynamicIconTilesInteractorFactory by
+    Kosmos.Fixture {
+        object : DynamicIconTilesInteractor.Factory {
+            override fun create(): DynamicIconTilesInteractor {
+                return DynamicIconTilesInteractor(iconTilesInteractor, currentTilesInteractor)
+            }
+        }
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
index b4317ad..b6b0a41 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
@@ -19,10 +19,7 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.qs.panels.ui.compose.infinitegrid.InfiniteGridLayout
 import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.qsColumnsViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.tileSquishinessViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.infiniteGridViewModelFactory
 
 val Kosmos.infiniteGridLayout by
-    Kosmos.Fixture {
-        InfiniteGridLayout(iconTilesViewModel, qsColumnsViewModel, tileSquishinessViewModel)
-    }
+    Kosmos.Fixture { InfiniteGridLayout(iconTilesViewModel, infiniteGridViewModelFactory) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/SizedTilesResetInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/SizedTilesResetInteractorKosmos.kt
new file mode 100644
index 0000000..70bf9bb
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/SizedTilesResetInteractorKosmos.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.internal.logging.uiEventLogger
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
+
+val Kosmos.sizedTilesResetInteractor by
+    Kosmos.Fixture {
+        SizedTilesResetInteractor(currentTilesInteractor, iconTilesInteractor, uiEventLogger)
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/dialog/QSResetDialogDelegateKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/dialog/QSResetDialogDelegateKosmos.kt
new file mode 100644
index 0000000..c58d55e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/dialog/QSResetDialogDelegateKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.dialog
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.panels.domain.interactor.sizedTilesResetInteractor
+import com.android.systemui.statusbar.phone.systemUIDialogFactory
+
+val Kosmos.qsResetDialogDelegateKosmos by
+    Kosmos.Fixture { QSResetDialogDelegate(systemUIDialogFactory, sizedTilesResetInteractor) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/DynamicIconTilesViewModelKosmosFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/DynamicIconTilesViewModelKosmosFactory.kt
new file mode 100644
index 0000000..d185287
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/DynamicIconTilesViewModelKosmosFactory.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.panels.domain.interactor.dynamicIconTilesInteractorFactory
+
+val Kosmos.dynamicIconTilesViewModelFactory by
+    Kosmos.Fixture {
+        object : DynamicIconTilesViewModel.Factory {
+            override fun create(): DynamicIconTilesViewModel {
+                return DynamicIconTilesViewModel(
+                    dynamicIconTilesInteractorFactory,
+                    iconTilesViewModel,
+                )
+            }
+        }
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridViewModelKosmos.kt
new file mode 100644
index 0000000..7613ea31
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridViewModelKosmos.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.panels.ui.dialog.qsResetDialogDelegateKosmos
+
+val Kosmos.infiniteGridViewModelFactory by
+    Kosmos.Fixture {
+        object : InfiniteGridViewModel.Factory {
+            override fun create(): InfiniteGridViewModel {
+                return InfiniteGridViewModel(
+                    dynamicIconTilesViewModelFactory,
+                    qsColumnsViewModel,
+                    tileSquishinessViewModel,
+                    qsResetDialogDelegateKosmos,
+                )
+            }
+        }
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt
index a9cce69..1c69eab 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeTileSpecRepository.kt
@@ -63,7 +63,7 @@
 
     override suspend fun reconcileRestore(
         restoreData: RestoreData,
-        currentAutoAdded: Set<TileSpec>
+        currentAutoAdded: Set<TileSpec>,
     ) {
         with(getFlow(restoreData.userId)) {
             value = UserTileSpecRepository.reconcileTiles(value, currentAutoAdded, restoreData)
@@ -73,4 +73,8 @@
     override suspend fun prependDefault(userId: Int) {
         with(getFlow(userId)) { value = defaultTilesRepository.defaultTiles + value }
     }
+
+    override suspend fun resetToDefault(userId: Int) {
+        with(getFlow(userId)) { value = defaultTilesRepository.defaultTiles }
+    }
 }