summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Fabian Kozynski <kozynski@google.com> 2025-03-05 10:33:20 -0800
committer Android (Google) Code Review <android-gerrit@google.com> 2025-03-05 10:33:20 -0800
commitdd4c87c9481bd20491a1d3b6431fbfb59d0a4e8f (patch)
tree44d745a227c7298831f3f295bbed5acb3cab45b9
parent23f87d67a93ca31ff9d802b606463180fa278cbf (diff)
parent2a6ef81832129e9163a89ece0bb68cd4105f47f0 (diff)
Merge "Convert to icon provider and separate from TileUiState" into main
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModelTest.kt29
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/IconProviderTest.kt74
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiStateTest.kt43
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/external/ui/dialog/TileRequestDialogComposeDelegate.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt255
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/TileUiState.kt34
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
+ }
+}