Loading packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +35 −24 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -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, Loading Loading @@ -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)) } Loading Loading @@ -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, ) } } } Loading Loading @@ -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), ) { Loading Loading @@ -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 Loading Loading @@ -478,6 +486,7 @@ private fun AvailableTileGrid( index = index, dragAndDropState = dragAndDropState, selectionState = selectionState, onAddTile = onAddTile, modifier = Modifier.weight(1f).fillMaxHeight(), ) } Loading Loading @@ -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( Loading @@ -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, Loading packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt +2 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -155,6 +156,7 @@ constructor( otherTiles = otherTiles, columns = columns, modifier = modifier, onAddTile = { onAddTile(it, POSITION_AT_END) }, onRemoveTile = onRemoveTile, onSetTiles = onSetTiles, onResize = iconTilesViewModel::resize, Loading packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt +8 −5 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 } } Loading packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt +1 −0 Original line number Diff line number Diff line Loading @@ -68,6 +68,7 @@ class DragAndDropTest : SysuiTestCase() { columns = 4, largeTilesSpan = 4, modifier = Modifier.fillMaxSize(), onAddTile = {}, onRemoveTile = {}, onSetTiles = onSetTiles, onResize = { _, _ -> }, Loading packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt 0 → 100644 +199 −0 Original line number Diff line number Diff line /* * 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), ) } } Loading
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +35 −24 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -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, Loading Loading @@ -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)) } Loading Loading @@ -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, ) } } } Loading Loading @@ -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), ) { Loading Loading @@ -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 Loading Loading @@ -478,6 +486,7 @@ private fun AvailableTileGrid( index = index, dragAndDropState = dragAndDropState, selectionState = selectionState, onAddTile = onAddTile, modifier = Modifier.weight(1f).fillMaxHeight(), ) } Loading Loading @@ -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( Loading @@ -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, Loading
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt +2 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -155,6 +156,7 @@ constructor( otherTiles = otherTiles, columns = columns, modifier = modifier, onAddTile = { onAddTile(it, POSITION_AT_END) }, onRemoveTile = onRemoveTile, onSetTiles = onSetTiles, onResize = iconTilesViewModel::resize, Loading
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt +8 −5 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 } } Loading
packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt +1 −0 Original line number Diff line number Diff line Loading @@ -68,6 +68,7 @@ class DragAndDropTest : SysuiTestCase() { columns = 4, largeTilesSpan = 4, modifier = Modifier.fillMaxSize(), onAddTile = {}, onRemoveTile = {}, onSetTiles = onSetTiles, onResize = { _, _ -> }, Loading
packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt 0 → 100644 +199 −0 Original line number Diff line number Diff line /* * 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), ) } }