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

Commit 3f7eb841 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Implement single click actions to add/remove tiles in edit mode" into main

parents 3af0fbfc e089d761
Loading
Loading
Loading
Loading
+35 −24
Original line number Diff line number Diff line
@@ -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,
+2 −0
Original line number Diff line number Diff line
@@ -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,
+8 −5
Original line number Diff line number Diff line
@@ -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
    }
}

+1 −0
Original line number Diff line number Diff line
@@ -68,6 +68,7 @@ class DragAndDropTest : SysuiTestCase() {
            columns = 4,
            largeTilesSpan = 4,
            modifier = Modifier.fillMaxSize(),
            onAddTile = {},
            onRemoveTile = {},
            onSetTiles = onSetTiles,
            onResize = { _, _ -> },
+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