[1/3] Add DragToBubbleController

Added DragToBubbleController class implementation to handle shell drags.

Bug: 411505605
Flag: com.android.wm.shell.enable_create_any_bubble
Test: tree hugger
Change-Id: Ia1a2b1dc7a16dde05deb3d66d3c122a3572ef38a
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt
index 8b50b9c..0d99a23 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt
@@ -693,10 +693,10 @@
 
     /** Bubble bar properties for generating a drop target. */
     interface BubbleBarPropertiesProvider {
-        fun getHeight(): Int
+        fun getHeight(): Int = 0
 
-        fun getWidth(): Int
+        fun getWidth(): Int = 0
 
-        fun getBottomPadding(): Int
+        fun getBottomPadding(): Int = 0
     }
 }
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DraggedObject.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DraggedObject.kt
index fdd2f57..b296ee5 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DraggedObject.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DraggedObject.kt
@@ -25,6 +25,7 @@
 
     data class ExpandedView(val initialLocation: BubbleBarLocation) : DraggedObject
 
+    // TODO(b/411505605) Remove onDropAction
     data class LauncherIcon(val bubbleBarHasBubbles: Boolean, val onDropAction: Runnable) :
         DraggedObject
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index e8975a6..cd4ad40 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -941,6 +941,16 @@
         }
     }
 
