Fix several issues with hub scrolling during widget drag-and-drop.

Bug: 347293340
Bug: 346328875
Test: Maunally by dragging widgets to the hub from the widget picker.
Flag: com.android.systemui.communal_hub

Change-Id: Ic2d9736e23d5b2ad9a1c316a9a3ee204320ca320
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 bd4710b..9f35fcc 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
@@ -509,7 +509,6 @@
                 gridState = gridState,
                 contentListState = contentListState,
                 contentOffset = contentOffset,
-                updateDragPositionForRemove = updateDragPositionForRemove
             )
 
         // A full size box in background that listens to widget drops from the picker.
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt
index 9e6f22a..0c29394 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/DragAndDropTargetState.kt
@@ -18,17 +18,13 @@
 
 import android.content.ClipDescription
 import android.view.DragEvent
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.draganddrop.dragAndDropTarget
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.scrollBy
-import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
 import androidx.compose.foundation.lazy.grid.LazyGridState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.rememberUpdatedState
@@ -45,8 +41,7 @@
 import com.android.systemui.communal.util.WidgetPickerIntentUtils
 import com.android.systemui.communal.util.WidgetPickerIntentUtils.getWidgetExtraFromIntent
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.isActive
+import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.launch
 
 /**
@@ -59,32 +54,22 @@
     gridState: LazyGridState,
     contentOffset: Offset,
     contentListState: ContentListState,
-    updateDragPositionForRemove: (offset: Offset) -> Boolean,
 ): DragAndDropTargetState {
     val scope = rememberCoroutineScope()
-    val autoScrollSpeed = remember { mutableFloatStateOf(0f) }
-    // Threshold of distance from edges that should start auto-scroll - chosen to be a narrow value
-    // that allows differentiating intention of scrolling from intention of dragging over the first
-    // visible item.
     val autoScrollThreshold = with(LocalDensity.current) { 60.dp.toPx() }
     val state =
-        remember(gridState, contentListState) {
+        remember(gridState, contentOffset, contentListState, autoScrollThreshold, scope) {
             DragAndDropTargetState(
                 state = gridState,
                 contentOffset = contentOffset,
                 contentListState = contentListState,
-                scope = scope,
-                autoScrollSpeed = autoScrollSpeed,
                 autoScrollThreshold = autoScrollThreshold,
-                updateDragPositionForRemove = updateDragPositionForRemove,
+                scope = scope,
             )
         }
-    LaunchedEffect(autoScrollSpeed.floatValue) {
-        if (autoScrollSpeed.floatValue != 0f) {
-            while (isActive) {
-                gridState.scrollBy(autoScrollSpeed.floatValue)
-                delay(10)
-            }
+    LaunchedEffect(state) {
+        for (diff in state.scrollChannel) {
+            gridState.scrollBy(diff)
         }
     }
     return state
@@ -96,7 +81,6 @@
  * @see androidx.compose.foundation.draganddrop.dragAndDropTarget
  * @see DragEvent
  */
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
 internal fun Modifier.dragAndDropTarget(
     dragDropTargetState: DragAndDropTargetState,
@@ -122,6 +106,10 @@
                         return state.onDrop(event)
                     }
 
+                    override fun onExited(event: DragAndDropEvent) {
+                        state.onExited()
+                    }
+
                     override fun onEnded(event: DragAndDropEvent) {
                         state.onEnded()
                     }
@@ -149,19 +137,17 @@
     private val state: LazyGridState,
     private val contentOffset: Offset,
     private val contentListState: ContentListState,
-    private val scope: CoroutineScope,
-    private val autoScrollSpeed: MutableState<Float>,
     private val autoScrollThreshold: Float,
