Merge "Make items selectable in the communal hub" into main
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 91a4d2e..c8e18d7 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
@@ -20,6 +20,7 @@
 import android.os.Bundle
 import android.util.SizeF
 import android.widget.FrameLayout
+import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.foundation.BorderStroke
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.background
@@ -38,6 +39,7 @@
 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.LazyGridState
 import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
 import androidx.compose.foundation.lazy.grid.rememberLazyGridState
 import androidx.compose.foundation.shape.RoundedCornerShape
@@ -58,17 +60,22 @@
 import androidx.compose.material3.OutlinedButton
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
 import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.boundsInWindow
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.layout.positionInWindow
@@ -86,6 +93,9 @@
 import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.systemui.communal.domain.model.CommunalContentModel
 import com.android.systemui.communal.shared.model.CommunalContentSize
+import com.android.systemui.communal.ui.compose.extensions.allowGestures
+import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset
+import com.android.systemui.communal.ui.compose.extensions.observeTapsWithoutConsuming
 import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
 import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
 import com.android.systemui.res.R
@@ -104,22 +114,59 @@
     var toolbarSize: IntSize? by remember { mutableStateOf(null) }
     var gridCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) }
     var isDraggingToRemove by remember { mutableStateOf(false) }
+    val gridState = rememberLazyGridState()
+    val contentListState = rememberContentListState(communalContent, viewModel)
+    val reorderingWidgets by viewModel.reorderingWidgets.collectAsState()
+    val selectedIndex = viewModel.selectedIndex.collectAsState()
+    val removeButtonEnabled by remember {
+        derivedStateOf { selectedIndex.value != null || reorderingWidgets }
+    }
+
+    val contentPadding = gridContentPadding(viewModel.isEditMode, toolbarSize)
+    val contentOffset = beforeContentPadding(contentPadding).toOffset()
 
     Box(
         modifier =
-            modifier.fillMaxSize().background(LocalAndroidColorScheme.current.outlineVariant),
+            modifier
+                .fillMaxSize()
+                .background(LocalAndroidColorScheme.current.outlineVariant)
+                .pointerInput(gridState, contentOffset, contentListState) {
+                    // If not in edit mode, don't allow selecting items.
+                    if (!viewModel.isEditMode) return@pointerInput
+                    observeTapsWithoutConsuming { offset ->
+                        val adjustedOffset = offset - contentOffset
+                        val index =
+                            gridState.layoutInfo.visibleItemsInfo
+                                .firstItemAtOffset(adjustedOffset)
+                                ?.index
+                        val newIndex =
+                            if (index?.let(contentListState::isItemEditable) == true) {
+                                index
+                            } else {
+                                null
+                            }
+                        viewModel.setSelectedIndex(newIndex)
+                    }
+                },
     ) {
         CommunalHubLazyGrid(
             communalContent = communalContent,
             viewModel = viewModel,
-            contentPadding = gridContentPadding(viewModel.isEditMode, toolbarSize),
+            contentPadding = contentPadding,
+            contentOffset = contentOffset,
             setGridCoordinates = { gridCoordinates = it },
-            updateDragPositionForRemove = {
+            updateDragPositionForRemove = { offset ->
                 isDraggingToRemove =
-                    checkForDraggingToRemove(it, removeButtonCoordinates, gridCoordinates)
+                    isPointerWithinCoordinates(
+                        offset = gridCoordinates?.let { it.positionInWindow() + offset },
+                        containerToCheck = removeButtonCoordinates
+                    )
                 isDraggingToRemove
             },
             onOpenWidgetPicker = onOpenWidgetPicker,
+            gridState = gridState,
+            contentListState = contentListState,
+            selectedIndex = selectedIndex
         )
 
         if (viewModel.isEditMode && onOpenWidgetPicker != null && onEditDone != null) {
@@ -129,6 +176,14 @@
                 setRemoveButtonCoordinates = { removeButtonCoordinates = it },
                 onEditDone = onEditDone,
                 onOpenWidgetPicker = onOpenWidgetPicker,
+                onRemoveClicked = {
+                    selectedIndex.value?.let { index ->
+                        contentListState.onRemove(index)
+                        contentListState.onSaveList()
+                        viewModel.setSelectedIndex(null)
+                    }
+                },
+                removeEnabled = removeButtonEnabled
             )
         } else {
             IconButton(onClick = viewModel::onOpenWidgetEditor) {
@@ -158,16 +213,18 @@
     communalContent: List<CommunalContentModel>,
     viewModel: BaseCommunalViewModel,
     contentPadding: PaddingValues,
+    selectedIndex: State<Int?>,
+    contentOffset: Offset,
+    gridState: LazyGridState,
+    contentListState: ContentListState,
     setGridCoordinates: (coordinates: LayoutCoordinates) -> Unit,
     updateDragPositionForRemove: (offset: Offset) -> Boolean,
     onOpenWidgetPicker: (() -> Unit)? = null,
 ) {
     var gridModifier = Modifier.align(Alignment.CenterStart)
-    val gridState = rememberLazyGridState()
     var list = communalContent
     var dragDropState: GridDragDropState? = null
     if (viewModel.isEditMode && viewModel is CommunalEditModeViewModel) {
-        val contentListState = rememberContentListState(list, viewModel)
         list = contentListState.list
         // for drag & drop operations within the communal hub grid
         dragDropState =
@@ -179,7 +236,7 @@
         gridModifier =
             gridModifier
                 .fillMaxSize()
-                .dragContainer(dragDropState, beforeContentPadding(contentPadding), viewModel)
+                .dragContainer(dragDropState, contentOffset, viewModel)
                 .onGloballyPositioned { setGridCoordinates(it) }
         // for widgets dropped from other activities
         val dragAndDropTargetState =
@@ -218,8 +275,10 @@
                     list[index].size.dp().value,
                 )
             if (viewModel.isEditMode && dragDropState != null) {
+                val selected by remember(index) { derivedStateOf { index == selectedIndex.value } }
                 DraggableItem(
                     dragDropState = dragDropState,
+                    selected = selected,
                     enabled = list[index] is CommunalContentModel.Widget,
                     index = index,
                     size = size
@@ -253,11 +312,19 @@
 @Composable
 private fun Toolbar(
     isDraggingToRemove: Boolean,
+    removeEnabled: Boolean,
+    onRemoveClicked: () -> Unit,
     setToolbarSize: (toolbarSize: IntSize) -> Unit,
     setRemoveButtonCoordinates: (coordinates: LayoutCoordinates) -> Unit,
     onOpenWidgetPicker: () -> Unit,
-    onEditDone: () -> Unit,
+    onEditDone: () -> Unit
 ) {
+    val removeButtonAlpha: Float by
+        animateFloatAsState(
+            targetValue = if (removeEnabled) 1f else 0.5f,
+            label = "RemoveButtonAlphaAnimation"
+        )
+
     Row(
         modifier =
             Modifier.fillMaxWidth()
@@ -301,13 +368,18 @@
             }
         } else {
             OutlinedButton(
-                // Button is disabled to make it non-clickable
-                enabled = false,
-                onClick = {},
-                colors = ButtonDefaults.outlinedButtonColors(disabledContentColor = colors.primary),
+                enabled = removeEnabled,
+                onClick = onRemoveClicked,
+                colors =
+                    ButtonDefaults.outlinedButtonColors(
+                        contentColor = colors.primary,
+                        disabledContentColor = colors.primary
+                    ),
                 border = BorderStroke(width = 1.0.dp, color = colors.primary),
                 contentPadding = Dimensions.ButtonPadding,
-                modifier = Modifier.onGloballyPositioned { setRemoveButtonCoordinates(it) }
+                modifier =
+                    Modifier.graphicsLayer { alpha = removeButtonAlpha }
+                        .onGloballyPositioned { setRemoveButtonCoordinates(it) }
             ) {
                 RemoveButtonContent(spacerModifier)
             }
@@ -385,7 +457,7 @@
 ) {
     when (model) {
         is CommunalContentModel.Widget -> WidgetContent(viewModel, model, size, modifier)
-        is CommunalContentModel.WidgetPlaceholder -> WidgetPlaceholderContent(size)
+        is CommunalContentModel.WidgetPlaceholder -> HighlightedItem(size)
         is CommunalContentModel.CtaTileInViewMode ->
             CtaTileInViewModeContent(viewModel, size, modifier)
         is CommunalContentModel.CtaTileInEditMode ->
@@ -396,11 +468,11 @@
     }
 }
 
-/** Presents a placeholder card for the new widget being dragged and dropping into the grid. */
+/** Creates an empty card used to highlight a particular spot on the grid. */
 @Composable
-fun WidgetPlaceholderContent(size: SizeF) {
+fun HighlightedItem(size: SizeF, modifier: Modifier = Modifier) {
     Card(
-        modifier = Modifier.size(Dp(size.width), Dp(size.height)),
+        modifier = modifier.size(Dp(size.width), Dp(size.height)),
         colors = CardDefaults.cardColors(containerColor = Color.Transparent),
         border = BorderStroke(3.dp, LocalAndroidColorScheme.current.tertiaryFixed),
         shape = RoundedCornerShape(16.dp)
@@ -528,7 +600,7 @@
         contentAlignment = Alignment.Center,
     ) {
         AndroidView(
-            modifier = modifier,
+            modifier = modifier.allowGestures(allowed = !viewModel.isEditMode),
             factory = { context ->
                 // The AppWidgetHostView will inherit the interaction handler from the
                 // AppWidgetHost. So set the interaction handler here before creating the view, and
@@ -616,8 +688,8 @@
 private fun beforeContentPadding(paddingValues: PaddingValues): ContentPaddingInPx {
     return with(LocalDensity.current) {
         ContentPaddingInPx(
-            startPadding = paddingValues.calculateLeftPadding(LayoutDirection.Ltr).toPx(),
-            topPadding = paddingValues.calculateTopPadding().toPx()
+            start = paddingValues.calculateLeftPadding(LayoutDirection.Ltr).toPx(),
+            top = paddingValues.calculateTopPadding().toPx()
         )
     }
 }
@@ -626,18 +698,15 @@
  * Check whether the pointer position that the item is being dragged at is within the coordinates of
  * the remove button in the toolbar. Returns true if the item is removable.
  */
-private fun checkForDraggingToRemove(
-    offset: Offset,
-    removeButtonCoordinates: LayoutCoordinates?,
-    gridCoordinates: LayoutCoordinates?,
+private fun isPointerWithinCoordinates(
+    offset: Offset?,
+    containerToCheck: LayoutCoordinates?
 ): Boolean {
-    if (removeButtonCoordinates == null || gridCoordinates == null) {
+    if (offset == null || containerToCheck == null) {
         return false
     }
-    val pointer = gridCoordinates.positionInWindow() + offset
-    val removeButton = removeButtonCoordinates.positionInWindow()
-    return pointer.x in removeButton.x..removeButton.x + removeButtonCoordinates.size.width &&
-        pointer.y in removeButton.y..removeButton.y + removeButtonCoordinates.size.height
+    val container = containerToCheck.boundsInWindow()
+    return container.contains(offset)
 }
 
 private fun CommunalContentSize.dp(): Dp {
@@ -648,7 +717,9 @@
     }
 }
 
-data class ContentPaddingInPx(val startPadding: Float, val topPadding: Float)
+data class ContentPaddingInPx(val start: Float, val top: Float) {
+    fun toOffset(): Offset = Offset(start, top)
+}
 
 object Dimensions {
     val CardWidth = 464.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
index 979991d..45f98b8 100644
--- 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
@@ -21,12 +21,12 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.toMutableStateList
 import com.android.systemui.communal.domain.model.CommunalContentModel
-import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
+import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
 
 @Composable
 fun rememberContentListState(
     communalContent: List<CommunalContentModel>,
-    viewModel: CommunalEditModeViewModel,
+    viewModel: BaseCommunalViewModel,
 ): ContentListState {
     return remember(communalContent) {
         ContentListState(
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
index 1138221..a195953 100644
--- 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
@@ -17,6 +17,10 @@
 package com.android.systemui.communal.ui.compose
 
 import android.util.SizeF
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
 import androidx.compose.foundation.gestures.scrollBy
@@ -32,6 +36,7 @@
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.pointer.pointerInput
@@ -39,6 +44,7 @@
 import androidx.compose.ui.unit.toOffset
 import androidx.compose.ui.unit.toSize
 import androidx.compose.ui.zIndex
+import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset
 import com.android.systemui.communal.ui.compose.extensions.plus
 import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
 import kotlinx.coroutines.CoroutineScope
@@ -109,13 +115,10 @@
 
     internal fun onDragStart(offset: Offset, contentOffset: Offset) {
         state.layoutInfo.visibleItemsInfo
-            .firstOrNull { item ->
-                // grid item offset is based off grid content container so we need to deduct
-                // before content padding from the initial pointer position
-                contentListState.isItemEditable(item.index) &&
-                    (offset.x - contentOffset.x).toInt() in item.offset.x..item.offsetEnd.x &&
-                    (offset.y - contentOffset.y).toInt() in item.offset.y..item.offsetEnd.y
-            }
+            .filter { item -> contentListState.isItemEditable(item.index) }
+            // grid item offset is based off grid content container so we need to deduct
+            // before content padding from the initial pointer position
+            .firstItemAtOffset(offset - contentOffset)
             ?.apply {
                 dragStartPointerOffset = offset - this.offset.toOffset()
                 draggingItemIndex = index
@@ -148,12 +151,11 @@
         val middleOffset = startOffset + (endOffset - startOffset) / 2f
 
         val targetItem =
-            state.layoutInfo.visibleItemsInfo.find { item ->
-                contentListState.isItemEditable(item.index) &&
-                    middleOffset.x.toInt() in item.offset.x..item.offsetEnd.x &&
-                    middleOffset.y.toInt() in item.offset.y..item.offsetEnd.y &&
-                    draggingItem.index != item.index
-            }
+            state.layoutInfo.visibleItemsInfo
+                .asSequence()
+                .filter { item -> contentListState.isItemEditable(item.index) }
+                .filter { item -> draggingItem.index != item.index }
+                .firstItemAtOffset(middleOffset)
 
         if (targetItem != null) {
             val scrollToIndex =
@@ -208,32 +210,31 @@
 
 fun Modifier.dragContainer(
     dragDropState: GridDragDropState,
-    beforeContentPadding: ContentPaddingInPx,
+    contentOffset: Offset,
     viewModel: BaseCommunalViewModel,
 ): Modifier {
-    return pointerInput(dragDropState, beforeContentPadding) {
-        detectDragGesturesAfterLongPress(
-            onDrag = { change, offset ->
-                change.consume()
-                dragDropState.onDrag(offset = offset)
-            },
-            onDragStart = { offset ->
-                dragDropState.onDragStart(
-                    offset,
-                    Offset(beforeContentPadding.startPadding, beforeContentPadding.topPadding)
-                )
-                viewModel.onReorderWidgetStart()
-            },
-            onDragEnd = {
-                dragDropState.onDragInterrupted()
-                viewModel.onReorderWidgetEnd()
-            },
-            onDragCancel = {
-                dragDropState.onDragInterrupted()
-                viewModel.onReorderWidgetCancel()
-            }
-        )
-    }
+    return this.then(
+        pointerInput(dragDropState, contentOffset) {
+            detectDragGesturesAfterLongPress(
+                onDrag = { change, offset ->
+                    change.consume()
+                    dragDropState.onDrag(offset = offset)
+                },
+                onDragStart = { offset ->
+                    dragDropState.onDragStart(offset, contentOffset)
+                    viewModel.onReorderWidgetStart()
+                },
+                onDragEnd = {
+                    dragDropState.onDragInterrupted()
+                    viewModel.onReorderWidgetEnd()
+                },
+                onDragCancel = {
+                    dragDropState.onDragInterrupted()
+                    viewModel.onReorderWidgetCancel()
+                }
+            )
+        }
+    )
 }
 
 /** Wrap LazyGrid item with additional modifier needed for drag and drop. */
@@ -243,6 +244,7 @@
     dragDropState: GridDragDropState,
     index: Int,
     enabled: Boolean,
+    selected: Boolean,
     size: SizeF,
     modifier: Modifier = Modifier,
     content: @Composable (isDragging: Boolean) -> Unit
@@ -250,21 +252,31 @@
     if (!enabled) {
         return Box(modifier = modifier) { content(false) }
     }
+
     val dragging = index == dragDropState.draggingItemIndex
+    val itemAlpha: Float by
+        animateFloatAsState(
+            targetValue = if (dragDropState.isDraggingToRemove) 0.5f else 1f,
+            label = "DraggableItemAlpha"
+        )
     val draggingModifier =
         if (dragging) {
             Modifier.zIndex(1f).graphicsLayer {
                 translationX = dragDropState.draggingItemOffset.x
                 translationY = dragDropState.draggingItemOffset.y
-                alpha = if (dragDropState.isDraggingToRemove) 0.5f else 1f
+                alpha = itemAlpha
             }
         } else {
             Modifier.animateItemPlacement()
         }
 
     Box(modifier) {
-        if (dragging) {
-            WidgetPlaceholderContent(size)
+        AnimatedVisibility(
+            visible = (dragging || selected) && !dragDropState.isDraggingToRemove,
+            enter = fadeIn(),
+            exit = fadeOut()
+        ) {
+            HighlightedItem(size)
         }
         Box(modifier = draggingModifier, propagateMinConstraints = true) { content(dragging) }
     }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/LazyGridStateExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/LazyGridStateExt.kt
new file mode 100644
index 0000000..132093f
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/LazyGridStateExt.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 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.extensions
+
+import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.toRect
+
+/**
+ * Determine the item at the specified offset, or null if none exist.
+ *
+ * @param offset The offset in pixels, relative to the top start of the grid.
+ */
+fun Iterable<LazyGridItemInfo>.firstItemAtOffset(offset: Offset): LazyGridItemInfo? =
+    firstOrNull { item ->
+        isItemAtOffset(item, offset)
+    }
+
+/**
+ * Determine the item at the specified offset, or null if none exist.
+ *
+ * @param offset The offset in pixels, relative to the top start of the grid.
+ */
+fun Sequence<LazyGridItemInfo>.firstItemAtOffset(offset: Offset): LazyGridItemInfo? =
+    firstOrNull { item ->
+        isItemAtOffset(item, offset)
+    }
+
+private fun isItemAtOffset(item: LazyGridItemInfo, offset: Offset): Boolean {
+    val boundingBox = IntRect(item.offset, item.size)
+    return boundingBox.toRect().contains(offset)
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/ModifierExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/ModifierExt.kt
new file mode 100644
index 0000000..b31008e
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/ModifierExt.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 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.extensions
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.pointerInput
+
+/** Sets whether gestures are allowed on children of this element. */
+fun Modifier.allowGestures(allowed: Boolean): Modifier =
+    if (allowed) {
+        this
+    } else {
+        this.then(pointerInput(Unit) { consumeAllGestures() })
+    }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt
new file mode 100644
index 0000000..1407494
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 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.extensions
+
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.waitForUpOrCancellation
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerInputScope
+import kotlinx.coroutines.coroutineScope
+
+/**
+ * Observe taps without actually consuming them, so child elements can still respond to them. Long
+ * presses are excluded.
+ */
+suspend fun PointerInputScope.observeTapsWithoutConsuming(
+    pass: PointerEventPass = PointerEventPass.Initial,
+    onTap: ((Offset) -> Unit)? = null,
+) = coroutineScope {
+    if (onTap == null) return@coroutineScope
+    awaitEachGesture {
+        awaitFirstDown(pass = pass)
+        val tapTimeout = viewConfiguration.longPressTimeoutMillis
+        val up = withTimeoutOrNull(tapTimeout) { waitForUpOrCancellation(pass = pass) }
+        if (up != null) {
+            onTap(up.position)
+        }
+    }
+}
+
+/** Consume all gestures on the initial pass so that child elements do not receive them. */
+suspend fun PointerInputScope.consumeAllGestures() = coroutineScope {
+    awaitEachGesture {
+        awaitPointerEvent(pass = PointerEventPass.Initial)
+            .changes
+            .forEach(PointerInputChange::consume)
+    }
+}
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 84708a4..4da348e 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
@@ -24,6 +24,7 @@
 import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
 import com.android.systemui.media.controls.ui.MediaHost
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.flowOf
 
@@ -36,6 +37,15 @@
 
     val currentScene: StateFlow<CommunalSceneKey> = communalInteractor.desiredScene
 
+    /** Whether widgets are currently being re-ordered. */
+    open val reorderingWidgets: StateFlow<Boolean> = MutableStateFlow(false)
+
+    private val _selectedIndex: MutableStateFlow<Int?> = MutableStateFlow(null)
+
+    /** The index of the currently selected item, or null if no item selected. */
+    val selectedIndex: StateFlow<Int?>
+        get() = _selectedIndex
+
     fun onSceneChanged(scene: CommunalSceneKey) {
         communalInteractor.onSceneChanged(scene)
     }
@@ -105,4 +115,9 @@
 
     /** Called as the user cancels dragging a widget to reorder. */
     open fun onReorderWidgetCancel() {}
+
+    /** Set the index of the currently selected item */
+    fun setSelectedIndex(index: Int?) {
+        _selectedIndex.value = index
+    }
 }
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 7faf653..fcad45f 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
@@ -37,7 +37,10 @@
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
 
 /** The view model for communal hub in edit mode. */
 @SysUISingleton
@@ -69,9 +72,15 @@
 
     // Only widgets are editable. The CTA tile comes last in the list and remains visible.
     override val communalContent: Flow<List<CommunalContentModel>> =
-        communalInteractor.widgetContent.map { widgets ->
-            widgets + listOf(CommunalContentModel.CtaTileInEditMode())
-        }
+        communalInteractor.widgetContent
+            // Clear the selected index when the list is updated.
+            .onEach { setSelectedIndex(null) }
+            .map { widgets -> widgets + listOf(CommunalContentModel.CtaTileInEditMode()) }
+
+    private val _reorderingWidgets = MutableStateFlow(false)
+
+    override val reorderingWidgets: StateFlow<Boolean>
+        get() = _reorderingWidgets
 
     override fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id)
 
@@ -135,14 +144,19 @@
     }
 
     override fun onReorderWidgetStart() {
+        // Clear selection status
+        setSelectedIndex(null)
+        _reorderingWidgets.value = true
         uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START)
     }
 
     override fun onReorderWidgetEnd() {
+        _reorderingWidgets.value = false
         uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_FINISH)
     }
 
     override fun onReorderWidgetCancel() {
+        _reorderingWidgets.value = false
         uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL)
     }
 }