Merge "Support SettingsDropdownCheckBox non changeable items" into main
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
index 460a6f7..e185367 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
@@ -28,7 +28,7 @@
 import com.android.settingslib.spa.gallery.dialog.NavDialogProvider
 import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider
 import com.android.settingslib.spa.gallery.editor.SettingsExposedDropdownMenuBoxPageProvider
-import com.android.settingslib.spa.gallery.editor.SettingsExposedDropdownMenuCheckBoxProvider
+import com.android.settingslib.spa.gallery.editor.SettingsDropdownCheckBoxProvider
 import com.android.settingslib.spa.gallery.home.HomePageProvider
 import com.android.settingslib.spa.gallery.itemList.ItemListPageProvider
 import com.android.settingslib.spa.gallery.itemList.ItemOperatePageProvider
@@ -100,7 +100,7 @@
                 EditorMainPageProvider,
                 SettingsOutlinedTextFieldPageProvider,
                 SettingsExposedDropdownMenuBoxPageProvider,
-                SettingsExposedDropdownMenuCheckBoxProvider,
+                SettingsDropdownCheckBoxProvider,
                 SettingsTextFieldPasswordPageProvider,
                 SearchScaffoldPageProvider,
                 SuwScaffoldPageProvider,
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt
index 4875ea9..9f2158a 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/EditorMainPageProvider.kt
@@ -37,7 +37,7 @@
                 .build(),
             SettingsExposedDropdownMenuBoxPageProvider.buildInjectEntry().setLink(fromPage = owner)
                 .build(),
