Implement the QS footer actions in Compose (1/2)
This CL reimplements the FooterActions in Compose. This is going to be
one of the first features to be hidden behind a build time flag in our
SystemUICompose build once that flag is available.
See http://b/242040009#comment20 for a video and http://ag/20745047 for
the screenshot difference between the Compose and View implementations.
Bug: 242040009
Test: atest FooterActionsScreenshotTest
Change-Id: I83df8d191c1ab5d95466471c834ac1b0903ad21a
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index b925921..d6fdc55 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2331,6 +2331,7 @@
<java-symbol type="drawable" name="scrubber_control_selector_holo" />
<java-symbol type="drawable" name="scrubber_progress_horizontal_holo_dark" />
<java-symbol type="drawable" name="progress_small_material" />
+ <java-symbol type="drawable" name="ic_chevron_end" />
<java-symbol type="string" name="chooseUsbActivity" />
<java-symbol type="string" name="ext_media_badremoval_notification_message" />
<java-symbol type="string" name="ext_media_badremoval_notification_title" />
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/Expandable.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/Expandable.kt
index 18534f4..d31ca51 100644
--- a/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/Expandable.kt
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/animation/Expandable.kt
@@ -24,6 +24,7 @@
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
@@ -62,6 +63,7 @@
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.layout.boundsInRoot
+import androidx.compose.ui.layout.findRootCoordinates
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.ComposeView
@@ -118,12 +120,14 @@
contentColor: Color = contentColorFor(color),
borderStroke: BorderStroke? = null,
onClick: ((Expandable) -> Unit)? = null,
+ interactionSource: MutableInteractionSource? = null,
content: @Composable (Expandable) -> Unit,
) {
Expandable(
rememberExpandableController(color, shape, contentColor, borderStroke),
modifier,
onClick,
+ interactionSource,
content,
)
}
@@ -158,6 +162,7 @@
controller: ExpandableController,
modifier: Modifier = Modifier,
onClick: ((Expandable) -> Unit)? = null,
+ interactionSource: MutableInteractionSource? = null,
content: @Composable (Expandable) -> Unit,
) {
val controller = controller as ExpandableControllerImpl
@@ -190,6 +195,18 @@
var thisExpandableSize by remember { mutableStateOf(Size.Zero) }
+ /** Set the current element size as this Expandable size. */
+ fun Modifier.updateExpandableSize(): Modifier {
+ return this.onGloballyPositioned { coords ->
+ thisExpandableSize =
+ coords
+ .findRootCoordinates()
+ // Make sure that we report the actual size, and not the visual/clipped one.
+ .localBoundingBoxOf(coords, clipBounds = false)
+ .size
+ }
+ }
+
// Make sure we don't read animatorState directly here to avoid recomposition every time the
// state changes (i.e. every frame of the animation).
val isAnimating by remember {
@@ -247,7 +264,7 @@
controller.isDialogShowing.value -> {
Box(
modifier
- .onGloballyPositioned { thisExpandableSize = it.boundsInRoot().size }
+ .updateExpandableSize()
.then(minInteractiveSizeModifier)
.drawWithContent { /* Don't draw anything when the dialog is shown. */}
.onGloballyPositioned {
@@ -258,18 +275,25 @@
else -> {
val clickModifier =
if (onClick != null) {
- Modifier.clickable { onClick(controller.expandable) }
+ if (interactionSource != null) {
+ // If the caller provided an interaction source, then that means that they
+ // will draw the click indication themselves.
+ Modifier.clickable(interactionSource, indication = null) {
+ onClick(controller.expandable)
+ }
+ } else {
+ // If no interaction source is provided, we draw the default indication (a
+ // ripple) and make sure it's clipped by the expandable shape.
+ Modifier.clip(shape).clickable { onClick(controller.expandable) }
+ }
} else {
Modifier
}
Box(
modifier
- .onGloballyPositioned { thisExpandableSize = it.boundsInRoot().size }
+ .updateExpandableSize()
.then(minInteractiveSizeModifier)
- // Note that clip() *must* be above the clickModifier to properly clip the
- // ripple.
- .clip(shape)
.then(clickModifier)
.background(color, shape)
.border(controller)
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/AndroidColorScheme.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/AndroidColorScheme.kt
index b8639e6..caa7e5f 100644
--- a/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/AndroidColorScheme.kt
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/AndroidColorScheme.kt
@@ -65,10 +65,12 @@
val colorForeground = getColor(context, R.attr.colorForeground)
val colorForegroundInverse = getColor(context, R.attr.colorForegroundInverse)
- private fun getColor(context: Context, attr: Int): Color {
- val ta = context.obtainStyledAttributes(intArrayOf(attr))
- @ColorInt val color = ta.getColor(0, 0)
- ta.recycle()
- return Color(color)
+ companion object {
+ fun getColor(context: Context, attr: Int): Color {
+ val ta = context.obtainStyledAttributes(intArrayOf(attr))
+ @ColorInt val color = ta.getColor(0, 0)
+ ta.recycle()
+ return Color(color)
+ }
}
}
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/Color.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/Color.kt
new file mode 100644
index 0000000..de47cce
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/Color.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.systemui.compose.theme
+
+import android.annotation.AttrRes
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+
+/** Read the [Color] from the given [attribute]. */
+@Composable
+@ReadOnlyComposable
+fun colorAttr(@AttrRes attribute: Int): Color {
+ return AndroidColorScheme.getColor(LocalContext.current, attribute)
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/ContentDescription.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/ContentDescription.kt
new file mode 100644
index 0000000..4a5ad65
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/ContentDescription.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.systemui.common.ui.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import com.android.systemui.common.shared.model.ContentDescription
+
+/** Returns the loaded [String] or `null` if there isn't one. */
+@Composable
+fun ContentDescription.load(): String? {
+ return when (this) {
+ is ContentDescription.Loaded -> description
+ is ContentDescription.Resource -> stringResource(res)
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt
new file mode 100644
index 0000000..6e83124
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.systemui.common.ui.compose
+
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.res.painterResource
+import androidx.core.graphics.drawable.toBitmap
+import com.android.systemui.common.shared.model.Icon
+
+/**
+ * Icon composable that draws [icon] using [tint].
+ *
+ * Note: You can use [Color.Unspecified] to disable the tint and keep the original icon colors.
+ */
+@Composable
+fun Icon(
+ icon: Icon,
+ modifier: Modifier = Modifier,
+ tint: Color = LocalContentColor.current,
+) {
+ val contentDescription = icon.contentDescription?.load()
+ when (icon) {
+ is Icon.Loaded -> {
+ Icon(icon.drawable.toBitmap().asImageBitmap(), contentDescription, modifier, tint)
+ }
+ is Icon.Resource -> Icon(painterResource(icon.res), contentDescription, modifier, tint)
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
index 2bf1937..2aac46e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
@@ -53,7 +53,6 @@
import com.android.systemui.compose.theme.LocalAndroidColorScheme
import com.android.systemui.people.ui.viewmodel.PeopleTileViewModel
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
-import kotlinx.coroutines.flow.collect
/**
* Compose the screen associated to a [PeopleViewModel].
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
new file mode 100644
index 0000000..654b723
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
@@ -0,0 +1,361 @@
+/*
+ * 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.systemui.qs.footer.ui.compose
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.LocalIndication
+import androidx.compose.foundation.indication
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+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.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+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.draw.clip
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.em
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.R
+import com.android.systemui.animation.Expandable
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.ui.compose.Icon
+import com.android.systemui.compose.animation.Expandable
+import com.android.systemui.compose.modifiers.background
+import com.android.systemui.compose.theme.LocalAndroidColorScheme
+import com.android.systemui.compose.theme.colorAttr
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsButtonViewModel
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsForegroundServicesButtonViewModel
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
+import kotlinx.coroutines.launch
+
+/** The Quick Settings footer actions row. */
+@Composable
+fun FooterActions(
+ viewModel: FooterActionsViewModel,
+ qsVisibilityLifecycleOwner: LifecycleOwner,
+ modifier: Modifier = Modifier,
+) {
+ val context = LocalContext.current
+
+ // Collect visibility and alphas as soon as we are composed, even when not visible.
+ val isVisible by viewModel.isVisible.collectAsState()
+ val alpha by viewModel.alpha.collectAsState()
+ val backgroundAlpha = viewModel.backgroundAlpha.collectAsState()
+
+ var security by remember { mutableStateOf<FooterActionsSecurityButtonViewModel?>(null) }
+ var foregroundServices by remember {
+ mutableStateOf<FooterActionsForegroundServicesButtonViewModel?>(null)
+ }
+ var userSwitcher by remember { mutableStateOf<FooterActionsButtonViewModel?>(null) }
+
+ LaunchedEffect(
+ context,
+ qsVisibilityLifecycleOwner,
+ viewModel,
+ viewModel.security,
+ viewModel.foregroundServices,
+ viewModel.userSwitcher,
+ ) {
+ launch {
+ // Listen for dialog requests as soon as we are composed, even when not visible.
+ viewModel.observeDeviceMonitoringDialogRequests(context)
+ }
+
+ // Listen for model changes only when QS are visible.
+ qsVisibilityLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ launch { viewModel.security.collect { security = it } }
+ launch { viewModel.foregroundServices.collect { foregroundServices = it } }
+ launch { viewModel.userSwitcher.collect { userSwitcher = it } }
+ }
+ }
+
+ val backgroundColor = colorAttr(R.attr.underSurfaceColor)
+ val contentColor = LocalAndroidColorScheme.current.textColorPrimary
+ val backgroundTopRadius = dimensionResource(R.dimen.qs_corner_radius)
+ val backgroundModifier =
+ remember(
+ backgroundColor,
+ backgroundAlpha,
+ backgroundTopRadius,
+ ) {
+ Modifier.background(
+ backgroundColor,
+ backgroundAlpha::value,
+ RoundedCornerShape(topStart = backgroundTopRadius, topEnd = backgroundTopRadius),
+ )
+ }
+
+ Row(
+ modifier
+ .fillMaxWidth()
+ .graphicsLayer { this.alpha = alpha }
+ .drawWithContent {
+ if (isVisible) {
+ drawContent()
+ }
+ }
+ .then(backgroundModifier)
+ .padding(
+ top = dimensionResource(R.dimen.qs_footer_actions_top_padding),
+ bottom = dimensionResource(R.dimen.qs_footer_actions_bottom_padding),
+ )
+ .layout { measurable, constraints ->
+ // All buttons have a 4dp padding to increase their touch size. To be consistent
+ // with the View implementation, we want to left-most and right-most buttons to be
+ // visually aligned with the left and right sides of this row. So we let this
+ // component be 2*4dp wider and then offset it by -4dp to the start.
+ val inset = 4.dp.roundToPx()
+ val additionalWidth = inset * 2
+ val newConstraints =
+ if (constraints.hasBoundedWidth) {
+ constraints.copy(maxWidth = constraints.maxWidth + additionalWidth)
+ } else {
+ constraints
+ }
+ val placeable = measurable.measure(newConstraints)
+
+ val width = constraints.constrainWidth(placeable.width - additionalWidth)
+ val height = constraints.constrainHeight(placeable.height)
+ layout(width, height) { placeable.place(-inset, 0) }
+ },
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ CompositionLocalProvider(
+ LocalContentColor provides contentColor,
+ ) {
+ if (security == null && foregroundServices == null) {
+ Spacer(Modifier.weight(1f))
+ }
+
+ security?.let { SecurityButton(it, Modifier.weight(1f)) }
+ foregroundServices?.let { ForegroundServicesButton(it) }
+ userSwitcher?.let { IconButton(it) }
+ IconButton(viewModel.settings)
+ viewModel.power?.let { IconButton(it) }
+ }
+ }
+}
+
+/** The security button. */
+@Composable
+private fun SecurityButton(
+ model: FooterActionsSecurityButtonViewModel,
+ modifier: Modifier = Modifier,
+) {
+ val onClick: ((Expandable) -> Unit)? =
+ model.onClick?.let { onClick ->
+ val context = LocalContext.current
+ { expandable -> onClick(context, expandable) }
+ }
+
+ TextButton(
+ model.icon,
+ model.text,
+ showNewDot = false,
+ onClick = onClick,
+ modifier,
+ )
+}
+
+/** The foreground services button. */
+@Composable
+private fun RowScope.ForegroundServicesButton(
+ model: FooterActionsForegroundServicesButtonViewModel,
+) {
+ if (model.displayText) {
+ TextButton(
+ Icon.Resource(R.drawable.ic_info_outline, contentDescription = null),
+ model.text,
+ showNewDot = model.hasNewChanges,
+ onClick = model.onClick,
+ Modifier.weight(1f),
+ )
+ } else {
+ NumberButton(
+ model.foregroundServicesCount,
+ showNewDot = model.hasNewChanges,
+ onClick = model.onClick,
+ )
+ }
+}
+
+/** A button with an icon. */
+@Composable
+private fun IconButton(
+ model: FooterActionsButtonViewModel,
+ modifier: Modifier = Modifier,
+) {
+ Expandable(
+ color = colorAttr(model.backgroundColor),
+ shape = CircleShape,
+ onClick = model.onClick,
+ modifier = modifier,
+ ) {
+ val tint = model.iconTint?.let { Color(it) } ?: Color.Unspecified
+ Icon(
+ model.icon,
+ tint = tint,
+ modifier = Modifier.size(20.dp),
+ )
+ }
+}
+
+/** A button with a number an an optional dot (to indicate new changes). */
+@Composable
+private fun NumberButton(
+ number: Int,
+ showNewDot: Boolean,
+ onClick: (Expandable) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ // By default Expandable will show a ripple above its content when clicked, and clip the content
+ // with the shape of the expandable. In this case we also want to show a "new changes dot"
+ // outside of the shape, so we can't clip. To work around that we can pass our own interaction
+ // source and draw the ripple indication ourselves above the text but below the "new changes
+ // dot".
+ val interactionSource = remember { MutableInteractionSource() }
+
+ Expandable(
+ color = colorAttr(R.attr.offStateColor),
+ shape = CircleShape,
+ onClick = onClick,
+ interactionSource = interactionSource,
+ modifier = modifier,
+ ) {
+ Box(Modifier.size(40.dp)) {
+ Box(
+ Modifier.fillMaxSize()
+ .clip(CircleShape)
+ .indication(
+ interactionSource,
+ LocalIndication.current,
+ )
+ ) {
+ Text(
+ number.toString(),
+ modifier = Modifier.align(Alignment.Center),
+ style = MaterialTheme.typography.bodyLarge,
+ color = LocalAndroidColorScheme.current.textColorPrimary,
+ // TODO(b/242040009): This should only use a standard text style instead and
+ // should not override the text size.
+ fontSize = 18.sp,
+ )
+ }
+
+ if (showNewDot) {
+ NewChangesDot(Modifier.align(Alignment.BottomEnd))
+ }
+ }
+ }
+}
+
+/** A dot that indicates new changes. */
+@Composable
+private fun NewChangesDot(modifier: Modifier = Modifier) {
+ val contentDescription = stringResource(R.string.fgs_dot_content_description)
+ val color = LocalAndroidColorScheme.current.colorAccentTertiary
+
+ Canvas(modifier.size(12.dp).semantics { this.contentDescription = contentDescription }) {
+ drawCircle(color)
+ }
+}
+
+/** A larger button with an icon, some text and an optional dot (to indicate new changes). */
+@Composable
+private fun TextButton(
+ icon: Icon,
+ text: String,
+ showNewDot: Boolean,
+ onClick: ((Expandable) -> Unit)?,
+ modifier: Modifier = Modifier,
+) {
+ Expandable(
+ shape = CircleShape,
+ color = colorAttr(R.attr.underSurfaceColor),
+ contentColor = LocalAndroidColorScheme.current.textColorSecondary,
+ borderStroke = BorderStroke(1.dp, LocalAndroidColorScheme.current.colorBackground),
+ modifier = modifier.padding(horizontal = 4.dp),
+ onClick = onClick,
+ ) {
+ Row(
+ Modifier.padding(horizontal = dimensionResource(R.dimen.qs_footer_padding)),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(icon, Modifier.padding(end = 12.dp).size(20.dp))
+
+ Text(
+ text,
+ Modifier.weight(1f),
+ style = MaterialTheme.typography.bodyMedium,
+ // TODO(b/242040009): Remove this letter spacing. We should only use the M3 text
+ // styles without modifying them.
+ letterSpacing = 0.01.em,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+
+ if (showNewDot) {
+ NewChangesDot(Modifier.padding(start = 8.dp))
+ }
+
+ if (onClick != null) {
+ Icon(
+ painterResource(com.android.internal.R.drawable.ic_chevron_end),
+ contentDescription = null,
+ Modifier.padding(start = 8.dp).size(20.dp),
+ )
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
index 6db3c99..30f8124 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
@@ -237,7 +237,13 @@
return
}
- buttonView.setBackgroundResource(model.background)
+ val backgroundResource =
+ when (model.backgroundColor) {
+ R.attr.offStateColor -> R.drawable.qs_footer_action_circle
+ com.android.internal.R.attr.colorAccent -> R.drawable.qs_footer_action_circle_color
+ else -> error("Unsupported icon background resource ${model.backgroundColor}")
+ }
+ buttonView.setBackgroundResource(backgroundResource)
buttonView.setOnClickListener { model.onClick(Expandable.fromView(buttonView)) }
val icon = model.icon
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
index 8d819da..2670787 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsButtonViewModel.kt
@@ -16,7 +16,8 @@
package com.android.systemui.qs.footer.ui.viewmodel
-import android.annotation.DrawableRes
+import android.annotation.AttrRes
+import android.annotation.ColorInt
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Icon
@@ -27,7 +28,7 @@
data class FooterActionsButtonViewModel(
val id: Int,
val icon: Icon,
- val iconTint: Int?,
- @DrawableRes val background: Int,
+ @ColorInt val iconTint: Int?,
+ @AttrRes val backgroundColor: Int,
val onClick: (Expandable) -> Unit,
)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
index dee6fad..fbf32b3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt
@@ -18,6 +18,7 @@
import android.content.Context
import android.util.Log
+import android.view.ContextThemeWrapper
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
@@ -49,12 +50,15 @@
/** A ViewModel for the footer actions. */
class FooterActionsViewModel(
- @Application private val context: Context,
+ @Application appContext: Context,
private val footerActionsInteractor: FooterActionsInteractor,
private val falsingManager: FalsingManager,
private val globalActionsDialogLite: GlobalActionsDialogLite,
showPowerButton: Boolean,
) {
+ /** The context themed with the Quick Settings colors. */
+ private val context = ContextThemeWrapper(appContext, R.style.Theme_SystemUI_QuickSettings)
+
/**
* Whether the UI rendering this ViewModel should be visible. Note that even when this is false,
* the UI should still participate to the layout it is included in (i.e. in the View world it
@@ -142,7 +146,7 @@
ContentDescription.Resource(R.string.accessibility_quick_settings_settings)
),
iconTint = null,
- R.drawable.qs_footer_action_circle,
+ backgroundColor = R.attr.offStateColor,
this::onSettingsButtonClicked,
)
@@ -160,7 +164,7 @@
context,
com.android.internal.R.attr.textColorOnAccent,
),
- R.drawable.qs_footer_action_circle_color,
+ backgroundColor = com.android.internal.R.attr.colorAccent,
this::onPowerButtonClicked,
)
} else {
@@ -260,7 +264,7 @@
),
),
iconTint = null,
- background = R.drawable.qs_footer_action_circle,
+ backgroundColor = R.attr.offStateColor,
onClick = this::onUserSwitcherClicked,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
index 01411c9..0b9fbd9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt
@@ -84,7 +84,7 @@
ContentDescription.Resource(R.string.accessibility_quick_settings_settings)
)
)
- assertThat(settings.background).isEqualTo(R.drawable.qs_footer_action_circle)
+ assertThat(settings.backgroundColor).isEqualTo(R.attr.offStateColor)
assertThat(settings.iconTint).isNull()
}
@@ -105,7 +105,7 @@
ContentDescription.Resource(R.string.accessibility_quick_settings_power_menu)
)
)
- assertThat(power.background).isEqualTo(R.drawable.qs_footer_action_circle_color)
+ assertThat(power.backgroundColor).isEqualTo(com.android.internal.R.attr.colorAccent)
assertThat(power.iconTint)
.isEqualTo(
Utils.getColorAttrDefaultColor(
@@ -170,7 +170,7 @@
assertThat(userSwitcher).isNotNull()
assertThat(userSwitcher!!.icon)
.isEqualTo(Icon.Loaded(picture, ContentDescription.Loaded("Signed in as foo")))
- assertThat(userSwitcher.background).isEqualTo(R.drawable.qs_footer_action_circle)
+ assertThat(userSwitcher.backgroundColor).isEqualTo(R.attr.offStateColor)
// Change the current user name.
userSwitcherControllerWrapper.currentUserName = "bar"