From 304250829269d92eb7500a102768785a2a3254a1 Mon Sep 17 00:00:00 2001 From: Jordan Demeulenaere Date: Wed, 19 Oct 2022 14:04:52 +0200 Subject: [PATCH] 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 --- core/res/res/values/symbols.xml | 1 + .../systemui/compose/animation/Expandable.kt | 36 +- .../compose/theme/AndroidColorScheme.kt | 12 +- .../android/systemui/compose/theme/Color.kt | 30 ++ .../common/ui/compose/ContentDescription.kt | 30 ++ .../systemui/common/ui/compose/Icon.kt | 47 +++ .../people/ui/compose/PeopleScreen.kt | 1 - .../qs/footer/ui/compose/FooterActions.kt | 361 ++++++++++++++++++ .../ui/binder/FooterActionsViewBinder.kt | 8 +- .../viewmodel/FooterActionsButtonViewModel.kt | 7 +- .../ui/viewmodel/FooterActionsViewModel.kt | 12 +- .../viewmodel/FooterActionsViewModelTest.kt | 6 +- 12 files changed, 528 insertions(+), 23 deletions(-) create mode 100644 packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/Color.kt create mode 100644 packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/ContentDescription.kt create mode 100644 packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt create mode 100644 packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index b9259218c2d30..d6fdc55c8bb88 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2331,6 +2331,7 @@ + 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 18534f43ae871..d31ca511130b9 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.BorderStroke 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.ContentDrawScope 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 @@ fun Expandable( 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 @@ fun Expandable( 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 @@ fun Expandable( 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 @@ fun Expandable( 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 @@ fun Expandable( 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 b8639e64e0020..caa7e5fae63f8 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 @@ class AndroidColorScheme internal constructor(context: Context) { 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 0000000000000..de47ccedb3ee9 --- /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 0000000000000..4a5ad6554dc67 --- /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 0000000000000..6e8312436d96f --- /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 2bf1937a1c1e8..2aac46e903426 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.R 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 0000000000000..654b723f5cffd --- /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(null) } + var foregroundServices by remember { + mutableStateOf(null) + } + var userSwitcher by remember { mutableStateOf(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 6db3c9941b784..30f81243e8d01 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 @@ object FooterActionsViewBinder { 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 8d819dacba672..2670787909b43 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 @@ import com.android.systemui.common.shared.model.Icon 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 dee6fadbc9cba..fbf32b3b99ea9 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 @@ package com.android.systemui.qs.footer.ui.viewmodel 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 @@ import kotlinx.coroutines.flow.map /** 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 @@ class FooterActionsViewModel( 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 @@ class FooterActionsViewModel( 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 @@ class FooterActionsViewModel( ), ), 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 01411c9e7f04b..0b9fbd919715a 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 @@ class FooterActionsViewModelTest : SysuiTestCase() { 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 @@ class FooterActionsViewModelTest : SysuiTestCase() { 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 @@ class FooterActionsViewModelTest : SysuiTestCase() { 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"