-    private val updateDragPositionForRemove: (offset: Offset) -> Boolean,
+    private val scope: CoroutineScope,
 ) {
     /**
      * The placeholder item that is treated as if it is being dragged across the grid. It is added
      * to grid once drag and drop event is started and removed when event ends.
      */
     private var placeHolder = CommunalContentModel.WidgetPlaceholder()
-
     private var placeHolderIndex: Int? = null
-    private var isOnRemoveButton = false
+
+    internal val scrollChannel = Channel<Float>()
 
     fun onStarted() {
         // assume item will be added to the end.
@@ -170,39 +156,39 @@
     }
 
     fun onMoved(event: DragAndDropEvent) {
-        val dragEvent = event.toAndroidDragEvent()
-        isOnRemoveButton = updateDragPositionForRemove(Offset(dragEvent.x, dragEvent.y))
-        if (!isOnRemoveButton) {
-            findTargetItem(dragEvent)?.apply {
-                var scrollIndex: Int? = null
-                var scrollOffset: Int? = null
-                if (placeHolderIndex == state.firstVisibleItemIndex) {
-                    // Save info about the first item before the move, to neutralize the automatic
-                    // keeping first item first.
-                    scrollIndex = placeHolderIndex
-                    scrollOffset = state.firstVisibleItemScrollOffset
-                }
+        val dragOffset = event.toOffset()
 
-                autoScrollIfNearEdges(dragEvent)
+        val targetItem =
+            state.layoutInfo.visibleItemsInfo
+                .asSequence()
+                .filter { item -> contentListState.isItemEditable(item.index) }
+                .firstItemAtOffset(dragOffset - contentOffset)
 
-                if (contentListState.isItemEditable(this.index)) {
-                    movePlaceholderTo(this.index)
-                    placeHolderIndex = this.index
-                }
-
-                if (scrollIndex != null && scrollOffset != null) {
-                    // this is needed to neutralize automatic keeping the first item first.
-                    scope.launch { state.scrollToItem(scrollIndex, scrollOffset) }
-                }
+        if (targetItem != null) {
+            var scrollIndex: Int? = null
+            var scrollOffset: Int? = null
+            if (placeHolderIndex == state.firstVisibleItemIndex) {
+                // Save info about the first item before the move, to neutralize the automatic
+                // keeping first item first.
+                scrollIndex = placeHolderIndex
+                scrollOffset = state.firstVisibleItemScrollOffset
             }
+
+            if (contentListState.isItemEditable(targetItem.index)) {
+                movePlaceholderTo(targetItem.index)
+                placeHolderIndex = targetItem.index
+            }
+
+            if (scrollIndex != null && scrollOffset != null) {
+                // this is needed to neutralize automatic keeping the first item first.
+                scope.launch { state.scrollToItem(scrollIndex, scrollOffset) }
+            }
+        } else {
+            computeAutoscroll(dragOffset).takeIf { it != 0f }?.let { scrollChannel.trySend(it) }
         }
     }
 
     fun onDrop(event: DragAndDropEvent): Boolean {
-        autoScrollSpeed.value = 0f
-        if (isOnRemoveButton) {
-            return false
-        }
         return placeHolderIndex?.let { dropIndex ->
             val widgetExtra = event.maybeWidgetExtra() ?: return false
             val (componentName, user) = widgetExtra
@@ -221,39 +207,35 @@
     }
 
     fun onEnded() {
-        autoScrollSpeed.value = 0f
         placeHolderIndex = null
         contentListState.list.remove(placeHolder)
-        isOnRemoveButton = updateDragPositionForRemove(Offset.Zero)
     }
 
-    private fun autoScrollIfNearEdges(dragEvent: DragEvent) {
+    fun onExited() {
+        onEnded()
+    }
+
+    private fun computeAutoscroll(dragOffset: Offset): Float {
         val orientation = state.layoutInfo.orientation
         val distanceFromStart =
             if (orientation == Orientation.Horizontal) {
-                dragEvent.x
+                dragOffset.x
             } else {
-                dragEvent.y
+                dragOffset.y
             }
         val distanceFromEnd =
             if (orientation == Orientation.Horizontal) {
-                state.layoutInfo.viewportSize.width - dragEvent.x
+                state.layoutInfo.viewportEndOffset - dragOffset.x
             } else {
-                state.layoutInfo.viewportSize.height - dragEvent.y
+                state.layoutInfo.viewportEndOffset - dragOffset.y
             }
-        autoScrollSpeed.value =
-            when {
-                distanceFromEnd < autoScrollThreshold -> autoScrollThreshold - distanceFromEnd
-                distanceFromStart < autoScrollThreshold ->
-                    -(autoScrollThreshold - distanceFromStart)
-                else -> 0f
-            }
-    }
 
-    private fun findTargetItem(dragEvent: DragEvent): LazyGridItemInfo? =
-        state.layoutInfo.visibleItemsInfo.firstItemAtOffset(
-            Offset(dragEvent.x, dragEvent.y) - contentOffset
-        )
+        return when {
+            distanceFromEnd < autoScrollThreshold -> autoScrollThreshold - distanceFromEnd
+            distanceFromStart < autoScrollThreshold -> distanceFromStart - autoScrollThreshold
+            else -> 0f
+        }
+    }
 
     private fun movePlaceholderTo(index: Int) {
         val currentIndex = contentListState.list.indexOf(placeHolder)
@@ -271,4 +253,6 @@
         val clipData = this.toAndroidDragEvent().clipData.takeIf { it.itemCount != 0 }
         return clipData?.getItemAt(0)?.intent?.let { intent -> getWidgetExtraFromIntent(intent) }
     }
+
+    private fun DragAndDropEvent.toOffset() = this.toAndroidDragEvent().run { Offset(x, y) }
 }