summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt59
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt13
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt1
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt199
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt1
6 files changed, 246 insertions, 29 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
index 1f4f9f98c5b2..7701b9087e23 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
@@ -31,6 +31,7 @@ import androidx.compose.foundation.LocalOverscrollFactory
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.clipScrollableContainer
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement.spacedBy
@@ -49,7 +50,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
-import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
@@ -101,7 +101,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.customActions
-import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.text.style.TextAlign
@@ -138,7 +137,6 @@ import com.android.systemui.qs.panels.ui.compose.selection.ResizingState
import com.android.systemui.qs.panels.ui.compose.selection.ResizingState.ResizeOperation
import com.android.systemui.qs.panels.ui.compose.selection.ResizingState.ResizeOperation.FinalResizeOperation
import com.android.systemui.qs.panels.ui.compose.selection.ResizingState.ResizeOperation.TemporaryResizeOperation
-import com.android.systemui.qs.panels.ui.compose.selection.clearSelectionTile
import com.android.systemui.qs.panels.ui.compose.selection.rememberResizingState
import com.android.systemui.qs.panels.ui.compose.selection.rememberSelectionState
import com.android.systemui.qs.panels.ui.compose.selection.selectableTile
@@ -190,6 +188,7 @@ fun DefaultEditTileGrid(
columns: Int,
largeTilesSpan: Int,
modifier: Modifier,
+ onAddTile: (TileSpec) -> Unit,
onRemoveTile: (TileSpec) -> Unit,
onSetTiles: (List<TileSpec>) -> Unit,
onResize: (TileSpec, toIcon: Boolean) -> Unit,
@@ -230,20 +229,26 @@ fun DefaultEditTileGrid(
modifier
.fillMaxSize()
// Apply top padding before the scroll so the scrollable doesn't show under
- // the
- // top bar
+ // the top bar
.padding(top = innerPadding.calculateTopPadding())
.clipScrollableContainer(Orientation.Vertical)
.verticalScroll(scrollState),
) {
AnimatedContent(
- targetState = listState.dragInProgress,
- modifier = Modifier.wrapContentSize(),
+ targetState = listState.dragInProgress || selectionState.selected,
label = "QSEditHeader",
- ) { dragIsInProgress ->
- EditGridHeader(Modifier.dragAndDropRemoveZone(listState, onRemoveTile)) {
- if (dragIsInProgress) {
- RemoveTileTarget()
+ ) { showRemoveTarget ->
+ EditGridHeader(
+ Modifier.dragAndDropRemoveZone(listState, onRemoveTile)
+ .padding(bottom = 26.dp)
+ ) {
+ if (showRemoveTarget) {
+ RemoveTileTarget {
+ selectionState.selection?.let {
+ selectionState.unSelect()
+ onRemoveTile(it.tileSpec)
+ }
+ }
} else {
Text(text = stringResource(id = R.string.drag_to_rearrange_tiles))
}
@@ -283,7 +288,13 @@ fun DefaultEditTileGrid(
Text(text = stringResource(id = R.string.drag_to_add_tiles))
}
- AvailableTileGrid(otherTiles, selectionState, columns, listState)
+ AvailableTileGrid(
+ otherTiles,
+ selectionState,
+ columns,
+ onAddTile,
+ listState,
+ )
}
}
}
@@ -347,22 +358,18 @@ private fun EditGridHeader(
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.colorScheme.onBackground.copy(alpha = .5f)
) {
- Box(
- contentAlignment = Alignment.Center,
- modifier = modifier.fillMaxWidth().wrapContentHeight(),
- ) {
- content()
- }
+ Box(contentAlignment = Alignment.Center, modifier = modifier.fillMaxWidth()) { content() }
}
}
@Composable
-private fun RemoveTileTarget() {
+private fun RemoveTileTarget(onClick: () -> Unit) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = tileHorizontalArrangement(),
modifier =
Modifier.fillMaxHeight()
+ .clickable(onClick = onClick)
.border(1.dp, LocalContentColor.current, shape = CircleShape)
.padding(10.dp),
) {
@@ -441,6 +448,7 @@ private fun AvailableTileGrid(
tiles: List<SizedTile<EditTileViewModel>>,
selectionState: MutableSelectionState,
columns: Int,
+ onAddTile: (TileSpec) -> Unit,
dragAndDropState: DragAndDropState,
) {
// Available tiles aren't visible during drag and drop, so the row/col isn't needed
@@ -478,6 +486,7 @@ private fun AvailableTileGrid(
index = index,
dragAndDropState = dragAndDropState,
selectionState = selectionState,
+ onAddTile = onAddTile,
modifier = Modifier.weight(1f).fillMaxHeight(),
)
}
@@ -682,11 +691,16 @@ private fun AvailableTileGridCell(
index: Int,
dragAndDropState: DragAndDropState,
selectionState: MutableSelectionState,
+ onAddTile: (TileSpec) -> Unit,
modifier: Modifier = Modifier,
) {
val onClickActionName = stringResource(id = R.string.accessibility_qs_edit_tile_add_action)
val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1)
val colors = EditModeTileDefaults.editTileColors()
+ val onClick = {
+ onAddTile(cell.tile.tileSpec)
+ selectionState.select(cell.tile.tileSpec, manual = false)
+ }
// Displays the tile as an icon tile with the label underneath
Column(
@@ -697,11 +711,8 @@ private fun AvailableTileGridCell(
Box(
Modifier.fillMaxWidth()
.height(TileHeight)
- .clearSelectionTile(selectionState)
- .semantics(mergeDescendants = true) {
- onClick(onClickActionName) { false }
- this.stateDescription = stateDescription
- }
+ .clickable(onClick = onClick, onClickLabel = onClickActionName)
+ .semantics(mergeDescendants = true) { this.stateDescription = stateDescription }
.dragAndDropTileSource(
SizedTileImpl(cell.tile, cell.width),
dragAndDropState,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
index cc4c3af1dc63..1c540eed8aa0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
@@ -42,6 +42,7 @@ import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
+import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor.Companion.POSITION_AT_END
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.shared.ui.ElementKeys.toElementKey
import com.android.systemui.res.R
@@ -155,6 +156,7 @@ constructor(
otherTiles = otherTiles,
columns = columns,
modifier = modifier,
+ onAddTile = { onAddTile(it, POSITION_AT_END) },
onRemoveTile = onRemoveTile,
onSetTiles = onSetTiles,
onResize = iconTilesViewModel::resize,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt
index c6c6dcaa896c..26dfc7224ff9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt
@@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import com.android.systemui.qs.pipeline.shared.TileSpec
@@ -39,17 +40,19 @@ data class Selection(val tileSpec: TileSpec, val manual: Boolean)
/** Holds the state of the current selection. */
class MutableSelectionState {
- private var _selection = mutableStateOf<Selection?>(null)
-
/** The [Selection] if a tile is selected, null if not. */
- val selection by _selection
+ var selection by mutableStateOf<Selection?>(null)
+ private set
+
+ val selected: Boolean
+ get() = selection != null
fun select(tileSpec: TileSpec, manual: Boolean) {
- _selection.value = Selection(tileSpec, manual)
+ selection = Selection(tileSpec, manual)
}
fun unSelect() {
- _selection.value = null
+ selection = null
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
index fc720b836f72..26cf4a261289 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
@@ -68,6 +68,7 @@ class DragAndDropTest : SysuiTestCase() {
columns = 4,
largeTilesSpan = 4,
modifier = Modifier.fillMaxSize(),
+ onAddTile = {},
onRemoveTile = {},
onSetTiles = onSetTiles,
onResize = { _, _ -> },
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt
new file mode 100644
index 000000000000..4e8b0bcd374c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt
@@ -0,0 +1,199 @@
+/*
+ * 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.compose
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.filter
+import androidx.compose.ui.test.hasContentDescription
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onChildren
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.text.AnnotatedString
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.qs.panels.shared.model.SizedTile
+import com.android.systemui.qs.panels.shared.model.SizedTileImpl
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.DefaultEditTileGrid
+import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.TileCategory
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class EditModeTest : SysuiTestCase() {
+ @get:Rule val composeRule = createComposeRule()
+
+ @Composable
+ private fun EditTileGridUnderTest() {
+ var tiles by remember { mutableStateOf(TestEditTiles) }
+ val (currentTiles, otherTiles) = tiles.partition { it.tile.isCurrent }
+ val listState = EditTileListState(currentTiles, columns = 4, largeTilesSpan = 2)
+ DefaultEditTileGrid(
+ listState = listState,
+ otherTiles = otherTiles,
+ columns = 4,
+ largeTilesSpan = 4,
+ modifier = Modifier.fillMaxSize(),
+ onAddTile = { tiles = tiles.add(it) },
+ onRemoveTile = { tiles = tiles.remove(it) },
+ onSetTiles = {},
+ onResize = { _, _ -> },
+ onStopEditing = {},
+ onReset = null,
+ )
+ }
+
+ @Test
+ fun clickAvailableTile_shouldAdd() {
+ composeRule.setContent { EditTileGridUnderTest() }
+ composeRule.waitForIdle()
+
+ composeRule.onNodeWithContentDescription("tileF").performClick() // Tap to add
+ composeRule.waitForIdle()
+
+ composeRule.assertCurrentTilesGridContainsExactly(
+ listOf("tileA", "tileB", "tileC", "tileD_large", "tileE", "tileF")
+ )
+ composeRule.assertAvailableTilesGridContainsExactly(listOf("tileG_large"))
+ }
+
+ @Test
+ fun clickRemoveTarget_shouldRemoveSelection() {
+ composeRule.setContent { EditTileGridUnderTest() }
+ composeRule.waitForIdle()
+
+ composeRule.onNodeWithContentDescription("tileA").performClick() // Selects
+ composeRule.onNodeWithText("Remove").performClick() // Removes
+
+ composeRule.waitForIdle()
+
+ composeRule.assertCurrentTilesGridContainsExactly(
+ listOf("tileB", "tileC", "tileD_large", "tileE")
+ )
+ composeRule.assertAvailableTilesGridContainsExactly(listOf("tileA", "tileF", "tileG_large"))
+ }
+
+ private fun ComposeContentTestRule.assertCurrentTilesGridContainsExactly(specs: List<String>) =
+ assertGridContainsExactly(CURRENT_TILES_GRID_TEST_TAG, specs)
+
+ private fun ComposeContentTestRule.assertAvailableTilesGridContainsExactly(
+ specs: List<String>
+ ) = assertGridContainsExactly(AVAILABLE_TILES_GRID_TEST_TAG, specs)
+
+ private fun ComposeContentTestRule.assertGridContainsExactly(
+ testTag: String,
+ specs: List<String>,
+ ) {
+ onNodeWithTag(testTag)
+ .onChildren()
+ .filter(SemanticsMatcher.keyIsDefined(SemanticsProperties.ContentDescription))
+ .apply {
+ fetchSemanticsNodes().forEachIndexed { index, _ ->
+ get(index).assert(hasContentDescription(specs[index]))
+ }
+ }
+ }
+
+ companion object {
+ private const val CURRENT_TILES_GRID_TEST_TAG = "CurrentTilesGrid"
+ private const val AVAILABLE_TILES_GRID_TEST_TAG = "AvailableTilesGrid"
+
+ private fun List<SizedTile<EditTileViewModel>>.add(
+ spec: TileSpec
+ ): List<SizedTile<EditTileViewModel>> {
+ return map {
+ if (it.tile.tileSpec == spec) {
+ createEditTile(it.tile.tileSpec.spec)
+ } else {
+ it
+ }
+ }
+ }
+
+ private fun List<SizedTile<EditTileViewModel>>.remove(
+ spec: TileSpec
+ ): List<SizedTile<EditTileViewModel>> {
+ return map {
+ if (it.tile.tileSpec == spec) {
+ createEditTile(it.tile.tileSpec.spec, isCurrent = false)
+ } else {
+ it
+ }
+ }
+ }
+
+ private fun createEditTile(
+ tileSpec: String,
+ isCurrent: Boolean = true,
+ ): SizedTile<EditTileViewModel> {
+ return SizedTileImpl(
+ EditTileViewModel(
+ tileSpec = TileSpec.create(tileSpec),
+ icon =
+ Icon.Resource(
+ android.R.drawable.star_on,
+ ContentDescription.Loaded(tileSpec),
+ ),
+ label = AnnotatedString(tileSpec),
+ appName = null,
+ isCurrent = isCurrent,
+ availableEditActions = emptySet(),
+ category = TileCategory.UNKNOWN,
+ ),
+ getWidth(tileSpec),
+ )
+ }
+
+ private fun getWidth(tileSpec: String): Int {
+ return if (tileSpec.endsWith("large")) {
+ 2
+ } else {
+ 1
+ }
+ }
+
+ private val TestEditTiles =
+ listOf(
+ createEditTile("tileA"),
+ createEditTile("tileB"),
+ createEditTile("tileC"),
+ createEditTile("tileD_large"),
+ createEditTile("tileE"),
+ createEditTile("tileF", isCurrent = false),
+ createEditTile("tileG_large", isCurrent = false),
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
index f23553eda3b2..a0be02f1ef7e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
@@ -65,6 +65,7 @@ class ResizingTest : SysuiTestCase() {
columns = 4,
largeTilesSpan = 4,
modifier = Modifier.fillMaxSize(),
+ onAddTile = {},
onRemoveTile = {},
onSetTiles = {},
onResize = onResize,