Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5a411676 authored by Olivier St-Onge's avatar Olivier St-Onge
Browse files

Block tile removal when the minimum amount of tiles is reached

This includes dragging and selecting tiles that can't be removed

Flag: com.android.systemui.qs_ui_refactor_compose_fragment
Fixes: 394799195
Test: DragAndDropTest.kt
Test: EditModeTest.kt
Change-Id: I81173c9489cd2f61231fb20ea75d5721f2afad64
parent afcc09c3
Loading
Loading
Loading
Loading
+5 −2
Original line number Diff line number Diff line
@@ -44,6 +44,7 @@ import com.android.systemui.qs.pipeline.shared.TileSpec
/** Holds the [TileSpec] of the tile being moved and receives drag and drop events. */
interface DragAndDropState {
    val draggedCell: SizedTile<EditTileViewModel>?
    val isDraggedCellRemovable: Boolean
    val draggedPosition: Offset
    val dragInProgress: Boolean
    val dragType: DragType?
@@ -76,7 +77,7 @@ enum class DragType {
@Composable
fun Modifier.dragAndDropRemoveZone(
    dragAndDropState: DragAndDropState,
    onDrop: (TileSpec) -> Unit,
    onDrop: (TileSpec, removalEnabled: Boolean) -> Unit,
): Modifier {
    val target =
        remember(dragAndDropState) {
@@ -87,13 +88,15 @@ fun Modifier.dragAndDropRemoveZone(

                override fun onDrop(event: DragAndDropEvent): Boolean {
                    return dragAndDropState.draggedCell?.let {
                        onDrop(it.tile.tileSpec)
                        onDrop(it.tile.tileSpec, dragAndDropState.isDraggedCellRemovable)
                        dragAndDropState.onDrop()
                        true
                    } ?: false
                }

                override fun onEntered(event: DragAndDropEvent) {
                    if (!dragAndDropState.isDraggedCellRemovable) return

                    dragAndDropState.movedOutOfBounds()
                }
            }
+11 −0
Original line number Diff line number Diff line
@@ -60,6 +60,11 @@ class EditTileListState(
    override var dragType by mutableStateOf<DragType?>(null)
        private set

    // A dragged cell can be removed if it was added in the drag movement OR if it's marked as
    // removable
    override val isDraggedCellRemovable: Boolean
        get() = dragType == DragType.Add || draggedCell?.tile?.isRemovable ?: false

    override val dragInProgress: Boolean
        get() = draggedCell != null

@@ -76,6 +81,12 @@ class EditTileListState(
        return _tiles.indexOfFirst { it is TileGridCell && it.tile.tileSpec == tileSpec }
    }

    fun isRemovable(tileSpec: TileSpec): Boolean {
        return _tiles.find {
            it is TileGridCell && it.tile.tileSpec == tileSpec && it.tile.isRemovable
        } != null
    }

    /** Resize the tile corresponding to the [TileSpec] to [toIcon] */
    fun resizeTile(tileSpec: TileSpec, toIcon: Boolean) {
        val fromIndex = indexOf(tileSpec)
+46 −23
Original line number Diff line number Diff line
@@ -152,7 +152,6 @@ import com.android.systemui.qs.panels.ui.model.AvailableTileGridCell
import com.android.systemui.qs.panels.ui.model.GridCell
import com.android.systemui.qs.panels.ui.model.SpacerGridCell
import com.android.systemui.qs.panels.ui.model.TileGridCell
import com.android.systemui.qs.panels.ui.viewmodel.AvailableEditActions
import com.android.systemui.qs.panels.ui.viewmodel.BounceableTileViewModel
import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
import com.android.systemui.qs.pipeline.shared.TileSpec
@@ -272,29 +271,23 @@ fun DefaultEditTileGrid(
                        .padding(top = innerPadding.calculateTopPadding())
                        .clipScrollableContainer(Orientation.Vertical)
                        .verticalScroll(scrollState)
                        .dragAndDropRemoveZone(listState, onRemoveTile),
                        .dragAndDropRemoveZone(listState) { spec, removalEnabled ->
                            if (removalEnabled) {
                                // If removal is enabled, remove the tile
                                onRemoveTile(spec)
                            } else {
                                // Otherwise submit the new tile ordering
                                onSetTiles(listState.tileSpecs())
                                selectionState.select(spec)
                            }
                        },
            ) {
                AnimatedContent(
                    targetState = listState.dragInProgress || selectionState.selected,
                    label = "QSEditHeader",
                    contentAlignment = Alignment.Center,
                CurrentTilesGridHeader(
                    listState,
                    selectionState,
                    onRemoveTile,
                    modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp),
                ) { showRemoveTarget ->
                    EditGridHeader {
                        if (showRemoveTarget) {
                            RemoveTileTarget {
                                selectionState.selection?.let {
                                    selectionState.unSelect()
                                    onRemoveTile(it)
                                }
                            }
                        } else {
                            EditGridCenteredText(
                                text = stringResource(id = R.string.drag_to_rearrange_tiles)
                )
                        }
                    }
                }

                CurrentTilesGrid(
                    listState,
@@ -398,6 +391,36 @@ private fun AutoScrollGrid(
    }
}

@Composable
private fun CurrentTilesGridHeader(
    listState: EditTileListState,
    selectionState: MutableSelectionState,
    onRemoveTile: (TileSpec) -> Unit,
    modifier: Modifier = Modifier,
) {
    AnimatedContent(
        targetState =
            listState.isDraggedCellRemovable ||
                selectionState.selection?.let { listState.isRemovable(it) } ?: false,
        label = "QSEditHeader",
        contentAlignment = Alignment.Center,
        modifier = modifier,
    ) { showRemoveTarget ->
        EditGridHeader {
            if (showRemoveTarget) {
                RemoveTileTarget {
                    selectionState.selection?.let {
                        selectionState.unSelect()
                        onRemoveTile(it)
                    }
                }
            } else {
                EditGridCenteredText(text = stringResource(id = R.string.drag_to_rearrange_tiles))
            }
        }
    }
}

@Composable
private fun EditGridHeader(
    modifier: Modifier = Modifier,
@@ -646,7 +669,7 @@ private fun TileGridCell(
    modifier: Modifier = Modifier,
) {
    val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1)
    val canShowRemovalBadge = cell.tile.availableEditActions.contains(AvailableEditActions.REMOVE)
    val canShowRemovalBadge = cell.tile.isRemovable
    var tileState by remember { mutableStateOf(TileState.None) }

    LaunchedEffect(selectionState.selection, canShowRemovalBadge) {
+3 −0
Original line number Diff line number Diff line
@@ -66,6 +66,9 @@ data class EditTileViewModel(
) : CategoryAndName {
    override val name
        get() = label.text

    val isRemovable
        get() = availableEditActions.contains(AvailableEditActions.REMOVE)
}

enum class AvailableEditActions {
+62 −6
Original line number Diff line number Diff line
@@ -37,6 +37,7 @@ 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.AvailableEditActions
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
@@ -84,7 +85,7 @@ class DragAndDropTest : SysuiTestCase() {
        }
        composeRule.waitForIdle()

        listState.onStarted(TestEditTiles[0], DragType.Add)
        listState.onStarted(TestEditTiles[0], DragType.Move)

        // Tile is being dragged, it should be replaced with a placeholder
        composeRule.onNodeWithContentDescription("tileA").assertDoesNotExist()
@@ -102,6 +103,45 @@ class DragAndDropTest : SysuiTestCase() {
        )
    }

    @Test
    fun nonRemovableDraggedTile_removeHeaderShouldNotExist() {
        val nonRemovableTile = createEditTile("tileA", isRemovable = false)
        val listState = EditTileListState(listOf(nonRemovableTile), columns = 4, largeTilesSpan = 2)
        composeRule.setContent { EditTileGridUnderTest(listState) {} }
        composeRule.waitForIdle()

        listState.onStarted(nonRemovableTile, DragType.Move)

        // Tile is being dragged, it should be replaced with a placeholder
        composeRule.onNodeWithContentDescription("tileA").assertDoesNotExist()

        // Remove drop zone should not appear
        composeRule.onNodeWithText("Remove").assertDoesNotExist()
    }

    @Test
    fun droppedNonRemovableDraggedTile_shouldStayInGrid() {
        val nonRemovableTile = createEditTile("tileA", isRemovable = false)
        val listState = EditTileListState(listOf(nonRemovableTile), columns = 4, largeTilesSpan = 2)
        composeRule.setContent { EditTileGridUnderTest(listState) {} }
        composeRule.waitForIdle()

        listState.onStarted(nonRemovableTile, DragType.Move)

        // Tile is being dragged, it should be replaced with a placeholder
        composeRule.onNodeWithContentDescription("tileA").assertDoesNotExist()

        // Remove drop zone should not appear
        composeRule.onNodeWithText("Remove").assertDoesNotExist()

        // Drop tile outside of the grid
        listState.movedOutOfBounds()
        listState.onDrop()

        // Tile A is still in the grid
        composeRule.assertGridContainsExactly(CURRENT_TILES_GRID_TEST_TAG, listOf("tileA"))
    }

    @Test
    fun draggedTile_shouldChangePosition() {
        var tiles by mutableStateOf(TestEditTiles)
@@ -113,7 +153,11 @@ class DragAndDropTest : SysuiTestCase() {
        }
        composeRule.waitForIdle()

        listState.onStarted(TestEditTiles[0], DragType.Add)
        listState.onStarted(TestEditTiles[0], DragType.Move)

        // Remove drop zone should appear
        composeRule.onNodeWithText("Remove").assertExists()

        listState.onTargeting(1, false)
        listState.onDrop()

@@ -141,7 +185,11 @@ class DragAndDropTest : SysuiTestCase() {
        }
        composeRule.waitForIdle()

        listState.onStarted(TestEditTiles[0], DragType.Add)
        listState.onStarted(TestEditTiles[0], DragType.Move)

        // Remove drop zone should appear
        composeRule.onNodeWithText("Remove").assertExists()

        listState.movedOutOfBounds()
        listState.onDrop()

@@ -169,7 +217,11 @@ class DragAndDropTest : SysuiTestCase() {
        }
        composeRule.waitForIdle()

        listState.onStarted(createEditTile("tile_new"), DragType.Add)
        listState.onStarted(createEditTile("tile_new", isRemovable = false), DragType.Add)

        // Remove drop zone should appear
        composeRule.onNodeWithText("Remove").assertExists()

        // Insert after tileD, which is at index 4
        // [ a ] [ b ] [ c ] [ empty ]
        // [ tile d ] [ e ]
@@ -193,7 +245,10 @@ class DragAndDropTest : SysuiTestCase() {
        private const val CURRENT_TILES_GRID_TEST_TAG = "CurrentTilesGrid"
        private const val AVAILABLE_TILES_GRID_TEST_TAG = "AvailableTilesGrid"

        private fun createEditTile(tileSpec: String): SizedTile<EditTileViewModel> {
        private fun createEditTile(
            tileSpec: String,
            isRemovable: Boolean = true,
        ): SizedTile<EditTileViewModel> {
            return SizedTileImpl(
                EditTileViewModel(
                    tileSpec = TileSpec.create(tileSpec),
@@ -205,7 +260,8 @@ class DragAndDropTest : SysuiTestCase() {
                    label = AnnotatedString(tileSpec),
                    appName = null,
                    isCurrent = true,
                    availableEditActions = emptySet(),
                    availableEditActions =
                        if (isRemovable) setOf(AvailableEditActions.REMOVE) else emptySet(),
                    category = TileCategory.UNKNOWN,
                ),
                getWidth(tileSpec),
Loading