diff options
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,  |