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
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 2ba1b77fb..e8ecd3a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -19,6 +19,8 @@
 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.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.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 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 @@
     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],
-                    viewModel = viewModel,
-                    deleteOnClick = if (viewModel.isEditMode) viewModel::onDeleteWidget else null,
-                    size =
-                        SizeF(
-                            Dimensions.CardWidth.value,
-                            communalContent[index].size.dp().value,
-                        ),
-                )
-            }
-        }
+        CommunalHubLazyGrid(
+            modifier = Modifier.height(Dimensions.GridHeight).align(Alignment.CenterStart),
+            communalContent = communalContent,
+            isEditMode = viewModel.isEditMode,
+            viewModel = viewModel,
+        )
         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 @@
     }
 }
 
+@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 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 object Dimensions {
+object Dimensions {
     val CardWidth = 464.dp
     val CardHeightFull = 630.dp
     val CardHeightHalf = 307.dp
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
new file mode 100644
index 0000000..89c5765
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
@@ -0,0 +1,75 @@
+/*
+ * 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)
+    }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt
new file mode 100644
index 0000000..6cfa2f2
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt
@@ -0,0 +1,247 @@
+/*
+ * 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)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
index e50850d..a12db6f 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
@@ -91,7 +91,8 @@
 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 @@
     @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(
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
index f7fee96..ded5581 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
@@ -61,6 +61,9 @@
 
     /** 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 @@
         }
     }
 
+    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 {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 927bf02..fd7f641 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -84,6 +84,9 @@
     /** Delete a widget by id. */
     fun deleteWidget(id: Int) = widgetRepository.deleteWidget(id)
 
+    /** Reorder widgets. The order in the list will be their display order in the hub. */
+    fun updateWidgetOrder(ids: List<Int>) = widgetRepository.updateWidgetOrder(ids)
+
     /** A list of widget content to be displayed in the communal hub. */
     val widgetContent: Flow<List<CommunalContentModel.Widget>> =
         widgetRepository.communalWidgets.map { widgets ->
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index 98f3594..b4ab5fb 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -43,6 +43,9 @@
     /** Called as the UI requests deleting a widget. */
     open fun onDeleteWidget(id: Int) {}
 
+    /** Called as the UI requests reordering widgets. */
+    open fun onReorderWidgets(ids: List<Int>) {}
+
     /** Called as the UI requests opening the widget editor. */
     open fun onOpenWidgetEditor() {}
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index 14d9b2c..111f8b4 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -41,4 +41,6 @@
         communalInteractor.widgetContent
 
     override fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id)
+
+    override fun onReorderWidgets(ids: List<Int>) = communalInteractor.updateWidgetOrder(ids)
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
index 14ec4d4..16b2ed6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
@@ -124,6 +124,39 @@
             assertThat(widgets()).containsExactly(communalItemRankEntry2, communalWidgetItemEntry2)
         }
 
+    @Test
+    fun reorderWidget_emitsWidgetsInNewOrder() =
+        testScope.runTest {
+            val widgetsToAdd = listOf(widgetInfo1, widgetInfo2)
+            val widgets = collectLastValue(communalWidgetDao.getWidgets())
+
+            widgetsToAdd.forEach {
+                val (widgetId, provider, priority) = it
+                communalWidgetDao.addWidget(
+                    widgetId = widgetId,
+                    provider = provider,
+                    priority = priority,
+                )
+            }
+            assertThat(widgets())
+                .containsExactly(
+                    communalItemRankEntry1,
+                    communalWidgetItemEntry1,
+                    communalItemRankEntry2,
+                    communalWidgetItemEntry2
+                )
+
+            val widgetIdsInNewOrder = listOf(widgetInfo2.widgetId, widgetInfo1.widgetId)
+            communalWidgetDao.updateWidgetOrder(widgetIdsInNewOrder)
+            assertThat(widgets())
+                .containsExactly(
+                    communalItemRankEntry2,
+                    communalWidgetItemEntry2,
+                    communalItemRankEntry1,
+                    communalWidgetItemEntry1
+                )
+        }
+
     data class FakeWidgetMetadata(
         val widgetId: Int,
         val provider: ComponentName,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
index 28fae81..182712a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
@@ -202,6 +202,20 @@
         }
 
     @Test
+    fun reorderWidgets_queryDb() =
+        testScope.runTest {
+            userUnlocked(true)
+            val repository = initCommunalWidgetRepository()
+            runCurrent()
+
+            val ids = listOf(104, 103, 101)
+            repository.updateWidgetOrder(ids)
+            runCurrent()
+
+            verify(communalWidgetDao).updateWidgetOrder(ids)
+        }
+
+    @Test
     fun broadcastReceiver_communalDisabled_doNotRegisterUserUnlockedBroadcastReceiver() =
         testScope.runTest {
             communalEnabled(false)