diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaEnvironment.kt index 3233d4d75631e..0d947342c930f 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaEnvironment.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaEnvironment.kt @@ -25,6 +25,7 @@ import com.android.settingslib.spa.gallery.page.SliderPageProvider import com.android.settingslib.spa.gallery.preference.PreferenceMainPageProvider import com.android.settingslib.spa.gallery.preference.PreferencePageProvider import com.android.settingslib.spa.gallery.preference.SwitchPreferencePageProvider +import com.android.settingslib.spa.gallery.preference.TwoTargetSwitchPreferencePageProvider import com.android.settingslib.spa.gallery.ui.SpinnerPageProvider val galleryPageProviders = SettingsPageProviderRepository( @@ -33,6 +34,7 @@ val galleryPageProviders = SettingsPageProviderRepository( PreferenceMainPageProvider, PreferencePageProvider, SwitchPreferencePageProvider, + TwoTargetSwitchPreferencePageProvider, ArgumentPageProvider, SliderPageProvider, SpinnerPageProvider, diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMain.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMain.kt index d3ca1d3f347ea..e71c30cc1f3d0 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMain.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/PreferenceMain.kt @@ -48,5 +48,6 @@ private fun PreferenceMain() { RegularScaffold(title = TITLE) { PreferencePageProvider.EntryItem() SwitchPreferencePageProvider.EntryItem() + TwoTargetSwitchPreferencePageProvider.EntryItem() } } diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePage.kt new file mode 100644 index 0000000000000..894692be13b88 --- /dev/null +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePage.kt @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2022 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.preference + +import android.os.Bundle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.tooling.preview.Preview +import com.android.settingslib.spa.framework.common.SettingsPageProvider +import com.android.settingslib.spa.framework.compose.navigator +import com.android.settingslib.spa.framework.compose.stateOf +import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel +import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference +import com.android.settingslib.spa.widget.scaffold.RegularScaffold +import kotlinx.coroutines.delay + +private const val TITLE = "Sample TwoTargetSwitchPreference" + +object TwoTargetSwitchPreferencePageProvider : SettingsPageProvider { + override val name = "TwoTargetSwitchPreference" + + @Composable + override fun Page(arguments: Bundle?) { + TwoTargetSwitchPreferencePage() + } + + @Composable + fun EntryItem() { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) + } +} + +@Composable +private fun TwoTargetSwitchPreferencePage() { + RegularScaffold(title = TITLE) { + SampleTwoTargetSwitchPreference() + SampleTwoTargetSwitchPreferenceWithSummary() + SampleTwoTargetSwitchPreferenceWithAsyncSummary() + SampleNotChangeableTwoTargetSwitchPreference() + } +} + +@Composable +private fun SampleTwoTargetSwitchPreference() { + val checked = rememberSaveable { mutableStateOf(false) } + TwoTargetSwitchPreference(remember { + object : SwitchPreferenceModel { + override val title = "TwoTargetSwitchPreference" + override val checked = checked + override val onCheckedChange = { newChecked: Boolean -> checked.value = newChecked } + } + }) {} +} + +@Composable +private fun SampleTwoTargetSwitchPreferenceWithSummary() { + val checked = rememberSaveable { mutableStateOf(true) } + TwoTargetSwitchPreference(remember { + object : SwitchPreferenceModel { + override val title = "TwoTargetSwitchPreference" + override val summary = stateOf("With summary") + override val checked = checked + override val onCheckedChange = { newChecked: Boolean -> checked.value = newChecked } + } + }) {} +} + +@Composable +private fun SampleTwoTargetSwitchPreferenceWithAsyncSummary() { + val checked = rememberSaveable { mutableStateOf(true) } + val summary = produceState(initialValue = " ") { + delay(1000L) + value = "Async summary" + } + TwoTargetSwitchPreference(remember { + object : SwitchPreferenceModel { + override val title = "TwoTargetSwitchPreference" + override val summary = summary + override val checked = checked + override val onCheckedChange = { newChecked: Boolean -> checked.value = newChecked } + } + }) {} +} + +@Composable +private fun SampleNotChangeableTwoTargetSwitchPreference() { + val checked = rememberSaveable { mutableStateOf(true) } + TwoTargetSwitchPreference(remember { + object : SwitchPreferenceModel { + override val title = "TwoTargetSwitchPreference" + override val summary = stateOf("Not changeable") + override val changeable = stateOf(false) + override val checked = checked + override val onCheckedChange = { newChecked: Boolean -> checked.value = newChecked } + } + }) {} +} + +@Preview(showBackground = true) +@Composable +private fun TwoTargetSwitchPreferencePagePreview() { + SettingsTheme { + TwoTargetSwitchPreferencePage() + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt index 965436847c642..c1a3c20181868 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt @@ -32,6 +32,7 @@ object SettingsDimension { bottom = itemPaddingVertical, ) val itemPaddingAround = 8.dp + val itemDividerHeight = 32.dp /** The size when app icon is displayed in list. */ val appIconItemSize = 32.dp diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt index 04ee3c3be402f..11af6ce0beabe 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsOpacity.kt @@ -19,4 +19,5 @@ package com.android.settingslib.spa.framework.theme object SettingsOpacity { const val Full = 1f const val Disabled = 0.38f + const val Divider = 0.2f } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetPreference.kt new file mode 100644 index 0000000000000..507421bf4a975 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetPreference.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2022 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.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.framework.theme.SettingsOpacity + +@Composable +internal fun TwoTargetPreference( + title: String, + summary: State, + onClick: () -> Unit, + icon: @Composable (() -> Unit)? = null, + widget: @Composable () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(end = SettingsDimension.itemPaddingEnd), + verticalAlignment = Alignment.CenterVertically, + ) { + Box(modifier = Modifier.weight(1f)) { + Preference(remember { + object : PreferenceModel { + override val title = title + override val summary = summary + override val icon = icon + override val onClick = onClick + } + }) + } + PreferenceDivider() + widget() + } +} + +@Composable +private fun PreferenceDivider() { + Box( + Modifier + .padding(horizontal = SettingsDimension.itemPaddingEnd) + .size(width = 1.dp, height = SettingsDimension.itemDividerHeight) + .background(color = MaterialTheme.colorScheme.onSurface.copy(SettingsOpacity.Disabled)) + ) +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt new file mode 100644 index 0000000000000..f1541b7f40084 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreference.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2022 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.Composable +import com.android.settingslib.spa.widget.ui.SettingsSwitch + +@Composable +fun TwoTargetSwitchPreference( + model: SwitchPreferenceModel, + icon: @Composable (() -> Unit)? = null, + onClick: () -> Unit, +) { + TwoTargetPreference( + title = model.title, + summary = model.summary, + onClick = onClick, + icon = icon, + ) { + SettingsSwitch( + checked = model.checked, + changeable = model.changeable, + onCheckedChange = model.onCheckedChange, + ) + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt index 8b530b0d7b9b1..1af4ce7553d8d 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsScaffold.kt @@ -26,6 +26,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsTheme @@ -47,6 +48,8 @@ fun SettingsScaffold( Text( text = title, modifier = Modifier.padding(SettingsDimension.itemPaddingAround), + overflow = TextOverflow.Ellipsis, + maxLines = 1, ) }, navigationIcon = { NavigateBack() }, diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreferenceTest.kt new file mode 100644 index 0000000000000..c49e92d52edb8 --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/TwoTargetSwitchPreferenceTest.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2022 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.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsOff +import androidx.compose.ui.test.assertIsOn +import androidx.compose.ui.test.isToggleable +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.framework.compose.stateOf +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class TwoTargetSwitchPreferenceTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun title_displayed() { + val checked = mutableStateOf(false) + composeTestRule.setContent { + TestTwoTargetSwitchPreference(checked = checked, changeable = true) + } + + composeTestRule.onNodeWithText("TwoTargetSwitchPreference").assertIsDisplayed() + } + + @Test + fun toggleable_initialStateIsCorrect() { + val checked = mutableStateOf(false) + composeTestRule.setContent { + TestTwoTargetSwitchPreference(checked = checked, changeable = true) + } + + composeTestRule.onNode(isToggleable()).assertIsOff() + } + + @Test + fun toggleable_changeable_withEffect() { + val checked = mutableStateOf(false) + composeTestRule.setContent { + TestTwoTargetSwitchPreference(checked = checked, changeable = true) + } + + composeTestRule.onNode(isToggleable()).performClick() + composeTestRule.onNode(isToggleable()).assertIsOn() + } + + @Test + fun toggleable_notChangeable_noEffect() { + val checked = mutableStateOf(false) + composeTestRule.setContent { + TestTwoTargetSwitchPreference(checked = checked, changeable = false) + } + + composeTestRule.onNode(isToggleable()).performClick() + composeTestRule.onNode(isToggleable()).assertIsOff() + } + + @Test + fun clickable_canBeClick() { + val checked = mutableStateOf(false) + var clicked = false + composeTestRule.setContent { + TestTwoTargetSwitchPreference(checked = checked, changeable = false) { + clicked = true + } + } + + composeTestRule.onNodeWithText("TwoTargetSwitchPreference").performClick() + assertThat(clicked).isTrue() + } +} + +@Composable +private fun TestTwoTargetSwitchPreference( + checked: MutableState, + changeable: Boolean, + onClick: () -> Unit = {}, +) { + TwoTargetSwitchPreference( + model = remember { + object : SwitchPreferenceModel { + override val title = "TwoTargetSwitchPreference" + override val checked = checked + override val changeable = stateOf(changeable) + override val onCheckedChange = { newChecked: Boolean -> checked.value = newChecked } + } + }, + onClick = onClick, + ) +}