Merge "[Spa] Add dismiss button for SettingsCard" into main
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt
index f216abb..66bd6f5 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt
@@ -28,7 +28,14 @@
 import androidx.compose.material.icons.outlined.WarningAmber
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.tooling.preview.Preview
 import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
@@ -41,14 +48,14 @@
 import com.android.settingslib.spa.widget.card.CardButton
 import com.android.settingslib.spa.widget.card.CardModel
 import com.android.settingslib.spa.widget.card.SettingsCard
-import com.android.settingslib.spa.widget.card.SettingsCollapsibleCard
 import com.android.settingslib.spa.widget.card.SettingsCardContent
+import com.android.settingslib.spa.widget.card.SettingsCollapsibleCard
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.preference.PreferenceModel
 import com.android.settingslib.spa.widget.scaffold.RegularScaffold
 
 object CardPageProvider : SettingsPageProvider {
-    override val name = "CardPage"
+    override val name = "Card"
 
     override fun getTitle(arguments: Bundle?) = TITLE
 
@@ -79,10 +86,13 @@
 
     @Composable
     private fun SettingsCardWithoutIcon() {
+        var isVisible by rememberSaveable { mutableStateOf(true) }
         SettingsCard(
             CardModel(
                 title = stringResource(R.string.sample_title),
                 text = stringResource(R.string.sample_text),
+                isVisible = { isVisible },
+                onDismiss = { isVisible = false },
                 buttons = listOf(
                     CardButton(text = "Action") {},
                 ),
@@ -92,21 +102,23 @@
 
     @Composable
     fun SampleSettingsCollapsibleCard() {
-        SettingsCollapsibleCard(
-            title = "More alerts",
-            imageVector = Icons.Outlined.Error,
-            models = listOf(
+        val context = LocalContext.current
+        var isVisible0 by rememberSaveable { mutableStateOf(true) }
+        val cards = remember {
+            mutableStateListOf(
                 CardModel(
-                    title = stringResource(R.string.sample_title),
-                    text = stringResource(R.string.sample_text),
+                    title = context.getString(R.string.sample_title),
+                    text = context.getString(R.string.sample_text),
                     imageVector = Icons.Outlined.PowerOff,
+                    isVisible = { isVisible0 },
+                    onDismiss = { isVisible0 = false },
                     buttons = listOf(
                         CardButton(text = "Action") {},
                     )
                 ),
                 CardModel(
-                    title = stringResource(R.string.sample_title),
-                    text = stringResource(R.string.sample_text),
+                    title = context.getString(R.string.sample_title),
+                    text = context.getString(R.string.sample_text),
                     imageVector = Icons.Outlined.Shield,
                     buttons = listOf(
                         CardButton(text = "Action") {},
@@ -114,6 +126,11 @@
                     )
                 )
             )
+        }
+        SettingsCollapsibleCard(
+            title = "More alerts",
+            imageVector = Icons.Outlined.Error,
+            models = cards.toList()
         )
     }
 
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 c143390..993cb4a 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
@@ -37,6 +37,7 @@
     val itemPaddingAround = 8.dp
     val itemDividerHeight = 32.dp
 
+    val iconSmall = 16.dp
     val iconLarge = 48.dp
 
     /** The size when app icon is displayed in list. */
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/CardModel.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/CardModel.kt
index c113f43..b18a1bc 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/CardModel.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/CardModel.kt
@@ -28,5 +28,14 @@
     val title: String,
     val text: String,
     val imageVector: ImageVector? = null,
+    val isVisible: () -> Boolean = { true },
+
+    /**
+     * A dismiss button will be displayed if this is not null.
+     *
+     * And this callback will be called when user clicks the button.
+     */
+    val onDismiss: (() -> Unit)? = null,
+
     val buttons: List<CardButton> = emptyList(),
 )
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt
index 4379278..7eec888 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt
@@ -16,28 +16,35 @@
 
 package com.android.settingslib.spa.widget.card
 
+import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.ColumnScope
 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.size
+import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Close
 import androidx.compose.material.icons.outlined.WarningAmber
 import androidx.compose.material3.Button
 import androidx.compose.material3.ButtonDefaults
 import androidx.compose.material3.Card
 import androidx.compose.material3.CardDefaults
 import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Surface
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import com.android.settingslib.spa.debug.UiModePreviews
 import com.android.settingslib.spa.framework.theme.SettingsDimension
@@ -87,20 +94,31 @@
 
 @Composable
 internal fun SettingsCardImpl(model: CardModel) {
-    SettingsCardContent {
-        Column(
-            modifier = Modifier.padding(SettingsDimension.itemPaddingStart),
-            verticalArrangement = Arrangement.spacedBy(SettingsDimension.itemPaddingAround)
-        ) {
-            CardIcon(model.imageVector)
-            SettingsTitle(model.title)
-            SettingsBody(model.text)
-            Buttons(model.buttons)
+    AnimatedVisibility(visible = model.isVisible()) {
+        SettingsCardContent {
+            Column(
+                modifier = Modifier.padding(SettingsDimension.itemPaddingStart),
+                verticalArrangement = Arrangement.spacedBy(SettingsDimension.itemPaddingAround)
+            ) {
+                CardHeader(model.imageVector, model.onDismiss)
+                SettingsTitle(model.title)
+                SettingsBody(model.text)
+                Buttons(model.buttons)
+            }
         }
     }
 }
 
 @Composable
+fun CardHeader(imageVector: ImageVector?, onDismiss: (() -> Unit)? = null) {
+    Row(Modifier.fillMaxWidth()) {
+        CardIcon(imageVector)
+        Spacer(modifier = Modifier.weight(1f))
+        DismissButton(onDismiss)
+    }
+}
+
+@Composable
 private fun CardIcon(imageVector: ImageVector?) {
     if (imageVector != null) {
         Icon(
@@ -113,6 +131,28 @@
 }
 
 @Composable
+private fun DismissButton(onDismiss: (() -> Unit)?) {
+    if (onDismiss == null) return
+    Surface(
+        shape = CircleShape,
+        color = MaterialTheme.colorScheme.secondaryContainer,
+    ) {
+        IconButton(
+            onClick = onDismiss,
+            modifier = Modifier.size(SettingsDimension.itemIconSize)
+        ) {
+            Icon(
+                imageVector = Icons.Outlined.Close,
+                contentDescription = stringResource(
+                    androidx.compose.material3.R.string.m3c_snackbar_dismiss
+                ),
+                modifier = Modifier.size(SettingsDimension.iconSmall),
+            )
+        }
+    }
+}
+
+@Composable
 private fun Buttons(buttons: List<CardButton>) {
     if (buttons.isNotEmpty()) {
         Row(
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCollapsibleCard.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCollapsibleCard.kt
index bf192a1..6e36490 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCollapsibleCard.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCollapsibleCard.kt
@@ -58,7 +58,7 @@
     var expanded by rememberSaveable { mutableStateOf(false) }
     SettingsCard {
         SettingsCardContent {
-            Header(title, imageVector, models.size, expanded) { expanded = it }
+            Header(title, imageVector, models.count { it.isVisible() }, expanded) { expanded = it }
         }
         AnimatedVisibility(expanded) {
             Column {
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCardTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCardTest.kt
index fd3ae49..beb9433 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCardTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCardTest.kt
@@ -16,10 +16,18 @@
 
 package com.android.settingslib.spa.widget.card
 
+import android.content.Context
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.isNotDisplayed
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithContentDescription
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.performClick
+import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
@@ -31,6 +39,8 @@
     @get:Rule
     val composeTestRule = createComposeRule()
 
+    private val context: Context = ApplicationProvider.getApplicationContext()
+
     @Test
     fun settingsCard_titleDisplayed() {
         composeTestRule.setContent {
@@ -96,6 +106,27 @@
         assertThat(buttonClicked).isTrue()
     }
 
+    @Test
+    fun settingsCard_dismiss() {
+        composeTestRule.setContent {
+            var isVisible by remember { mutableStateOf(true) }
+            SettingsCard(
+                CardModel(
+                    title = TITLE,
+                    text = "",
+                    isVisible = { isVisible },
+                    onDismiss = { isVisible = false },
+                )
+            )
+        }
+
+        composeTestRule.onNodeWithContentDescription(
+            context.getString(androidx.compose.material3.R.string.m3c_snackbar_dismiss)
+        ).performClick()
+
+        composeTestRule.onNodeWithText(TEXT).isNotDisplayed()
+    }
+
     private companion object {
         const val TITLE = "Title"
         const val TEXT = "Text"
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCollapsibleCardTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCollapsibleCardTest.kt
index efe1c70..aba9d7b 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCollapsibleCardTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCollapsibleCardTest.kt
@@ -16,12 +16,20 @@
 
 package com.android.settingslib.spa.widget.card
 
+import android.content.Context
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.outlined.Error
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.isNotDisplayed
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithContentDescription
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.performClick
+import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import org.junit.Rule
 import org.junit.Test
@@ -32,6 +40,8 @@
     @get:Rule
     val composeTestRule = createComposeRule()
 
+    private val context: Context = ApplicationProvider.getApplicationContext()
+
     @Test
     fun settingsCollapsibleCard_titleDisplayed() {
         setContent()
@@ -62,8 +72,22 @@
         composeTestRule.onNodeWithText(CARD_TEXT).assertIsDisplayed()
     }
 
+    @Test
+    fun settingsCollapsibleCard_dismiss() {
+        setContent()
+        composeTestRule.onNodeWithText(TITLE).performClick()
+
+        composeTestRule.onNodeWithContentDescription(
+            context.getString(androidx.compose.material3.R.string.m3c_snackbar_dismiss)
+        ).performClick()
+
+        composeTestRule.onNodeWithText(CARD_TEXT).isNotDisplayed()
+        composeTestRule.onNodeWithText("0").assertIsDisplayed()
+    }
+
     private fun setContent() {
         composeTestRule.setContent {
+            var isVisible by rememberSaveable { mutableStateOf(true) }
             SettingsCollapsibleCard(
                 title = TITLE,
                 imageVector = Icons.Outlined.Error,
@@ -71,6 +95,8 @@
                     CardModel(
                         title = "",
                         text = CARD_TEXT,
+                        isVisible = { isVisible },
+                        onDismiss = { isVisible = false },
                     )
                 ),
             )