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

Commit fff1a904 authored by Coco Duan's avatar Coco Duan Committed by Android (Google) Code Review
Browse files

Merge changes Icc3721d9,I78357990 into main

* changes:
  Move communal hub lazygrid logic to a separate function
  Drag & drop to reorder and remove widget from glanceable hub
parents bfbd7ae9 12537140
Loading
Loading
Loading
Loading
+82 −35
Original line number Diff line number Diff line
@@ -19,6 +19,8 @@ package com.android.systemui.communal.ui.compose
import android.os.Bundle
import android.util.SizeF
import android.widget.FrameLayout
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -31,11 +33,13 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
@@ -45,7 +49,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -53,6 +56,7 @@ import androidx.compose.ui.viewinterop.AndroidView
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.shared.model.CommunalContentSize
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
import com.android.systemui.media.controls.ui.MediaHierarchyManager
import com.android.systemui.media.controls.ui.MediaHostState
import com.android.systemui.res.R
@@ -67,31 +71,12 @@ fun CommunalHub(
    Box(
        modifier = modifier.fillMaxSize().background(Color.White),
    ) {
        LazyHorizontalGrid(
            modifier = modifier.height(Dimensions.GridHeight).align(Alignment.CenterStart),
            rows = GridCells.Fixed(CommunalContentSize.FULL.span),
            contentPadding = PaddingValues(horizontal = Dimensions.Spacing),
            horizontalArrangement = Arrangement.spacedBy(Dimensions.Spacing),
            verticalArrangement = Arrangement.spacedBy(Dimensions.Spacing),
        ) {
            items(
                count = communalContent.size,
                key = { index -> communalContent[index].key },
                span = { index -> GridItemSpan(communalContent[index].size.span) },
            ) { index ->
                CommunalContent(
                    modifier = Modifier.fillMaxHeight().width(Dimensions.CardWidth),
                    model = communalContent[index],
        CommunalHubLazyGrid(
            modifier = Modifier.height(Dimensions.GridHeight).align(Alignment.CenterStart),
            communalContent = communalContent,
            isEditMode = viewModel.isEditMode,
            viewModel = viewModel,
                    deleteOnClick = if (viewModel.isEditMode) viewModel::onDeleteWidget else null,
                    size =
                        SizeF(
                            Dimensions.CardWidth.value,
                            communalContent[index].size.dp().value,
                        ),
        )
            }
        }
        if (viewModel.isEditMode && onOpenWidgetPicker != null) {
            IconButton(onClick = onOpenWidgetPicker) {
                Icon(Icons.Default.Add, stringResource(R.string.hub_mode_add_widget_button_text))
@@ -114,16 +99,80 @@ fun CommunalHub(
    }
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun CommunalHubLazyGrid(
    communalContent: List<CommunalContentModel>,
    isEditMode: Boolean,
    viewModel: BaseCommunalViewModel,
    modifier: Modifier = Modifier,
) {
    var gridModifier = modifier
    val gridState = rememberLazyGridState()
    var list = communalContent
    var dragDropState: GridDragDropState? = null
    if (isEditMode && viewModel is CommunalEditModeViewModel) {
        val contentListState = rememberContentListState(communalContent, viewModel)
        list = contentListState.list
        dragDropState = rememberGridDragDropState(gridState, contentListState)
        gridModifier = gridModifier.dragContainer(dragDropState)
    }
    LazyHorizontalGrid(
        modifier = gridModifier,
        state = gridState,
        rows = GridCells.Fixed(CommunalContentSize.FULL.span),
        contentPadding = PaddingValues(horizontal = Dimensions.Spacing),
        horizontalArrangement = Arrangement.spacedBy(Dimensions.Spacing),
        verticalArrangement = Arrangement.spacedBy(Dimensions.Spacing),
    ) {
        items(
            count = list.size,
            key = { index -> list[index].key },
            span = { index -> GridItemSpan(list[index].size.span) },
        ) { index ->
            val cardModifier = Modifier.fillMaxHeight().width(Dimensions.CardWidth)
            val size =
                SizeF(
                    Dimensions.CardWidth.value,
                    list[index].size.dp().value,
                )
            if (isEditMode && dragDropState != null) {
                DraggableItem(dragDropState = dragDropState, enabled = true, index = index) {
                    isDragging ->
                    val elevation by animateDpAsState(if (isDragging) 4.dp else 1.dp)
                    CommunalContent(
                        modifier = cardModifier,
                        deleteOnClick = viewModel::onDeleteWidget,
                        elevation = elevation,
                        model = list[index],
                        viewModel = viewModel,
                        size = size,
                    )
                }
            } else {
                CommunalContent(
                    modifier = cardModifier,
                    model = list[index],
                    viewModel = viewModel,
                    size = size,
                )
            }
        }
    }
}

@Composable
private fun CommunalContent(
    model: CommunalContentModel,
    viewModel: BaseCommunalViewModel,
    size: SizeF,
    deleteOnClick: ((id: Int) -> Unit)?,
    modifier: Modifier = Modifier,
    elevation: Dp = 0.dp,
    deleteOnClick: ((id: Int) -> Unit)? = null,
) {
    when (model) {
        is CommunalContentModel.Widget -> WidgetContent(model, size, deleteOnClick, modifier)
        is CommunalContentModel.Widget ->
            WidgetContent(model, size, elevation, deleteOnClick, modifier)
        is CommunalContentModel.Smartspace -> SmartspaceContent(model, modifier)
        is CommunalContentModel.Tutorial -> TutorialContent(modifier)
        is CommunalContentModel.Umo -> Umo(viewModel, modifier)
@@ -134,22 +183,20 @@ private fun CommunalContent(
private fun WidgetContent(
    model: CommunalContentModel.Widget,
    size: SizeF,
    elevation: Dp,
    deleteOnClick: ((id: Int) -> Unit)?,
    modifier: Modifier = Modifier,
) {
    // TODO(b/309009246): update background color
    Box(
    Card(
        modifier = modifier.fillMaxSize().background(Color.White),
        elevation = CardDefaults.cardElevation(draggedElevation = elevation),
    ) {
        if (deleteOnClick != null) {
            IconButton(onClick = { deleteOnClick(model.appWidgetId) }) {
                Icon(
                    Icons.Default.Close,
                    LocalContext.current.getString(R.string.button_to_remove_widget)
                )
                Icon(Icons.Default.Close, stringResource(R.string.button_to_remove_widget))
            }
        }

        AndroidView(
            modifier = modifier,
            factory = { context ->
@@ -210,7 +257,7 @@ private fun CommunalContentSize.dp(): Dp {
    }
}

private object Dimensions {
object Dimensions {
    val CardWidth = 464.dp
    val CardHeightFull = 630.dp
    val CardHeightHalf = 307.dp
+75 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.communal.ui.compose

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 com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel

@Composable
fun rememberContentListState(
    communalContent: List<CommunalContentModel>,
    viewModel: CommunalEditModeViewModel,
): ContentListState {
    return remember(communalContent) {
        ContentListState(
            communalContent,
            viewModel::onDeleteWidget,
            viewModel::onReorderWidgets,
        )
    }
}

/**
 * Keeps the current state of the [CommunalContentModel] list being edited. [GridDragDropState]
 * interacts with this class to update the order in the list. [onSaveList] should be called on
 * dragging ends to persist the state in db for better performance.
 */
class ContentListState
internal constructor(
    communalContent: List<CommunalContentModel>,
    private val onDeleteWidget: (id: Int) -> Unit,
    private val onReorderWidgets: (ids: List<Int>) -> Unit,
) {
    var list by mutableStateOf(communalContent)
        private set

    /** Move item to a new position in the list. */
    fun onMove(fromIndex: Int, toIndex: Int) {
        list = list.toMutableList().apply { add(toIndex, removeAt(fromIndex)) }
    }

    /** Remove widget from the list and the database. */
    fun onRemove(indexToRemove: Int) {
        if (list[indexToRemove] is CommunalContentModel.Widget) {
            val widget = list[indexToRemove] as CommunalContentModel.Widget
            list = list.toMutableList().apply { removeAt(indexToRemove) }
            onDeleteWidget(widget.appWidgetId)
        }
    }

    /** Persist the new order with all the movements happened during dragging. */
    fun onSaveList() {
        val widgetIds: List<Int> =
            list.filterIsInstance<CommunalContentModel.Widget>().map { it.appWidgetId }
        onReorderWidgets(widgetIds)
    }
}
+247 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.communal.ui.compose

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
import androidx.compose.foundation.lazy.grid.LazyGridItemScope
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.toOffset
import androidx.compose.ui.unit.toSize
import androidx.compose.ui.zIndex
import com.android.systemui.communal.domain.model.CommunalContentModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch

@Composable
fun rememberGridDragDropState(
    gridState: LazyGridState,
    contentListState: ContentListState
): GridDragDropState {
    val scope = rememberCoroutineScope()
    val state =
        remember(gridState, contentListState) {
            GridDragDropState(state = gridState, contentListState = contentListState, scope = scope)
        }
    LaunchedEffect(state) {
        while (true) {
            val diff = state.scrollChannel.receive()
            gridState.scrollBy(diff)
        }
    }
    return state
}

/**
 * Handles drag and drop cards in the glanceable hub. While dragging to move, other items that are
 * affected will dynamically get positioned and the state is tracked by [ContentListState]. When
 * dragging to remove, affected cards will be moved and [ContentListState.onRemove] is called to
 * remove the dragged item. On dragging ends, call [ContentListState.onSaveList] to persist the
 * change.
 */
class GridDragDropState
internal constructor(
    private val state: LazyGridState,
    private val contentListState: ContentListState,
    private val scope: CoroutineScope,
) {
    var draggingItemIndex by mutableStateOf<Int?>(null)
        private set

    internal val scrollChannel = Channel<Float>()

    private var draggingItemDraggedDelta by mutableStateOf(Offset.Zero)
    private var draggingItemInitialOffset by mutableStateOf(Offset.Zero)
    internal val draggingItemOffset: Offset
        get() =
            draggingItemLayoutInfo?.let { item ->
                draggingItemInitialOffset + draggingItemDraggedDelta - item.offset.toOffset()
            }
                ?: Offset.Zero

    private val draggingItemLayoutInfo: LazyGridItemInfo?
        get() = state.layoutInfo.visibleItemsInfo.firstOrNull { it.index == draggingItemIndex }

    internal fun onDragStart(offset: Offset) {
        state.layoutInfo.visibleItemsInfo
            .firstOrNull { item ->
                item.isEditable &&
                    offset.x.toInt() in item.offset.x..item.offsetEnd.x &&
                    offset.y.toInt() in item.offset.y..item.offsetEnd.y
            }
            ?.apply {
                draggingItemIndex = index
                draggingItemInitialOffset = this.offset.toOffset()
            }
    }

    internal fun onDragInterrupted() {
        if (draggingItemIndex != null) {
            // persist list editing changes on dragging ends
            contentListState.onSaveList()
            draggingItemIndex = null
        }
        draggingItemDraggedDelta = Offset.Zero
        draggingItemInitialOffset = Offset.Zero
    }

    internal fun onDrag(offset: Offset) {
        draggingItemDraggedDelta += offset

        val draggingItem = draggingItemLayoutInfo ?: return
        val startOffset = draggingItem.offset.toOffset() + draggingItemOffset
        val endOffset = startOffset + draggingItem.size.toSize()
        val middleOffset = startOffset + (endOffset - startOffset) / 2f

        val targetItem =
            state.layoutInfo.visibleItemsInfo.find { item ->
                item.isEditable &&
                    middleOffset.x.toInt() in item.offset.x..item.offsetEnd.x &&
                    middleOffset.y.toInt() in item.offset.y..item.offsetEnd.y &&
                    draggingItem.index != item.index
            }

        if (targetItem != null) {
            val scrollToIndex =
                if (targetItem.index == state.firstVisibleItemIndex) {
                    draggingItem.index
                } else if (draggingItem.index == state.firstVisibleItemIndex) {
                    targetItem.index
                } else {
                    null
                }
            if (scrollToIndex != null) {
                scope.launch {
                    // this is needed to neutralize automatic keeping the first item first.
                    state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset)
                    contentListState.onMove(draggingItem.index, targetItem.index)
                }
            } else {
                contentListState.onMove(draggingItem.index, targetItem.index)
            }
            draggingItemIndex = targetItem.index
        } else {
            val overscroll = checkForOverscroll(startOffset, endOffset)
            if (overscroll != 0f) {
                scrollChannel.trySend(overscroll)
            }
            val removeOffset = checkForRemove(startOffset)
            if (removeOffset != 0f) {
                draggingItemIndex?.let {
                    contentListState.onRemove(it)
                    draggingItemIndex = null
                }
            }
        }
    }

    private val LazyGridItemInfo.offsetEnd: IntOffset
        get() = this.offset + this.size

    /** Whether the grid item can be dragged or be a drop target. Only widget card is editable. */
    private val LazyGridItemInfo.isEditable: Boolean
        get() = contentListState.list[this.index] is CommunalContentModel.Widget

    /** Calculate the amount dragged out of bound on both sides. Returns 0f if not overscrolled */
    private fun checkForOverscroll(startOffset: Offset, endOffset: Offset): Float {
        return when {
            draggingItemDraggedDelta.x > 0 ->
                (endOffset.x - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f)
            draggingItemDraggedDelta.x < 0 ->
                (startOffset.x - state.layoutInfo.viewportStartOffset).coerceAtMost(0f)
            else -> 0f
        }
    }

    // TODO(b/309968801): a temporary solution to decide whether to remove card when it's dragged up
    //  and out of grid. Once we have a taskbar, calculate the intersection of the dragged item with
    //  the Remove button.
    private fun checkForRemove(startOffset: Offset): Float {
        return if (draggingItemDraggedDelta.y < 0)
            (startOffset.y + Dimensions.CardHeightHalf.value - state.layoutInfo.viewportStartOffset)
                .coerceAtMost(0f)
        else 0f
    }
}

private operator fun IntOffset.plus(size: IntSize): IntOffset {
    return IntOffset(x + size.width, y + size.height)
}

private operator fun Offset.plus(size: Size): Offset {
    return Offset(x + size.width, y + size.height)
}

fun Modifier.dragContainer(dragDropState: GridDragDropState): Modifier {
    return pointerInput(dragDropState) {
        detectDragGesturesAfterLongPress(
            onDrag = { change, offset ->
                change.consume()
                dragDropState.onDrag(offset = offset)
            },
            onDragStart = { offset -> dragDropState.onDragStart(offset) },
            onDragEnd = { dragDropState.onDragInterrupted() },
            onDragCancel = { dragDropState.onDragInterrupted() }
        )
    }
}

/** Wrap LazyGrid item with additional modifier needed for drag and drop. */
@ExperimentalFoundationApi
@Composable
fun LazyGridItemScope.DraggableItem(
    dragDropState: GridDragDropState,
    index: Int,
    enabled: Boolean,
    modifier: Modifier = Modifier,
    content: @Composable (isDragging: Boolean) -> Unit
) {
    if (!enabled) {
        return Box(modifier = modifier) { content(false) }
    }
    val dragging = index == dragDropState.draggingItemIndex
    val draggingModifier =
        if (dragging) {
            Modifier.zIndex(1f).graphicsLayer {
                translationX = dragDropState.draggingItemOffset.x
                translationY = dragDropState.draggingItemOffset.y
            }
        } else {
            Modifier.animateItemPlacement()
        }
    Box(modifier = modifier.then(draggingModifier), propagateMinConstraints = true) {
        content(dragging)
    }
}
+13 −1
Original line number Diff line number Diff line
@@ -91,7 +91,8 @@ constructor(
interface CommunalWidgetDao {
    @Query(
        "SELECT * FROM communal_widget_table JOIN communal_item_rank_table " +
            "ON communal_item_rank_table.uid = communal_widget_table.item_id"
            "ON communal_item_rank_table.uid = communal_widget_table.item_id " +
            "ORDER BY communal_item_rank_table.rank DESC"
    )
    fun getWidgets(): Flow<Map<CommunalItemRank, CommunalWidgetItem>>

@@ -112,6 +113,17 @@ interface CommunalWidgetDao {
    @Query("INSERT INTO communal_item_rank_table(rank) VALUES(:rank)")
    fun insertItemRank(rank: Int): Long

    @Query("UPDATE communal_item_rank_table SET rank = :order WHERE uid = :itemUid")
    fun updateItemRank(itemUid: Long, order: Int)

    @Transaction
    fun updateWidgetOrder(ids: List<Int>) {
        ids.forEachIndexed { index, it ->
            val widget = getWidgetByIdNow(it)
            updateItemRank(widget.itemId, ids.size - index)
        }
    }

    @Transaction
    fun addWidget(widgetId: Int, provider: ComponentName, priority: Int): Long {
        return insertWidget(
+12 −0
Original line number Diff line number Diff line
@@ -61,6 +61,9 @@ interface CommunalWidgetRepository {

    /** Delete a widget by id from app widget service and the database. */
    fun deleteWidget(widgetId: Int) {}

    /** Update the order of widgets in the database. */
    fun updateWidgetOrder(ids: List<Int>) {}
}

@OptIn(ExperimentalCoroutinesApi::class)
@@ -165,6 +168,15 @@ constructor(
        }
    }

    override fun updateWidgetOrder(ids: List<Int>) {
        applicationScope.launch(bgDispatcher) {
            communalWidgetDao.updateWidgetOrder(ids)
            logger.i({ "Updated the order of widget list with ids: $str1." }) {
                str1 = ids.toString()
            }
        }
    }

    private fun mapToContentModel(
        entry: Map.Entry<CommunalItemRank, CommunalWidgetItem>
    ): CommunalWidgetContentModel {
Loading