+    /**
+     * Show bubble bar pin view given location.
+     */
+    public void showBubbleBarPinAtLocation(BubbleBarLocation bubbleBarLocation) {
+        if (isShowingAsBubbleBar() && mBubbleStateListener != null) {
+            // TODO(b/411505605) show bubble bar drop target in launcher since taskbar window is
+            // layered on top of the shell drag window
+        }
+    }
+
     @Override
     public void onDragItemOverBubbleBarDragZone(@Nullable BubbleBarLocation bubbleBarLocation) {
         if (bubbleBarLocation == null) return;
@@ -1435,7 +1445,6 @@
      * Whether or not there are bubbles present, regardless of them being visible on the
      * screen (e.g. if on AOD).
      */
-    @VisibleForTesting
     public boolean hasBubbles() {
         if (mStackView == null && mLayerView == null) {
             return false;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/DragToBubbleController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/DragToBubbleController.kt
new file mode 100644
index 0000000..f770e86
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/DragToBubbleController.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.bubbles.bar
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.pm.ShortcutInfo
+import android.os.UserHandle
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import com.android.wm.shell.bubbles.BubbleController
+import com.android.wm.shell.bubbles.BubblePositioner
+import com.android.wm.shell.draganddrop.DragAndDropController.DragAndDropListener
+import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation
+import com.android.wm.shell.shared.bubbles.DragZone
+import com.android.wm.shell.shared.bubbles.DragZoneFactory
+import com.android.wm.shell.shared.bubbles.DragZoneFactory.BubbleBarPropertiesProvider
+import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker.SplitScreenMode
+import com.android.wm.shell.shared.bubbles.DraggedObject
+import com.android.wm.shell.shared.bubbles.DraggedObject.LauncherIcon
+import com.android.wm.shell.shared.bubbles.DropTargetManager
+
+/** Handles scenarios when launcher icon is being dragged to the bubble bar drop zones. */
+class DragToBubbleController(
+    val context: Context,
+    val bubblePositioner: BubblePositioner,
+    val bubbleController: BubbleController,
+) : DragAndDropListener {
+
+    private val containerView: FrameLayout =
+        FrameLayout(context).apply {
+            layoutParams =
+                FrameLayout.LayoutParams(
+                    FrameLayout.LayoutParams.MATCH_PARENT,
+                    FrameLayout.LayoutParams.MATCH_PARENT,
+                )
+        }
+
+    private val isRtl: Boolean
+        get() = containerView.isLayoutRtl
+
+    private val dropTargetManager: DropTargetManager =
+        DropTargetManager(context, containerView, createDragZoneListener())
+
+    private val dragZoneFactory = createDragZoneFactory()
+    private var lastDragZone: DragZone? = null
+
+    /** Returns the container view in which drop targets are added. */
+    fun getDropTargetContainer(): ViewGroup = containerView
+
+    /** Called when the drag is tarted. */
+    override fun onDragStarted() {
+        if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) {
+            return
+        }
+        val draggedObject = LauncherIcon(bubbleBarHasBubbles = true) {}
+        val dragZones = dragZoneFactory.createSortedDragZones(draggedObject)
+        dropTargetManager.onDragStarted(draggedObject, dragZones)
+    }
+
+    /**
+     * Called when drag position is updated.
+     *
+     * @return true if drag is over any bubble bar drop zones
+     */
+    fun onDragUpdate(x: Int, y: Int): Boolean {
+        lastDragZone = dropTargetManager.onDragUpdated(x, y)
+        return lastDragZone != null
+    }
+
+    /** Called when the item with the [ShortcutInfo] is dropped over the bubble bar drop target. */
+    fun onItemDropped(shortcutInfo: ShortcutInfo) {
+        val dropLocation = lastDragZone?.getBubbleBarLocation() ?: return
+        bubbleController.expandStackAndSelectBubble(shortcutInfo, dropLocation)
+    }
+
+    /**
+     * Called when the item with the [PendingIntent] and the [UserHandle] is dropped over the
+     * bubble bar drop target.
+     */
+    fun onItemDropped(pendingIntent: PendingIntent, userHandle: UserHandle) {
+        val dropLocation = lastDragZone?.getBubbleBarLocation() ?: return
+        bubbleController.expandStackAndSelectBubble(pendingIntent, userHandle, dropLocation)
+    }
+
+    /** Called when the drag is ended. */
+    override fun onDragEnded() {
+        dropTargetManager.onDragEnded()
+    }
+
+    private fun createDragZoneFactory(): DragZoneFactory {
+        return DragZoneFactory(
+            context,
+            bubblePositioner.currentConfig,
+            { SplitScreenMode.UNSUPPORTED },
+            { false },
+            object : BubbleBarPropertiesProvider {},
+        )
+    }
+
+    private fun createDragZoneListener() = object : DropTargetManager.DragZoneChangedListener {
+
+            private var lastUpdateLocation: BubbleBarLocation? = null
+            private val isLocationChangedFromOriginal: Boolean
+                get() = lastUpdateLocation != null
+                        && isDifferentSides(lastUpdateLocation, bubbleController.bubbleBarLocation)
+
+            override fun onInitialDragZoneSet(dragZone: DragZone?) {}
+
+            override fun onDragZoneChanged(
+                draggedObject: DraggedObject,
+                from: DragZone?,
+                to: DragZone?,
+            ) {
+                val updateLocation = getBarLocation(to)
+                updateBubbleBarLocation(updateLocation)
+                lastUpdateLocation = updateLocation
+            }
+
+            override fun onDragEnded(zone: DragZone?) {
+                updateBubbleBarLocation(updateLocation = null)
+            }
+
+            fun updateBubbleBarLocation(updateLocation: BubbleBarLocation?) {
+                val updatedBefore = lastUpdateLocation != null
+                val originalLocation = bubbleController.bubbleBarLocation
+                val isLocationUpdated = isDifferentSides(lastUpdateLocation, updateLocation)
+                if (!bubbleController.hasBubbles()) {
+                    // has no bubbles, so showing the pin view
+                    if (updateLocation == null || !updatedBefore || isLocationUpdated) {
+                        bubbleController.showBubbleBarPinAtLocation(updateLocation)
+                    }
+                    return
+                }
+                if (updateLocation == null && isLocationChangedFromOriginal) {
+                    bubbleController.animateBubbleBarLocation(originalLocation)
+                    return
+                }
+                if (updatedBefore && isLocationUpdated) {
+                    // updated before and location updated - update to new location
+                    bubbleController.animateBubbleBarLocation(updateLocation)
+                    return
+                }
+                if (!updatedBefore && isDifferentSides(originalLocation, updateLocation)) {
+                    // not updated before and location changed from original
+                    bubbleController.animateBubbleBarLocation(updateLocation)
+                }
+            }
+
+            fun getBarLocation(dragZone: DragZone?): BubbleBarLocation? {
+                return when (dragZone) {
+                    is DragZone.Bubble.Left -> BubbleBarLocation.LEFT
+                    is DragZone.Bubble.Right -> BubbleBarLocation.RIGHT
+                    else -> null
+                }
+            }
+
+            fun isDifferentSides(f: BubbleBarLocation?, s: BubbleBarLocation?): Boolean {
+                return f != null && s != null && f.isOnLeft(isRtl) != s.isOnLeft(isRtl)
+            }
+        }
+
+    private fun DragZone.getBubbleBarLocation(): BubbleBarLocation? =
+        when (this) {
+            is DragZone.Bubble.Left -> BubbleBarLocation.LEFT
+            is DragZone.Bubble.Right -> BubbleBarLocation.RIGHT
+            else -> null
+        }
+}