-            SettingsExposedDropdownMenuCheckBoxProvider.buildInjectEntry().setLink(fromPage = owner)
+            SettingsDropdownCheckBoxProvider.buildInjectEntry().setLink(fromPage = owner)
                 .build(),
             SettingsTextFieldPasswordPageProvider.buildInjectEntry().setLink(fromPage = owner)
                 .build(),
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsDropdownCheckBoxProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsDropdownCheckBoxProvider.kt
new file mode 100644
index 0000000..33ab75d
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsDropdownCheckBoxProvider.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.settingslib.spa.gallery.editor
+
+import android.os.Bundle
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.editor.SettingsDropdownCheckBox
+import com.android.settingslib.spa.widget.editor.SettingsDropdownCheckOption
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+
+private const val TITLE = "Sample SettingsDropdownCheckBox"
+
+object SettingsDropdownCheckBoxProvider : SettingsPageProvider {
+    override val name = "SettingsDropdownCheckBox"
+
+    override fun getTitle(arguments: Bundle?): String {
+        return TITLE
+    }
+
+    @Composable
+    override fun Page(arguments: Bundle?) {
+        RegularScaffold(title = TITLE) {
+            SettingsDropdownCheckBox(
+                label = "SettingsDropdownCheckBox",
+                options = remember {
+                    listOf(
+                        SettingsDropdownCheckOption("Item 1"),
+                        SettingsDropdownCheckOption("Item 2"),
+                        SettingsDropdownCheckOption("Item 3"),
+                    )
+                },
+            )
+            SettingsDropdownCheckBox(
+                label = "Empty list",
+                options = emptyList(),
+            )
+            SettingsDropdownCheckBox(
+                label = "Disabled",
+                options = remember {
+                    listOf(
+                        SettingsDropdownCheckOption("Item 1", selected = mutableStateOf(true)),
+                        SettingsDropdownCheckOption("Item 2"),
+                        SettingsDropdownCheckOption("Item 3"),
+                    )
+                },
+                enabled = false,
+            )
+            SettingsDropdownCheckBox(
+                label = "With disabled item",
+                options = remember {
+                    listOf(
+                        SettingsDropdownCheckOption("Item 1"),
+                        SettingsDropdownCheckOption("Item 2"),
+                        SettingsDropdownCheckOption(
+                            text = "Disabled item 1",
+                            changeable = false,
+                            selected = mutableStateOf(true),
+                        ),
+                        SettingsDropdownCheckOption("Disabled item 2", changeable = false),
+                    )
+                },
+            )
+            SettingsDropdownCheckBox(
+                label = "With select all",
+                options = remember {
+                    listOf(
+                        SettingsDropdownCheckOption("All", isSelectAll = true),
+                        SettingsDropdownCheckOption("Item 1"),
+                        SettingsDropdownCheckOption("Item 2"),
+                        SettingsDropdownCheckOption("Item 3"),
+                    )
+                },
+            )
+            SettingsDropdownCheckBox(
+                label = "With disabled item and select all",
+                options =
+                remember {
+                    listOf(
+                        SettingsDropdownCheckOption("All", isSelectAll = true, changeable = false),
+                        SettingsDropdownCheckOption("Item 1"),
+                        SettingsDropdownCheckOption("Item 2"),
+                        SettingsDropdownCheckOption(
+                            text = "Disabled item 1",
+                            changeable = false,
+                            selected = mutableStateOf(true),
+                        ),
+                        SettingsDropdownCheckOption("Disabled item 2", changeable = false),
+                    )
+                },
+            )
+        }
+    }
+
+    fun buildInjectEntry(): SettingsEntryBuilder {
+        return SettingsEntryBuilder.createInject(owner = createSettingsPage()).setUiLayoutFn {
+            Preference(object : PreferenceModel {
+                override val title = TITLE
+                override val onClick = navigator(name)
+            })
+        }
+    }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SettingsDropdownCheckBoxPagePreview() {
+    SettingsTheme {
+        SettingsDropdownCheckBoxProvider.Page(null)
+    }
+}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsExposedDropdownMenuCheckBoxProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsExposedDropdownMenuCheckBoxProvider.kt
deleted file mode 100644
index d289646..0000000
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/editor/SettingsExposedDropdownMenuCheckBoxProvider.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2023 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.settingslib.spa.gallery.editor
-
-import android.os.Bundle
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateListOf
-import androidx.compose.runtime.remember
-import androidx.compose.ui.tooling.preview.Preview
-import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
-import com.android.settingslib.spa.framework.common.SettingsPageProvider
-import com.android.settingslib.spa.framework.common.createSettingsPage
-import com.android.settingslib.spa.framework.compose.navigator
-import com.android.settingslib.spa.framework.theme.SettingsTheme
-import com.android.settingslib.spa.widget.editor.SettingsExposedDropdownMenuCheckBox
-import com.android.settingslib.spa.widget.preference.Preference
-import com.android.settingslib.spa.widget.preference.PreferenceModel
-import com.android.settingslib.spa.widget.scaffold.RegularScaffold
-
-private const val TITLE = "Sample SettingsExposedDropdownMenuCheckBox"
-
-object SettingsExposedDropdownMenuCheckBoxProvider : SettingsPageProvider {
-    override val name = "SettingsExposedDropdownMenuCheckBox"
-    private const val exposedDropdownMenuCheckBoxLabel = "ExposedDropdownMenuCheckBoxLabel"
-    private val options = listOf("item1", "item2", "item3")
-    private val selectedOptionsState1 = mutableStateListOf(0, 1)
-
-    override fun getTitle(arguments: Bundle?): String {
-        return TITLE
-    }
-
-    @Composable
-    override fun Page(arguments: Bundle?) {
-        RegularScaffold(title = TITLE) {
-            SettingsExposedDropdownMenuCheckBox(
-                label = exposedDropdownMenuCheckBoxLabel,
-                options = options,
-                selectedOptionsState = remember { selectedOptionsState1 },
-                enabled = true,
-                onSelectedOptionStateChange = {},
-            )
-        }
-    }
-
-    fun buildInjectEntry(): SettingsEntryBuilder {
-        return SettingsEntryBuilder.createInject(owner = createSettingsPage()).setUiLayoutFn {
-            Preference(object : PreferenceModel {
-                override val title = TITLE
-                override val onClick = navigator(name)
-            })
-        }
-    }
-}
-
-@Preview(showBackground = true)
-@Composable
-private fun SettingsExposedDropdownMenuCheckBoxPagePreview() {
-    SettingsTheme {
-        SettingsExposedDropdownMenuCheckBoxProvider.Page(null)
-    }
-}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt
new file mode 100644
index 0000000..57963e6
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBox.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2023 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.settingslib.spa.widget.editor
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.SettingsOpacity.alphaForEnabled
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.editor.SettingsDropdownCheckOption.Companion.changeable
+
+data class SettingsDropdownCheckOption(
+    /** The displayed text of this option. */
+    val text: String,
+
+    /** If true, check / uncheck this item will check / uncheck all enabled options. */
+    val isSelectAll: Boolean = false,
+
+    /** If not changeable, cannot check or uncheck this option. */
+    val changeable: Boolean = true,
+
+    /** The selected state of this option. */
+    val selected: MutableState<Boolean> = mutableStateOf(false),
+
+    /** Get called when the option is clicked, no matter if it's changeable. */
+    val onClick: () -> Unit = {},
+) {
+    companion object {
+        val List<SettingsDropdownCheckOption>.changeable: Boolean
+            get() = filter { !it.isSelectAll }.any { it.changeable }
+    }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SettingsDropdownCheckBox(
+    label: String,
+    options: List<SettingsDropdownCheckOption>,
+    emptyText: String = "",
+    enabled: Boolean = true,
+    errorMessage: String? = null,
+    onSelectedStateChange: () -> Unit = {},
+) {
+    var dropDownWidth by remember { mutableIntStateOf(0) }
+    var expanded by remember { mutableStateOf(false) }
+    val changeable = enabled && options.changeable
+    ExposedDropdownMenuBox(
+        expanded = expanded,
+        onExpandedChange = { expanded = changeable && it },
+        modifier = Modifier
+            .width(350.dp)
+            .padding(SettingsDimension.textFieldPadding)
+            .onSizeChanged { dropDownWidth = it.width },
+    ) {
+        OutlinedTextField(
+            // The `menuAnchor` modifier must be passed to the text field for correctness.
+            modifier = Modifier
+                .menuAnchor()
+                .fillMaxWidth(),
+            value = getDisplayText(options) ?: emptyText,
+            onValueChange = {},
+            label = { Text(text = label) },
+            trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) },
+            readOnly = true,
+            enabled = changeable,
+            isError = errorMessage != null,
+            supportingText = errorMessage?.let { { Text(text = it) } },
+        )
+        ExposedDropdownMenu(
+            expanded = expanded,
+            modifier = Modifier.width(with(LocalDensity.current) { dropDownWidth.toDp() }),
+            onDismissRequest = { expanded = false },
+        ) {
+            for (option in options) {
+                CheckboxItem(option) {
+                    option.onClick()
+                    if (option.changeable) {
+                        checkboxItemOnClick(options, option)
+                        onSelectedStateChange()
+                    }
+                }
+            }
+        }
+    }
+}
+
+private fun getDisplayText(options: List<SettingsDropdownCheckOption>): String? {
+    val selectedOptions = options.filter { it.selected.value }
+    if (selectedOptions.isEmpty()) return null
+    return selectedOptions.filter { it.isSelectAll }.ifEmpty { selectedOptions }
+        .joinToString { it.text }
+}
+
+private fun checkboxItemOnClick(
+    options: List<SettingsDropdownCheckOption>,
+    clickedOption: SettingsDropdownCheckOption,
+) {
+    if (!clickedOption.changeable) return
+    val newChecked = !clickedOption.selected.value
+    if (clickedOption.isSelectAll) {
+        for (option in options.filter { it.changeable }) option.selected.value = newChecked
+    } else {
+        clickedOption.selected.value = newChecked
+    }
+    val (selectAllOptions, regularOptions) = options.partition { it.isSelectAll }
+    val isAllRegularOptionsChecked = regularOptions.all { it.selected.value }
+    selectAllOptions.forEach { it.selected.value = isAllRegularOptionsChecked }
+}
+
+@Composable
+private fun CheckboxItem(
+    option: SettingsDropdownCheckOption,
+    onClick: (SettingsDropdownCheckOption) -> Unit,
+) {
+    TextButton(
+        onClick = { onClick(option) },
+        modifier = Modifier.fillMaxWidth(),
+    ) {
+        Row(
+            modifier = Modifier.fillMaxWidth(),
+            horizontalArrangement = Arrangement.spacedBy(SettingsDimension.itemPaddingAround),
+            verticalAlignment = Alignment.CenterVertically
+        ) {
+            Checkbox(
+                checked = option.selected.value,
+                onCheckedChange = null,
+                enabled = option.changeable,
+            )
+            Text(text = option.text, modifier = Modifier.alphaForEnabled(option.changeable))
+        }
+    }
+}
+
+@Preview
+@Composable
+private fun ActionButtonsPreview() {
+    val item1 = SettingsDropdownCheckOption("item1")
+    val item2 = SettingsDropdownCheckOption("item2")
+    val item3 = SettingsDropdownCheckOption("item3")
+    val options = listOf(item1, item2, item3)
+    SettingsTheme {
+        SettingsDropdownCheckBox(
+            label = "label",
+            options = options,
+        )
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt
deleted file mode 100644
index e704505..0000000
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Copyright (C) 2023 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.settingslib.spa.widget.editor
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
-import androidx.compose.material3.Checkbox
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExposedDropdownMenuBox
-import androidx.compose.material3.ExposedDropdownMenuDefaults
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateListOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshots.SnapshotStateList
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import com.android.settingslib.spa.framework.theme.SettingsDimension
-import com.android.settingslib.spa.framework.theme.SettingsTheme
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun SettingsExposedDropdownMenuCheckBox(
-    label: String,
-    options: List<String>,
-    selectedOptionsState: SnapshotStateList<Int>,
-    emptyVal: String = "",
-    enabled: Boolean,
-    errorMessage: String? = null,
-    onSelectedOptionStateChange: () -> Unit,
-) {
-    var dropDownWidth by remember { mutableIntStateOf(0) }
-    var expanded by remember { mutableStateOf(false) }
-    val allIndex = options.indexOf("*")
-    ExposedDropdownMenuBox(
-        expanded = expanded,
-        onExpandedChange = { expanded = it },
-        modifier = Modifier
-            .width(350.dp)
-            .padding(SettingsDimension.textFieldPadding)
-            .onSizeChanged { dropDownWidth = it.width },
-    ) {
-        OutlinedTextField(
-            // The `menuAnchor` modifier must be passed to the text field for correctness.
-            modifier = Modifier
-                .menuAnchor()
-                .fillMaxWidth(),
-            value = if (selectedOptionsState.size == 0) emptyVal
-            else if (selectedOptionsState.contains(allIndex)) "*"
-            else selectedOptionsState.joinToString { options[it] },
-            onValueChange = {},
-            label = { Text(text = label) },
-            trailingIcon = {
-                ExposedDropdownMenuDefaults.TrailingIcon(
-                    expanded = expanded
-                )
-            },
-            readOnly = true,
-            enabled = enabled,
-            isError = errorMessage != null,
-            supportingText = {
-                if (errorMessage != null) {
-                    Text(text = errorMessage)
-                }
-            }
-        )
-        if (options.isNotEmpty()) {
-            ExposedDropdownMenu(
-                expanded = expanded,
-                modifier = Modifier.width(with(LocalDensity.current) { dropDownWidth.toDp() }),
-                onDismissRequest = { expanded = false },
-            ) {
-                options.forEachIndexed { index, option ->
-                    CheckboxItem(
-                        selectedOptionsState,
-                        index,
-                        allIndex,
-                        onSelectedOptionStateChange,
-                        option,
-                    )
-                }
-            }
-        }
-    }
-}
-
-@Composable
-private fun CheckboxItem(
-    selectedOptionsState: SnapshotStateList<Int>,
-    index: Int,
-    allIndex: Int,
-    onSelectedOptionStateChange: () -> Unit,
-    option: String
-) {
-    TextButton(
-        modifier = Modifier.fillMaxWidth(),
-        onClick = {
-            if (selectedOptionsState.contains(index)) {
-                if (index == allIndex) {
-                    selectedOptionsState.clear()
-                } else {
-                    selectedOptionsState.remove(index)
-                    selectedOptionsState.remove(allIndex)
-                }
-            } else {
-                selectedOptionsState.add(index)
-            }
-            onSelectedOptionStateChange()
-        }) {
-        Row(
-            modifier = Modifier.fillMaxWidth(),
-            horizontalArrangement = Arrangement.spacedBy(SettingsDimension.itemPaddingAround),
-            verticalAlignment = Alignment.CenterVertically
-        ) {
-            Checkbox(
-                checked = selectedOptionsState.contains(index),
-                onCheckedChange = null,
-            )
-            Text(text = option)
-        }
-    }
-}
-
-@Preview
-@Composable
-private fun ActionButtonsPreview() {
-    val options = listOf("item1", "item2", "item3")
-    val selectedOptionsState = remember { mutableStateListOf(0, 1) }
-    SettingsTheme {
-        SettingsExposedDropdownMenuCheckBox(
-            label = "label",
-            options = options,
-            selectedOptionsState = selectedOptionsState,
-            enabled = true,
-            onSelectedOptionStateChange = {})
-    }
-}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBoxTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBoxTest.kt
new file mode 100644
index 0000000..72b7b98
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsDropdownCheckBoxTest.kt
@@ -0,0 +1,145 @@
+/*
+ * 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.settingslib.spa.widget.editor
+
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.hasAnyAncestor
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.isPopup
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.testutils.hasRole
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SettingsDropdownCheckBoxTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Test
+    fun dropdownCheckBox_displayed() {
+        val item1 = SettingsDropdownCheckOption("item1")
+        val item2 = SettingsDropdownCheckOption("item2")
+        val item3 = SettingsDropdownCheckOption("item3")
+        composeTestRule.setContent {
+            SettingsDropdownCheckBox(
+                label = LABEL,
+                options = listOf(item1, item2, item3),
+            )
+        }
+
+        composeTestRule.onNodeWithText(LABEL).assertIsDisplayed()
+    }
+
+    @Test
+    fun dropdownCheckBox_expanded() {
+        val item1 = SettingsDropdownCheckOption("item1")
+        val item2 = SettingsDropdownCheckOption("item2")
+        val item3 = SettingsDropdownCheckOption("item3")
+        composeTestRule.setContent {
+            SettingsDropdownCheckBox(
+                label = LABEL,
+                options = listOf(item1, item2, item3),
+            )
+        }
+        composeTestRule.onOption(item3).assertDoesNotExist()
+
+        composeTestRule.onNodeWithText(LABEL).performClick()
+
+        composeTestRule.onOption(item3).assertIsDisplayed()
+    }
+
+    @Test
+    fun dropdownCheckBox_valueAdded() {
+        val item1 = SettingsDropdownCheckOption("item1")
+        val item2 = SettingsDropdownCheckOption("item2")
+        val item3 = SettingsDropdownCheckOption("item3")
+        composeTestRule.setContent {
+            SettingsDropdownCheckBox(
+                label = LABEL,
+                options = listOf(item1, item2, item3),
+            )
+        }
+        composeTestRule.onDropdownBox(item3.text).assertDoesNotExist()
+
+        composeTestRule.onNodeWithText(LABEL).performClick()
+        composeTestRule.onOption(item3).performClick()
+
+        composeTestRule.onDropdownBox(item3.text).assertIsDisplayed()
+        assertThat(item3.selected.value).isTrue()
+    }
+
+    @Test
+    fun dropdownCheckBox_valueDeleted() {
+        val item1 = SettingsDropdownCheckOption("item1")
+        val item2 = SettingsDropdownCheckOption("item2", selected = mutableStateOf(true))
+        val item3 = SettingsDropdownCheckOption("item3")
+        composeTestRule.setContent {
+            SettingsDropdownCheckBox(
+                label = LABEL,
+                options = listOf(item1, item2, item3),
+            )
+        }
+        composeTestRule.onDropdownBox(item2.text).assertIsDisplayed()
+
+        composeTestRule.onNodeWithText(LABEL).performClick()
+        composeTestRule.onOption(item2).performClick()
+
+        composeTestRule.onDropdownBox(item2.text).assertDoesNotExist()
+        assertThat(item2.selected.value).isFalse()
+    }
+
+    @Test
+    fun dropdownCheckBox_withSelectAll() {
+        val selectAll = SettingsDropdownCheckOption("All", isSelectAll = true)
+        val item1 = SettingsDropdownCheckOption("item1")
+        val item2 = SettingsDropdownCheckOption("item2")
+        composeTestRule.setContent {
+            SettingsDropdownCheckBox(
+                label = LABEL,
+                options = listOf(selectAll, item1, item2),
+            )
+        }
+
+        composeTestRule.onNodeWithText(LABEL).performClick()
+        composeTestRule.onOption(selectAll).performClick()
+
+        composeTestRule.onDropdownBox(selectAll.text).assertIsDisplayed()
+        composeTestRule.onDropdownBox(item1.text).assertDoesNotExist()
+        composeTestRule.onDropdownBox(item2.text).assertDoesNotExist()
+        assertThat(item1.selected.value).isTrue()
+        assertThat(item2.selected.value).isTrue()
+    }
+
+    private companion object {
+        const val LABEL = "Label"
+    }
+}
+
+private fun ComposeContentTestRule.onDropdownBox(text: String) =
+    onNode(hasRole(Role.DropdownList) and hasText(text))
+
+private fun ComposeContentTestRule.onOption(option: SettingsDropdownCheckOption) =
+    onNode(hasAnyAncestor(isPopup()) and hasText(option.text))
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBoxTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBoxTest.kt
deleted file mode 100644
index 2b78ed7..0000000
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBoxTest.kt
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2023 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.settingslib.spa.widget.editor
-
-import androidx.compose.runtime.mutableStateListOf
-import androidx.compose.runtime.remember
-import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.hasText
-import androidx.compose.ui.test.isFocused
-import androidx.compose.ui.test.junit4.ComposeContentTestRule
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.test.performClick
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class SettingsExposedDropdownMenuCheckBoxTest {
-    @get:Rule
-    val composeTestRule = createComposeRule()
-    private val item1 = "item1"
-    private val item2 = "item2"
-    private val item3 = "item3"
-    private val options = listOf(item1, item2, item3)
-    private val selectedOptionsState1 = mutableStateListOf(0, 1)
-    private val exposedDropdownMenuCheckBoxLabel = "ExposedDropdownMenuCheckBoxLabel"
-
-    @Test
-    fun exposedDropdownMenuCheckBox_displayed() {
-        composeTestRule.setContent {
-            SettingsExposedDropdownMenuCheckBox(
-                label = exposedDropdownMenuCheckBoxLabel,
-                options = options,
-                selectedOptionsState = remember { selectedOptionsState1 },
-                enabled = true,
-            ) {}
-        }
-        composeTestRule.onNodeWithText(
-            exposedDropdownMenuCheckBoxLabel, substring = true
-        ).assertIsDisplayed()
-    }
-
-    @Test
-    fun exposedDropdownMenuCheckBox_expanded() {
-        composeTestRule.setContent {
-            SettingsExposedDropdownMenuCheckBox(
-                label = exposedDropdownMenuCheckBoxLabel,
-                options = options,
-                selectedOptionsState = remember { selectedOptionsState1 },
-                enabled = true,
-            ) {}
-        }
-        composeTestRule.onNodeWithText(item3, substring = true).assertDoesNotExist()
-        composeTestRule.onNodeWithText(exposedDropdownMenuCheckBoxLabel, substring = true)
-            .performClick()
-        composeTestRule.onNodeWithText(item3, substring = true).assertIsDisplayed()
-    }
-
-    @Test
-    fun exposedDropdownMenuCheckBox_valueAdded() {
-        composeTestRule.setContent {
-            SettingsExposedDropdownMenuCheckBox(
-                label = exposedDropdownMenuCheckBoxLabel,
-                options = options,
-                selectedOptionsState = remember { selectedOptionsState1 },
-                enabled = true,
-            ) {}
-        }
-        composeTestRule.onNodeWithText(item3, substring = true).assertDoesNotExist()
-        composeTestRule.onNodeWithText(exposedDropdownMenuCheckBoxLabel, substring = true)
-            .performClick()
-        composeTestRule.onNodeWithText(item3, substring = true).performClick()
-        composeTestRule.onFocusedText(item3).assertIsDisplayed()
-    }
-
-    @Test
-    fun exposedDropdownMenuCheckBox_valueDeleted() {
-        composeTestRule.setContent {
-            SettingsExposedDropdownMenuCheckBox(
-                label = exposedDropdownMenuCheckBoxLabel,
-                options = options,
-                selectedOptionsState = remember { selectedOptionsState1 },
-                enabled = true,
-            ) {}
-        }
-        composeTestRule.onNodeWithText(item2, substring = true).assertIsDisplayed()
-        composeTestRule.onNodeWithText(exposedDropdownMenuCheckBoxLabel, substring = true)
-            .performClick()
-        composeTestRule.onNotFocusedText(item2).performClick()
-        composeTestRule.onFocusedText(item2).assertDoesNotExist()
-    }
-}
-
-fun ComposeContentTestRule.onFocusedText(text: String): SemanticsNodeInteraction =
-    onNode(isFocused() and hasText(text, substring = true))
-
-fun ComposeContentTestRule.onNotFocusedText(text: String): SemanticsNodeInteraction =
-    onNode(!isFocused() and hasText(text, substring = true))
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/SemanticsMatcher.kt b/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/SemanticsMatcher.kt
new file mode 100644
index 0000000..856bed6
--- /dev/null
+++ b/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/SemanticsMatcher.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.settingslib.spa.testutils
+
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.semantics.getOrNull
+import androidx.compose.ui.test.SemanticsMatcher
+
+fun hasRole(role: Role) = SemanticsMatcher("${SemanticsProperties.Role.name} has $role") {
+    it.config.getOrNull(SemanticsProperties.Role) == role
+}