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 e5d009930edf0..783fef7e91cfa 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 @@ -19,6 +19,7 @@ package com.android.settingslib.spa.gallery import com.android.settingslib.spa.framework.common.SettingsEntryRepository import com.android.settingslib.spa.framework.common.SettingsPage import com.android.settingslib.spa.framework.common.SettingsPageProviderRepository +import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider import com.android.settingslib.spa.gallery.home.HomePageProvider import com.android.settingslib.spa.gallery.page.ArgumentPageProvider import com.android.settingslib.spa.gallery.page.FooterPageProvider @@ -51,6 +52,7 @@ object SpaEnvironment { FooterPageProvider, IllustrationPageProvider, CategoryPageProvider, + ActionButtonPageProvider, ), rootPages = listOf( SettingsPage.create(HomePageProvider.name) diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/button/ActionButtonsPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/button/ActionButtonsPage.kt new file mode 100644 index 0000000000000..96e2498e39bf1 --- /dev/null +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/button/ActionButtonsPage.kt @@ -0,0 +1,77 @@ +/* + * 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.button + +import android.os.Bundle +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Launch +import androidx.compose.material.icons.outlined.WarningAmber +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.android.settingslib.spa.framework.common.SettingsEntryBuilder +import com.android.settingslib.spa.framework.common.SettingsPage +import com.android.settingslib.spa.framework.common.SettingsPageProvider +import com.android.settingslib.spa.framework.compose.navigator +import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.button.ActionButton +import com.android.settingslib.spa.widget.button.ActionButtons +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 ActionButton" + +object ActionButtonPageProvider : SettingsPageProvider { + override val name = "ActionButton" + + @Composable + override fun Page(arguments: Bundle?) { + ActionButtonPage() + } + + fun buildInjectEntry(): SettingsEntryBuilder { + return SettingsEntryBuilder.createInject(owner = SettingsPage.create(name)) + .setIsAllowSearch(true) + .setUiLayoutFn { + Preference(object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + }) + } + } +} + +@Composable +fun ActionButtonPage() { + RegularScaffold(title = TITLE) { + val actionButtons = listOf( + ActionButton(text = "Open", imageVector = Icons.Outlined.Launch) {}, + ActionButton(text = "Uninstall", imageVector = Icons.Outlined.Delete) {}, + ActionButton(text = "Force stop", imageVector = Icons.Outlined.WarningAmber) {}, + ) + ActionButtons(actionButtons) + } +} + +@Preview(showBackground = true) +@Composable +private fun ActionButtonPagePreview() { + SettingsTheme { + ActionButtonPage() + } +} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt index f840a37fa81b3..a16ee6c854098 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePage.kt @@ -25,6 +25,7 @@ import com.android.settingslib.spa.framework.common.SettingsPage import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.gallery.R +import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider import com.android.settingslib.spa.gallery.page.ArgumentPageModel import com.android.settingslib.spa.gallery.page.ArgumentPageProvider import com.android.settingslib.spa.gallery.page.FooterPageProvider @@ -50,6 +51,7 @@ object HomePageProvider : SettingsPageProvider { FooterPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), IllustrationPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), CategoryPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), + ActionButtonPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), ) } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt index d1a11384ff47c..3fa8c658e1989 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/MaterialColors.kt @@ -24,6 +24,7 @@ import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext @Composable @@ -40,3 +41,6 @@ internal fun materialColorScheme(isDarkTheme: Boolean): ColorScheme { } } } + +val ColorScheme.divider: Color + get() = onSurface.copy(SettingsOpacity.Divider) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt new file mode 100644 index 0000000000000..3555ec7467894 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt @@ -0,0 +1,127 @@ +/* + * 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.button + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Launch +import androidx.compose.material.icons.outlined.WarningAmber +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.vector.ImageVector +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.SettingsShape +import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.framework.theme.divider + +data class ActionButton( + val text: String, + val imageVector: ImageVector, + val onClick: () -> Unit, +) + +@Composable +fun ActionButtons(actionButtons: List) { + Row( + Modifier + .padding(SettingsDimension.itemPaddingVertical) + .clip(SettingsShape.CornerLarge) + .height(IntrinsicSize.Min) + ) { + for ((index, actionButton) in actionButtons.withIndex()) { + if (index > 0) ButtonDivider() + ActionButton(actionButton) + } + } +} + +@Composable +private fun RowScope.ActionButton(actionButton: ActionButton) { + FilledTonalButton( + onClick = actionButton.onClick, + modifier = Modifier + .weight(1f) + .fillMaxHeight(), + shape = RectangleShape, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = SettingsTheme.colorScheme.categoryTitle, + ), + contentPadding = PaddingValues(horizontal = 4.dp, vertical = 20.dp), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + imageVector = actionButton.imageVector, + contentDescription = null, + modifier = Modifier.size(SettingsDimension.itemIconSize), + ) + Spacer(Modifier.height(4.dp)) + Text( + text = actionButton.text, + style = MaterialTheme.typography.labelLarge, + ) + } + } +} + +@Composable +private fun ButtonDivider() { + Box( + Modifier + .width(1.dp) + .background(color = MaterialTheme.colorScheme.divider) + ) +} + +@Preview +@Composable +private fun ActionButtonsPreview() { + SettingsTheme { + ActionButtons( + listOf( + ActionButton(text = "Open", imageVector = Icons.Outlined.Launch) {}, + ActionButton(text = "Uninstall", imageVector = Icons.Outlined.Delete) {}, + ActionButton(text = "Force stop", imageVector = Icons.Outlined.WarningAmber) {}, + ) + ) + } +} 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 index 77ea3c5448e4e..566361029ff6a 100644 --- 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 @@ -30,7 +30,7 @@ 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 +import com.android.settingslib.spa.framework.theme.divider @Composable internal fun TwoTargetPreference( @@ -67,6 +67,6 @@ private fun PreferenceDivider() { Modifier .padding(horizontal = SettingsDimension.itemPaddingEnd) .size(width = 1.dp, height = SettingsDimension.itemDividerHeight) - .background(color = MaterialTheme.colorScheme.onSurface.copy(SettingsOpacity.Divider)) + .background(color = MaterialTheme.colorScheme.divider) ) } diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/button/ActionButtonsTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/button/ActionButtonsTest.kt new file mode 100644 index 0000000000000..8101a94a9461a --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/button/ActionButtonsTest.kt @@ -0,0 +1,69 @@ +/* + * 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.button + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Launch +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.test.assertIsDisplayed +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.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ActionButtonsTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun button_displayed() { + composeTestRule.setContent { + ActionButtons( + listOf( + ActionButton(text = "Open", imageVector = Icons.Outlined.Launch) {}, + ) + ) + } + + composeTestRule.onNodeWithText("Open").assertIsDisplayed() + } + + @Test + fun button_clickable() { + var clicked by mutableStateOf(false) + composeTestRule.setContent { + ActionButtons( + listOf( + ActionButton(text = "Open", imageVector = Icons.Outlined.Launch) { + clicked = true + }, + ) + ) + } + + composeTestRule.onNodeWithText("Open").performClick() + + assertThat(clicked).isTrue() + } +}