Merge "Add ListPreference2." into main
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/RadioPreferences.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/RadioPreferences.kt
new file mode 100644
index 0000000..8300ce8
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/RadioPreferences.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.preference
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.selection.selectable
+import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.material3.RadioButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.Role
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.widget.ui.CategoryTitle
+import com.android.settingslib.spa.widget.ui.SettingsListItem
+
+@Composable
+fun RadioPreferences(model: ListPreferenceModel) {
+    CategoryTitle(title = model.title)
+    Spacer(modifier = Modifier.width(SettingsDimension.itemDividerHeight))
+    Column(modifier = Modifier.selectableGroup()) {
+        for (option in model.options) {
+            Radio2(option, model.selectedId.intValue, model.enabled()) {
+                model.onIdSelected(it)
+            }
+        }
+    }
+}
+
+@Composable
+fun Radio2(
+    option: ListPreferenceOption,
+    selectedId: Int,
+    enabled: Boolean,
+    onIdSelected: (id: Int) -> Unit,
+) {
+    val selected = option.id == selectedId
+    Row(
+        modifier = Modifier
+            .fillMaxWidth()
+            .selectable(
+                selected = selected,
+                enabled = enabled,
+                onClick = { onIdSelected(option.id) },
+                role = Role.RadioButton,
+            )
+            .padding(SettingsDimension.dialogItemPadding),
+        verticalAlignment = Alignment.CenterVertically,
+    ) {
+        RadioButton(selected = selected, onClick = null, enabled = enabled)
+        Spacer(modifier = Modifier.width(SettingsDimension.itemDividerHeight))
+        SettingsListItem(text = option.text, enabled = enabled)
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt
index b4a6a0d..a59b95a 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt
@@ -66,6 +66,19 @@
 }
 
 @Composable
+fun SettingsListItem(text: String, enabled: Boolean = true) {
+    Text(
+        text = text,
+        modifier = Modifier
+            .alphaForEnabled(enabled)
+            .padding(vertical = SettingsDimension.paddingTiny),
+        color = MaterialTheme.colorScheme.onSurface,
+        style = MaterialTheme.typography.titleMedium,
+        overflow = TextOverflow.Ellipsis,
+    )
+}
+
+@Composable
 fun SettingsBody(
     body: String,
     maxLines: Int = Int.MAX_VALUE,
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/RadioPreferencesTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/RadioPreferencesTest.kt
new file mode 100644
index 0000000..2f98b02
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/RadioPreferencesTest.kt
@@ -0,0 +1,150 @@
+/*
+ * 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.preference
+
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.assertIsNotSelected
+import androidx.compose.ui.test.assertIsSelectable
+import androidx.compose.ui.test.assertIsSelected
+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 RadioPreferencesTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Test
+    fun title_displayed() {
+        composeTestRule.setContent {
+            RadioPreferences(remember {
+                object : ListPreferenceModel {
+                    override val title = TITLE
+                    override val options = emptyList<ListPreferenceOption>()
+                    override val selectedId = mutableIntStateOf(0)
+                    override val onIdSelected: (Int) -> Unit = {}
+                }
+            })
+        }
+        composeTestRule.onNodeWithText(TITLE).assertIsDisplayed()
+    }
+
+    @Test
+    fun item_displayed() {
+        val selectedId = mutableIntStateOf(1)
+        composeTestRule.setContent {
+            RadioPreferences(remember {
+                object : ListPreferenceModel {
+                    override val title = TITLE
+                    override val options = listOf(
+                        ListPreferenceOption(id = 1, text = "A"),
+                        ListPreferenceOption(id = 2, text = "B"),
+                    )
+                    override val selectedId = selectedId
+                    override val onIdSelected = { id: Int -> selectedId.intValue = id }
+                }
+            })
+        }
+        composeTestRule.onNodeWithText("A").assertIsDisplayed()
+        composeTestRule.onNodeWithText("B").assertIsDisplayed()
+    }
+
+    @Test
+    fun item_selectable() {
+        val selectedId = mutableIntStateOf(1)
+        val enabledState = mutableStateOf(true)
+        composeTestRule.setContent {
+            RadioPreferences(remember {
+                object : ListPreferenceModel {
+                    override val title = TITLE
+                    override val enabled = { enabledState.value }
+                    override val options = listOf(
+                        ListPreferenceOption(id = 1, text = "A"),
+                        ListPreferenceOption(id = 2, text = "B"),
+                    )
+                    override val selectedId = selectedId
+                    override val onIdSelected = { id: Int -> selectedId.intValue = id }
+                }
+            })
+        }
+        composeTestRule.onNodeWithText("A").assertIsSelectable()
+        composeTestRule.onNodeWithText("B").assertIsSelectable()
+    }
+
+    @Test
+    fun item_single_selected() {
+        val selectedId = mutableIntStateOf(1)
+        val enabledState = mutableStateOf(true)
+        composeTestRule.setContent {
+            RadioPreferences(remember {
+                object : ListPreferenceModel {
+                    override val title = TITLE
+                    override val enabled = { enabledState.value }
+                    override val options = listOf(
+                        ListPreferenceOption(id = 1, text = "A"),
+                        ListPreferenceOption(id = 2, text = "B"),
+                    )
+                    override val selectedId = selectedId
+                    override val onIdSelected = { id: Int -> selectedId.intValue = id }
+                }
+            })
+        }
+        composeTestRule.onNodeWithText("B").assertIsSelectable()
+        composeTestRule.onNodeWithText("B").performClick()
+        composeTestRule.onNodeWithText("B").assertIsSelected()
+        composeTestRule.onNodeWithText("A").assertIsNotSelected()
+        composeTestRule.onNodeWithText("A").performClick()
+        composeTestRule.onNodeWithText("A").assertIsSelected()
+        composeTestRule.onNodeWithText("B").assertIsNotSelected()
+    }
+
+    @Test
+    fun select_itemDisabled() {
+        val selectedId = mutableIntStateOf(1)
+        val enabledState = mutableStateOf(true)
+        composeTestRule.setContent {
+            RadioPreferences(remember {
+                object : ListPreferenceModel {
+                    override val title = TITLE
+                    override val enabled = { enabledState.value }
+                    override val options = listOf(
+                        ListPreferenceOption(id = 1, text = "A"),
+                        ListPreferenceOption(id = 2, text = "B"),
+                    )
+                    override val selectedId = selectedId
+                    override val onIdSelected = { id: Int -> selectedId.intValue = id }
+                }
+            })
+        }
+        enabledState.value = false
+        composeTestRule.onNodeWithText("A").assertIsDisplayed().assertIsNotEnabled()
+        composeTestRule.onNodeWithText("B").assertIsDisplayed().assertIsNotEnabled()
+    }
+
+    private companion object {
+        const val TITLE = "Title"
+    }
+}
\ No newline at end of file