diff options
8 files changed, 267 insertions, 174 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModelTest.kt index 3029928f070f..cb13b118fd68 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModelTest.kt @@ -25,6 +25,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.app.iUriGrantsManager import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.ui.viewmodel.iconProvider import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest @@ -32,6 +33,8 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.external.TileData +import com.android.systemui.qs.panels.ui.viewmodel.IconProvider +import com.android.systemui.qs.panels.ui.viewmodel.toIconProvider import com.android.systemui.qs.panels.ui.viewmodel.toUiState import com.android.systemui.qs.tileimpl.QSTileImpl import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon @@ -80,28 +83,32 @@ class TileRequestDialogViewModelTest : SysuiTestCase() { @Test fun uiState_beforeActivation_hasDefaultIcon_andCorrectData() = kosmos.runTest { - val expectedState = - baseResultLegacyState.apply { icon = defaultIcon }.toUiState(mainResources) + val state = baseResultLegacyState.apply { icon = defaultIcon } + + val expectedState = state.toUiState(mainResources) + val expectedIconProvider = state.toIconProvider() with(underTest.uiState) { expect.that(label).isEqualTo(TEST_LABEL) expect.that(secondaryLabel).isEmpty() - expect.that(state).isEqualTo(expectedState.state) + expect.that(this.state).isEqualTo(expectedState.state) expect.that(handlesLongClick).isFalse() expect.that(handlesSecondaryClick).isFalse() - expect.that(icon).isEqualTo(defaultIcon) expect.that(sideDrawable).isNull() expect.that(accessibilityUiState).isEqualTo(expectedState.accessibilityUiState) } + + expect.that(underTest.iconProvider).isEqualTo(expectedIconProvider) } @Test fun uiState_afterActivation_hasCorrectIcon_andCorrectData() = kosmos.runTest { - val expectedState = - baseResultLegacyState - .apply { icon = QSTileImpl.DrawableIcon(loadedDrawable) } - .toUiState(mainResources) + val state = + baseResultLegacyState.apply { icon = QSTileImpl.DrawableIcon(loadedDrawable) } + + val expectedState = state.toUiState(mainResources) + val expectedIconProvider = state.toIconProvider() underTest.activateIn(testScope) runCurrent() @@ -109,13 +116,13 @@ class TileRequestDialogViewModelTest : SysuiTestCase() { with(underTest.uiState) { expect.that(label).isEqualTo(TEST_LABEL) expect.that(secondaryLabel).isEmpty() - expect.that(state).isEqualTo(expectedState.state) + expect.that(this.state).isEqualTo(expectedState.state) expect.that(handlesLongClick).isFalse() expect.that(handlesSecondaryClick).isFalse() - expect.that(icon).isEqualTo(QSTileImpl.DrawableIcon(loadedDrawable)) expect.that(sideDrawable).isNull() expect.that(accessibilityUiState).isEqualTo(expectedState.accessibilityUiState) } + expect.that(underTest.iconProvider).isEqualTo(expectedIconProvider) } @Test @@ -135,7 +142,7 @@ class TileRequestDialogViewModelTest : SysuiTestCase() { underTest.activateIn(testScope) runCurrent() - assertThat(underTest.uiState.icon).isEqualTo(defaultIcon) + assertThat(underTest.iconProvider).isEqualTo(IconProvider.ConstantIcon(defaultIcon)) } companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/IconProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/IconProviderTest.kt new file mode 100644 index 000000000000..7257a89c214b --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/IconProviderTest.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2025 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.panels.ui.viewmodel + +import android.graphics.drawable.TestStubDrawable +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.qs.tileimpl.QSTileImpl +import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon +import com.android.systemui.res.R +import com.google.common.truth.Truth.assertThat +import java.util.function.Supplier +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class IconProviderTest : SysuiTestCase() { + + @Test + fun iconAndSupplier_prefersIcon() { + val state = + QSTile.State().apply { + icon = ResourceIcon.get(R.drawable.android) + iconSupplier = Supplier { QSTileImpl.DrawableIcon(TestStubDrawable()) } + } + val iconProvider = state.toIconProvider() + + assertThat(iconProvider).isEqualTo(IconProvider.ConstantIcon(state.icon)) + } + + @Test + fun iconOnly_hasIcon() { + val state = QSTile.State().apply { icon = ResourceIcon.get(R.drawable.android) } + val iconProvider = state.toIconProvider() + + assertThat(iconProvider).isEqualTo(IconProvider.ConstantIcon(state.icon)) + } + + @Test + fun supplierOnly_hasIcon() { + val state = + QSTile.State().apply { + iconSupplier = Supplier { ResourceIcon.get(R.drawable.android) } + } + val iconProvider = state.toIconProvider() + + assertThat(iconProvider).isEqualTo(IconProvider.IconSupplier(state.iconSupplier)) + } + + @Test + fun noIconOrSupplier_iconNull() { + val state = QSTile.State() + val iconProvider = state.toIconProvider() + + assertThat(iconProvider).isEqualTo(IconProvider.Empty) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiStateTest.kt index 9c8e3225f3a4..b144f0678471 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiStateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiStateTest.kt @@ -18,7 +18,6 @@ package com.android.systemui.qs.panels.ui.viewmodel import android.content.res.Resources import android.content.res.mainResources -import android.graphics.drawable.TestStubDrawable import android.service.quicksettings.Tile import android.widget.Button import android.widget.Switch @@ -27,12 +26,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.plugins.qs.QSTile -import com.android.systemui.qs.tileimpl.QSTileImpl -import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon import com.android.systemui.res.R import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat -import java.util.function.Supplier import org.junit.Test import org.junit.runner.RunWith @@ -267,45 +263,6 @@ class TileUiStateTest : SysuiTestCase() { .contains(resources.getString(R.string.tile_unavailable)) } - @Test - fun iconAndSupplier_prefersIcon() { - val state = - QSTile.State().apply { - icon = ResourceIcon.get(R.drawable.android) - iconSupplier = Supplier { QSTileImpl.DrawableIcon(TestStubDrawable()) } - } - val uiState = state.toUiState() - - assertThat(uiState.icon).isEqualTo(state.icon) - } - - @Test - fun iconOnly_hasIcon() { - val state = QSTile.State().apply { icon = ResourceIcon.get(R.drawable.android) } - val uiState = state.toUiState() - - assertThat(uiState.icon).isEqualTo(state.icon) - } - - @Test - fun supplierOnly_hasIcon() { - val state = - QSTile.State().apply { - iconSupplier = Supplier { ResourceIcon.get(R.drawable.android) } - } - val uiState = state.toUiState() - - assertThat(uiState.icon).isEqualTo(state.iconSupplier.get()) - } - - @Test - fun noIconOrSupplier_iconNull() { - val state = QSTile.State() - val uiState = state.toUiState() - - assertThat(uiState.icon).isNull() - } - private fun QSTile.State.toUiState() = toUiState(resources) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/ui/dialog/TileRequestDialogComposeDelegate.kt b/packages/SystemUI/src/com/android/systemui/qs/external/ui/dialog/TileRequestDialogComposeDelegate.kt index 446be9b9ebcb..59844c7ae664 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/ui/dialog/TileRequestDialogComposeDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/external/ui/dialog/TileRequestDialogComposeDelegate.kt @@ -85,6 +85,7 @@ constructor( LargeStaticTile( uiState = viewModel.uiState, + iconProvider = viewModel.iconProvider, modifier = Modifier.width( dimensionResource( diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModel.kt index c756adc07ba4..dd281ccc9c50 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModel.kt @@ -26,6 +26,7 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.external.TileData +import com.android.systemui.qs.panels.ui.viewmodel.toIconProvider import com.android.systemui.qs.panels.ui.viewmodel.toUiState import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon @@ -58,6 +59,8 @@ constructor( val uiState by derivedStateOf { state.toUiState(dialogContext.resources) } + val iconProvider by derivedStateOf { state.toIconProvider() } + override suspend fun onActivated(): Nothing { withContext(backgroundDispatcher) { tileData.icon diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt index a56fabcc7dc3..bf63c3858542 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt @@ -20,6 +20,7 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid import android.content.Context import android.content.res.Resources +import android.os.Trace import android.service.quicksettings.Tile.STATE_ACTIVE import android.service.quicksettings.Tile.STATE_INACTIVE import androidx.compose.animation.animateColorAsState @@ -47,8 +48,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState @@ -59,7 +60,7 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role @@ -69,6 +70,7 @@ import androidx.compose.ui.semantics.toggleableState import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.trace import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.animation.Expandable import com.android.compose.animation.bounceable @@ -80,7 +82,6 @@ import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.haptics.msdl.qs.TileHapticsViewModel import com.android.systemui.haptics.msdl.qs.TileHapticsViewModelFactoryProvider import com.android.systemui.lifecycle.rememberViewModel -import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.flags.QsDetailedView import com.android.systemui.qs.panels.ui.compose.BounceableInfo import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius @@ -88,9 +89,12 @@ import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TileHeight import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TileStartPadding import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.longPressLabel +import com.android.systemui.qs.panels.ui.viewmodel.AccessibilityUiState import com.android.systemui.qs.panels.ui.viewmodel.DetailsViewModel +import com.android.systemui.qs.panels.ui.viewmodel.IconProvider import com.android.systemui.qs.panels.ui.viewmodel.TileUiState import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel +import com.android.systemui.qs.panels.ui.viewmodel.toIconProvider import com.android.systemui.qs.panels.ui.viewmodel.toUiState import com.android.systemui.qs.tileimpl.QSTileImpl import com.android.systemui.qs.ui.compose.borderOnFocus @@ -119,6 +123,9 @@ fun TileLazyGrid( ) } +private val TileViewModel.traceName + get() = spec.toString().takeLast(Trace.MAX_SECTION_NAME_LEN) + @Composable fun Tile( tile: TileViewModel, @@ -130,105 +137,114 @@ fun Tile( modifier: Modifier = Modifier, detailsViewModel: DetailsViewModel?, ) { - val currentBounceableInfo by rememberUpdatedState(bounceableInfo) - val resources = resources() - - /* - * Use produce state because [QSTile.State] doesn't have well defined equals (due to - * inheritance). This way, even if tile.state changes, uiState may not change and lead to - * recomposition. - */ - val uiState by - produceState(tile.currentState.toUiState(resources), tile, resources) { - tile.state.collect { value = it.toUiState(resources) } - } + trace(tile.traceName) { + val currentBounceableInfo by rememberUpdatedState(bounceableInfo) + val resources = resources() + + /* + * Use produce state because [QSTile.State] doesn't have well defined equals (due to + * inheritance). This way, even if tile.state changes, uiState may not change and lead to + * recomposition. + */ + val uiState by + produceState(tile.currentState.toUiState(resources), tile, resources) { + tile.state.collect { value = it.toUiState(resources) } + } - val colors = TileDefaults.getColorForState(uiState, iconOnly) - val hapticsViewModel: TileHapticsViewModel? = - rememberViewModel(traceName = "TileHapticsViewModel") { - tileHapticsViewModelFactoryProvider.getHapticsViewModelFactory()?.create(tile) - } + val icon by + produceState(tile.currentState.toIconProvider(), tile) { + tile.state.collect { value = it.toIconProvider() } + } - // TODO(b/361789146): Draw the shapes instead of clipping - val tileShape by TileDefaults.animateTileShapeAsState(uiState.state) - val animatedColor by animateColorAsState(colors.background, label = "QSTileBackgroundColor") + val colors = TileDefaults.getColorForState(uiState, iconOnly) + val hapticsViewModel: TileHapticsViewModel? = + rememberViewModel(traceName = "TileHapticsViewModel") { + tileHapticsViewModelFactoryProvider.getHapticsViewModelFactory()?.create(tile) + } - TileExpandable( - color = { animatedColor }, - shape = tileShape, - squishiness = squishiness, - hapticsViewModel = hapticsViewModel, - modifier = - modifier - .borderOnFocus(color = MaterialTheme.colorScheme.secondary, tileShape.topEnd) - .fillMaxWidth() - .bounceable( - bounceable = currentBounceableInfo.bounceable, - previousBounceable = currentBounceableInfo.previousTile, - nextBounceable = currentBounceableInfo.nextTile, - orientation = Orientation.Horizontal, - bounceEnd = currentBounceableInfo.bounceEnd, - ), - ) { expandable -> - val longClick: (() -> Unit)? = - { - hapticsViewModel?.setTileInteractionState( - TileHapticsViewModel.TileInteractionState.LONG_CLICKED + // TODO(b/361789146): Draw the shapes instead of clipping + val tileShape by TileDefaults.animateTileShapeAsState(uiState.state) + val animatedColor by animateColorAsState(colors.background, label = "QSTileBackgroundColor") + + TileExpandable( + color = { animatedColor }, + shape = tileShape, + squishiness = squishiness, + hapticsViewModel = hapticsViewModel, + modifier = + modifier + .borderOnFocus(color = MaterialTheme.colorScheme.secondary, tileShape.topEnd) + .fillMaxWidth() + .bounceable( + bounceable = currentBounceableInfo.bounceable, + previousBounceable = currentBounceableInfo.previousTile, + nextBounceable = currentBounceableInfo.nextTile, + orientation = Orientation.Horizontal, + bounceEnd = currentBounceableInfo.bounceEnd, + ), + ) { expandable -> + val longClick: (() -> Unit)? = + { + hapticsViewModel?.setTileInteractionState( + TileHapticsViewModel.TileInteractionState.LONG_CLICKED + ) + tile.onLongClick(expandable) + } + .takeIf { uiState.handlesLongClick } + TileContainer( + onClick = { + var hasDetails = false + if (QsDetailedView.isEnabled) { + hasDetails = detailsViewModel?.onTileClicked(tile.spec) == true + } + if (!hasDetails) { + // For those tile's who doesn't have a detailed view, process with their + // `onClick` behavior. + tile.onClick(expandable) + hapticsViewModel?.setTileInteractionState( + TileHapticsViewModel.TileInteractionState.CLICKED + ) + if (uiState.accessibilityUiState.toggleableState != null) { + coroutineScope.launch { + currentBounceableInfo.bounceable.animateBounce() + } + } + } + }, + onLongClick = longClick, + accessibilityUiState = uiState.accessibilityUiState, + iconOnly = iconOnly, + ) { + val iconProvider: Context.() -> Icon = { getTileIcon(icon = icon) } + if (iconOnly) { + SmallTileContent( + iconProvider = iconProvider, + color = colors.icon, + modifier = Modifier.align(Alignment.Center), ) - tile.onLongClick(expandable) - } - .takeIf { uiState.handlesLongClick } - TileContainer( - onClick = { - var hasDetails = false - if (QsDetailedView.isEnabled) { - hasDetails = detailsViewModel?.onTileClicked(tile.spec) == true - } - if (!hasDetails) { - // For those tile's who doesn't have a detailed view, process with their - // `onClick` behavior. - tile.onClick(expandable) - hapticsViewModel?.setTileInteractionState( - TileHapticsViewModel.TileInteractionState.CLICKED + } else { + val iconShape by TileDefaults.animateIconShapeAsState(uiState.state) + val secondaryClick: (() -> Unit)? = + { + hapticsViewModel?.setTileInteractionState( + TileHapticsViewModel.TileInteractionState.CLICKED + ) + tile.onSecondaryClick() + } + .takeIf { uiState.handlesSecondaryClick } + LargeTileContent( + label = uiState.label, + secondaryLabel = uiState.secondaryLabel, + iconProvider = iconProvider, + sideDrawable = uiState.sideDrawable, + colors = colors, + iconShape = iconShape, + toggleClick = secondaryClick, + onLongClick = longClick, + accessibilityUiState = uiState.accessibilityUiState, + squishiness = squishiness, ) - if (uiState.accessibilityUiState.toggleableState != null) { - coroutineScope.launch { currentBounceableInfo.bounceable.animateBounce() } - } } - }, - onLongClick = longClick, - uiState = uiState, - iconOnly = iconOnly, - ) { - val iconProvider: Context.() -> Icon = { getTileIcon(icon = uiState.icon) } - if (iconOnly) { - SmallTileContent( - iconProvider = iconProvider, - color = colors.icon, - modifier = Modifier.align(Alignment.Center), - ) - } else { - val iconShape by TileDefaults.animateIconShapeAsState(uiState.state) - val secondaryClick: (() -> Unit)? = - { - hapticsViewModel?.setTileInteractionState( - TileHapticsViewModel.TileInteractionState.CLICKED - ) - tile.onSecondaryClick() - } - .takeIf { uiState.handlesSecondaryClick } - LargeTileContent( - label = uiState.label, - secondaryLabel = uiState.secondaryLabel, - iconProvider = iconProvider, - sideDrawable = uiState.sideDrawable, - colors = colors, - iconShape = iconShape, - toggleClick = secondaryClick, - onLongClick = longClick, - accessibilityUiState = uiState.accessibilityUiState, - squishiness = squishiness, - ) } } } @@ -257,7 +273,7 @@ private fun TileExpandable( fun TileContainer( onClick: () -> Unit, onLongClick: (() -> Unit)?, - uiState: TileUiState, + accessibilityUiState: AccessibilityUiState, iconOnly: Boolean, content: @Composable BoxScope.() -> Unit, ) { @@ -268,7 +284,7 @@ fun TileContainer( .tileCombinedClickable( onClick = onClick, onLongClick = onLongClick, - uiState = uiState, + accessibilityUiState = accessibilityUiState, iconOnly = iconOnly, ) .sysuiResTag(if (iconOnly) TEST_TAG_SMALL else TEST_TAG_LARGE) @@ -278,7 +294,11 @@ fun TileContainer( } @Composable -fun LargeStaticTile(uiState: TileUiState, modifier: Modifier = Modifier) { +fun LargeStaticTile( + uiState: TileUiState, + iconProvider: IconProvider, + modifier: Modifier = Modifier, +) { val colors = TileDefaults.getColorForState(uiState = uiState, iconOnly = false) Box( @@ -291,7 +311,7 @@ fun LargeStaticTile(uiState: TileUiState, modifier: Modifier = Modifier) { LargeTileContent( label = uiState.label, secondaryLabel = "", - iconProvider = { getTileIcon(icon = uiState.icon) }, + iconProvider = { getTileIcon(icon = iconProvider) }, sideDrawable = null, colors = colors, squishiness = { 1f }, @@ -299,8 +319,8 @@ fun LargeStaticTile(uiState: TileUiState, modifier: Modifier = Modifier) { } } -private fun Context.getTileIcon(icon: QSTile.Icon?): Icon { - return icon?.let { +private fun Context.getTileIcon(icon: IconProvider): Icon { + return icon.icon?.let { if (it is QSTileImpl.ResourceIcon) { Icon.Resource(it.resId, null) } else { @@ -321,28 +341,26 @@ fun Modifier.largeTilePadding(): Modifier { fun Modifier.tileCombinedClickable( onClick: () -> Unit, onLongClick: (() -> Unit)?, - uiState: TileUiState, + accessibilityUiState: AccessibilityUiState, iconOnly: Boolean, ): Modifier { val longPressLabel = longPressLabel() return combinedClickable( onClick = onClick, onLongClick = onLongClick, - onClickLabel = uiState.accessibilityUiState.clickLabel, + onClickLabel = accessibilityUiState.clickLabel, onLongClickLabel = longPressLabel, hapticFeedbackEnabled = !Flags.msdlFeedback(), ) .semantics { - role = uiState.accessibilityUiState.accessibilityRole - if (uiState.accessibilityUiState.accessibilityRole == Role.Switch) { - uiState.accessibilityUiState.toggleableState?.let { toggleableState = it } + role = accessibilityUiState.accessibilityRole + if (accessibilityUiState.accessibilityRole == Role.Switch) { + accessibilityUiState.toggleableState?.let { toggleableState = it } } - stateDescription = uiState.accessibilityUiState.stateDescription + stateDescription = accessibilityUiState.stateDescription } .thenIf(iconOnly) { - Modifier.semantics { - contentDescription = uiState.accessibilityUiState.contentDescription - } + Modifier.semantics { contentDescription = accessibilityUiState.contentDescription } } } @@ -474,14 +492,15 @@ private object TileDefaults { label = label, ) - val corner = remember { - object : CornerSize { - override fun toPx(shapeSize: Size, density: Density): Float { - return with(density) { animatedCornerRadius.toPx() } + return remember { + val corner = + object : CornerSize { + override fun toPx(shapeSize: Size, density: Density): Float { + return with(density) { animatedCornerRadius.toPx() } + } } - } + mutableStateOf(RoundedCornerShape(corner)) } - return remember { derivedStateOf { RoundedCornerShape(corner) } } } } @@ -493,5 +512,5 @@ private object TileDefaults { @ReadOnlyComposable private fun resources(): Resources { LocalConfiguration.current - return LocalContext.current.resources + return LocalResources.current } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModel.kt index 03f0297e0d54..3287443f0405 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.panels.ui.viewmodel +import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import com.android.systemui.dagger.SysUISingleton @@ -25,6 +26,7 @@ import com.android.systemui.qs.pipeline.shared.TileSpec import javax.inject.Inject @SysUISingleton +@Stable class DetailsViewModel @Inject constructor(val currentTilesInteractor: CurrentTilesInteractor) { /** diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt index 19e542e6a21e..15e71c88bea1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt @@ -22,12 +22,18 @@ import android.service.quicksettings.Tile import android.text.TextUtils import android.widget.Switch import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable import androidx.compose.ui.semantics.Role import androidx.compose.ui.state.ToggleableState import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.tileimpl.SubtitleArrayMapping import com.android.systemui.res.R +import java.util.function.Supplier +/** + * Ui State for the tiles. It doesn't contain the icon to be able to invalidate the icon part + * separately. For the icon, use [IconProvider]. + */ @Immutable data class TileUiState( val label: String, @@ -35,7 +41,6 @@ data class TileUiState( val state: Int, val handlesLongClick: Boolean, val handlesSecondaryClick: Boolean, - val icon: QSTile.Icon?, val sideDrawable: Drawable?, val accessibilityUiState: AccessibilityUiState, ) @@ -90,7 +95,6 @@ fun QSTile.State.toUiState(resources: Resources): TileUiState { state = if (disabledByPolicy) Tile.STATE_UNAVAILABLE else state, handlesLongClick = handlesLongClick, handlesSecondaryClick = handlesSecondaryClick, - icon = icon ?: iconSupplier?.get(), sideDrawable = sideViewCustomDrawable, AccessibilityUiState( contentDescription?.toString() ?: "", @@ -104,6 +108,14 @@ fun QSTile.State.toUiState(resources: Resources): TileUiState { ) } +fun QSTile.State.toIconProvider(): IconProvider { + return when { + icon != null -> IconProvider.ConstantIcon(icon) + iconSupplier != null -> IconProvider.IconSupplier(iconSupplier) + else -> IconProvider.Empty + } +} + private fun QSTile.State.getStateText(resources: Resources): CharSequence { val arrayResId = SubtitleArrayMapping.getSubtitleId(spec) val array = resources.getStringArray(arrayResId) @@ -114,3 +126,21 @@ private fun getUnavailableText(spec: String?, resources: Resources): String { val arrayResId = SubtitleArrayMapping.getSubtitleId(spec) return resources.getStringArray(arrayResId)[Tile.STATE_UNAVAILABLE] } + +@Stable +sealed interface IconProvider { + + val icon: QSTile.Icon? + + data class ConstantIcon(override val icon: QSTile.Icon) : IconProvider + + data class IconSupplier(val supplier: Supplier<QSTile.Icon?>) : IconProvider { + override val icon: QSTile.Icon? + get() = supplier.get() + } + + data object Empty : IconProvider { + override val icon: QSTile.Icon? + get() = null + } +} |