Merge "Add myself to launcher OWNERS" into main
diff --git a/quickstep/res/drawable/bg_bubble_expanded_view_drop_target.xml b/quickstep/res/drawable/bg_bubble_expanded_view_drop_target.xml
new file mode 100644
index 0000000..98aab67
--- /dev/null
+++ b/quickstep/res/drawable/bg_bubble_expanded_view_drop_target.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:shape="rectangle">
+    <corners android:radius="@dimen/bubble_expanded_view_drop_target_corner_radius" />
+    <solid android:color="@color/bubblebar_drop_target_bg_color" />
+    <stroke
+        android:width="1dp"
+        android:color="?androidprv:attr/materialColorPrimaryContainer" />
+</shape>
diff --git a/quickstep/res/layout/bubble_expanded_view_drop_target.xml b/quickstep/res/layout/bubble_expanded_view_drop_target.xml
new file mode 100644
index 0000000..15ec49a
--- /dev/null
+++ b/quickstep/res/layout/bubble_expanded_view_drop_target.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+
+<!-- TODO(b/330585402): replace 600dp height with calculated value -->
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="@dimen/bubble_expanded_view_drop_target_width"
+    android:layout_height="600dp"
+    android:layout_margin="@dimen/bubble_expanded_view_drop_target_margin"
+    android:background="@drawable/bg_bubble_expanded_view_drop_target"
+    android:elevation="@dimen/bubblebar_elevation" />
\ No newline at end of file
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index b862d7c..c5f25ad 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -456,6 +456,9 @@
 
     <!-- Bubble bar drop target -->
     <dimen name="bubblebar_drop_target_corner_radius">36dp</dimen>
+    <dimen name="bubble_expanded_view_drop_target_corner_radius">16dp</dimen>
+    <dimen name="bubble_expanded_view_drop_target_width">412dp</dimen>
+    <dimen name="bubble_expanded_view_drop_target_margin">16dp</dimen>
 
     <!-- Launcher splash screen -->
     <!-- Note: keep this value in sync with the WindowManager/Shell dimens.xml -->
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index a4b6ad0..e5396ee 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -106,6 +106,7 @@
 import com.android.launcher3.taskbar.bubbles.BubbleControllers;
 import com.android.launcher3.taskbar.bubbles.BubbleDismissController;
 import com.android.launcher3.taskbar.bubbles.BubbleDragController;
+import com.android.launcher3.taskbar.bubbles.BubblePinController;
 import com.android.launcher3.taskbar.bubbles.BubbleStashController;
 import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController;
 import com.android.launcher3.taskbar.navbutton.NearestTouchFrame;
@@ -259,6 +260,8 @@
                     new BubbleDragController(this),
                     new BubbleDismissController(this, mDragLayer),
                     new BubbleBarPinController(this, mDragLayer,
+                            () -> getDeviceProfile().getDisplayInfo().currentSize),
+                    new BubblePinController(this, mDragLayer,
                             () -> getDeviceProfile().getDisplayInfo().currentSize)
             ));
         }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
index ec47c4f..90c3ea7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
@@ -131,21 +131,15 @@
 
         // Draw background.
         val radius = backgroundHeight / 2f
-        val left = if (anchorLeft) 0f else bounds.width().toFloat() - width
-        val right = if (anchorLeft) width else bounds.width().toFloat()
-        canvas.drawRoundRect(
-            left,
-            pointerVisibleHeight,
-            right,
-            bounds.height().toFloat(),
-            radius,
-            radius,
-            paint
-        )
+        val left = bounds.left + (if (anchorLeft) 0f else bounds.width().toFloat() - width)
+        val right = bounds.left + (if (anchorLeft) width else bounds.width().toFloat())
+        val top = bounds.top + pointerVisibleHeight
+        val bottom = bounds.top + bounds.height().toFloat()
+        canvas.drawRoundRect(left, top, right, bottom, radius, radius, paint)
 
         if (showingArrow) {
             // Draw arrow.
-            val transX = arrowPositionX - pointerWidth / 2f
+            val transX = bounds.left + arrowPositionX - pointerWidth / 2f
             canvas.translate(transX, 0f)
             arrowDrawable.draw(canvas)
         }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 60e8abe..de93ba5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.taskbar.bubbles;
 
 import static com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
 
 import android.animation.Animator;
@@ -29,6 +30,7 @@
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.util.AttributeSet;
+import android.util.FloatProperty;
 import android.util.LayoutDirection;
 import android.util.Log;
 import android.view.Gravity;
@@ -79,6 +81,7 @@
     // TODO: (b/273594744) calculate the amount of space we have and base the max on that
     //  if it's smaller than 5.
     private static final int MAX_BUBBLES = 5;
+    private static final int MAX_VISIBLE_BUBBLES_COLLAPSED = 2;
     private static final int ARROW_POSITION_ANIMATION_DURATION_MS = 200;
     private static final int WIDTH_ANIMATION_DURATION_MS = 200;
 
@@ -94,6 +97,40 @@
     // During fade in animation we shift the bubble bar 1/60th of the screen width
     private static final float FADE_IN_ANIM_POSITION_SHIFT = 1 / 60f;
 
+    /**
+     * Custom property to set translationX value for the bar view while a bubble is being dragged.
+     * Skips applying translation to the dragged bubble.
+     */
+    private static final FloatProperty<BubbleBarView> BUBBLE_DRAG_TRANSLATION_X =
+            new FloatProperty<>("bubbleDragTranslationX") {
+                @Override
+                public void setValue(BubbleBarView bubbleBarView, float translationX) {
+                    bubbleBarView.setTranslationXDuringBubbleDrag(translationX);
+                }
+
+                @Override
+                public Float get(BubbleBarView bubbleBarView) {
+                    return bubbleBarView.mTranslationXDuringDrag;
+                }
+            };
+
+    /**
+     * Custom property to set alpha value for the bar view while a bubble is being dragged.
+     * Skips applying alpha to the dragged bubble.
+     */
+    private static final FloatProperty<BubbleBarView> BUBBLE_DRAG_ALPHA =
+            new FloatProperty<>("bubbleDragAlpha") {
+                @Override
+                public void setValue(BubbleBarView bubbleBarView, float alpha) {
+                    bubbleBarView.setAlphaDuringBubbleDrag(alpha);
+                }
+
+                @Override
+                public Float get(BubbleBarView bubbleBarView) {
+                    return bubbleBarView.mAlphaDuringDrag;
+                }
+            };
+
     private final BubbleBarBackground mBubbleBarBackground;
 
     private boolean mIsAnimatingNewBubble = false;
@@ -117,6 +154,8 @@
     private final float mDragElevation;
     private final int mPointerSize;
 
+    private final Rect mTempBackgroundBounds = new Rect();
+
     // Whether the bar is expanded (i.e. the bubble activity is being displayed).
     private boolean mIsBarExpanded = false;
     // The currently selected bubble view.
@@ -150,6 +189,8 @@
 
     @Nullable
     private BubbleView mDraggedBubbleView;
+    private float mTranslationXDuringDrag = 0f;
+    private float mAlphaDuringDrag = 0f;
 
     private int mPreviousLayoutDirection = LayoutDirection.UNDEFINED;
 
@@ -259,8 +300,10 @@
         setPivotX(mRelativePivotX * getWidth());
         setPivotY(mRelativePivotY * getHeight());
 
-        // Position the views
-        updateChildrenRenderNodeProperties(mBubbleBarLocation);
+        if (!mDragging) {
+            // Position the views when not dragging
+            updateChildrenRenderNodeProperties(mBubbleBarLocation);
+        }
     }
 
     @Override
@@ -302,17 +345,15 @@
             mBubbleBarLocationAnimator.cancel();
             mBubbleBarLocationAnimator = null;
         }
-        setTranslationX(0f);
-        setAlpha(1f);
+        resetDragAnimation();
         if (bubbleBarLocation != mBubbleBarLocation) {
             mBubbleBarLocation = bubbleBarLocation;
             onBubbleBarLocationChanged();
-            invalidate();
         }
     }
 
     /**
-     * Set whether this view is being currently being dragged
+     * Set whether this view is currently being dragged
      */
     public void setIsDragging(boolean dragging) {
         if (mDragging == dragging) {
@@ -326,7 +367,7 @@
      * Get translation for bubble bar when drag is released and it needs to animate back to the
      * resting position.
      * Resting position is based on the supplied location. If the supplied location is different
-     * from the internal location that was used to lay out the bubble bar, translation values are
+     * from the internal location that was used during bubble bar layout, translation values are
      * calculated to position the bar at the desired location.
      *
      * @param initialTranslation initial bubble bar translation at the start of drag
@@ -353,6 +394,30 @@
         return dragEndTranslation;
     }
 
+    /**
+     * Get translation for a bubble when drag is released and it needs to animate back to the
+     * resting position.
+     * Resting position is based on the supplied location. If the supplied location is different
+     * from the internal location that was used during bubble bar layout, translation values are
+     * calculated to position the bar at the desired location.
+     *
+     * @param initialTranslation initial bubble bar translation at the start of drag
+     * @param location           desired location of the bubble bar when drag is released
+     * @return point with x and y values representing translation on x and y-axis
+     */
+    public PointF getDraggedBubbleReleaseTranslation(PointF initialTranslation,
+            BubbleBarLocation location) {
+        // Start with bubble bar translation
+        final PointF dragEndTranslation = new PointF(
+                getBubbleBarDragReleaseTranslation(initialTranslation, location));
+        // Apply individual bubble translation, as the order may have changed
+        int viewIndex = indexOfChild(mDraggedBubbleView);
+        dragEndTranslation.x += getExpandedBubbleTranslationX(viewIndex,
+                getChildCount(),
+                location.isOnLeft(isLayoutRtl()));
+        return dragEndTranslation;
+    }
+
     private float getDistanceFromOtherSide() {
         // Calculate the shift needed to position the bubble bar on the other side
         int displayWidth = getResources().getDisplayMetrics().widthPixels;
@@ -393,17 +458,18 @@
         mBubbleBarLocationAnimator.start();
     }
 
-    private Animator getLocationUpdateFadeOutAnimator(BubbleBarLocation bubbleBarLocation) {
+    private Animator getLocationUpdateFadeOutAnimator(BubbleBarLocation newLocation) {
+        final FloatProperty<? super BubbleBarView> txProp = getLocationAnimTranslationXProperty();
         final float shift =
                 getResources().getDisplayMetrics().widthPixels * FADE_OUT_ANIM_POSITION_SHIFT;
-        final boolean onLeft = bubbleBarLocation.isOnLeft(isLayoutRtl());
-        final float tx = getTranslationX() + (onLeft ? shift : -shift);
+        final boolean onLeft = newLocation.isOnLeft(isLayoutRtl());
+        final float tx = txProp.get(this) + (onLeft ? -shift : shift);
 
-        ObjectAnimator positionAnim = ObjectAnimator.ofFloat(this, TRANSLATION_X, tx)
-                .setDuration(FADE_OUT_ANIM_POSITION_DURATION_MS);
+        ObjectAnimator positionAnim = ObjectAnimator.ofFloat(this, txProp, tx).setDuration(
+                FADE_OUT_ANIM_POSITION_DURATION_MS);
         positionAnim.setInterpolator(EMPHASIZED_ACCELERATE);
 
-        ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(this, ALPHA, 0f)
+        ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(this, getLocationAnimAlphaProperty(), 0f)
                 .setDuration(FADE_OUT_ANIM_ALPHA_DURATION_MS);
         alphaAnim.setStartDelay(FADE_OUT_ANIM_ALPHA_DELAY_MS);
 
@@ -412,14 +478,14 @@
         return animatorSet;
     }
 
-    private Animator getLocationUpdateFadeInAnimator(BubbleBarLocation animatedLocation) {
+    private Animator getLocationUpdateFadeInAnimator(BubbleBarLocation newLocation) {
         final float shift =
                 getResources().getDisplayMetrics().widthPixels * FADE_IN_ANIM_POSITION_SHIFT;
 
-        final boolean onLeft = animatedLocation.isOnLeft(isLayoutRtl());
+        final boolean onLeft = newLocation.isOnLeft(isLayoutRtl());
         final float startTx;
         final float finalTx;
-        if (animatedLocation == mBubbleBarLocation) {
+        if (newLocation == mBubbleBarLocation) {
             // Animated location matches layout location.
             finalTx = 0;
         } else {
@@ -439,9 +505,9 @@
                 .setEndValue(finalTx)
                 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
                 .setStiffness(FADE_IN_ANIM_POSITION_SPRING_STIFFNESS)
-                .build(this, VIEW_TRANSLATE_X);
+                .build(this, getLocationAnimTranslationXProperty());
 
-        ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(this, ALPHA, 1f)
+        ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(this, getLocationAnimAlphaProperty(), 1f)
                 .setDuration(FADE_IN_ANIM_ALPHA_DURATION_MS);
 
         AnimatorSet animatorSet = new AnimatorSet();
@@ -450,6 +516,84 @@
     }
 
     /**
+     * Get property that can be used to animate the translation-x value for the bar.
+     * When a bubble is being dragged, uses {@link #BUBBLE_DRAG_TRANSLATION_X}.
+     * Falls back to {@link com.android.launcher3.LauncherAnimUtils#VIEW_TRANSLATE_X} otherwise.
+     */
+    private FloatProperty<? super BubbleBarView> getLocationAnimTranslationXProperty() {
+        return mDraggedBubbleView == null ? VIEW_TRANSLATE_X : BUBBLE_DRAG_TRANSLATION_X;
+    }
+
+    /**
+     * Get property that can be used to animate the alpha value for the bar.
+     * When a bubble is being dragged, uses {@link #BUBBLE_DRAG_ALPHA}.
+     * Falls back to {@link com.android.launcher3.LauncherAnimUtils#VIEW_ALPHA} otherwise.
+     */
+    private FloatProperty<? super BubbleBarView> getLocationAnimAlphaProperty() {
+        return mDraggedBubbleView == null ? VIEW_ALPHA : BUBBLE_DRAG_ALPHA;
+    }
+
+    /**
+     * Set translation-x value for the bar while a bubble is being dragged.
+     * We can not update translation on the bar directly because the dragged bubble would be
+     * affected as well. As it is a child view.
+     * Instead, while a bubble is being dragged, set translation on each child view, that is not the
+     * dragged view. And set a translation on the background.
+     * This allows for the dragged bubble view to remain in position while the bar moves during
+     * animation.
+     */
+    private void setTranslationXDuringBubbleDrag(float translationX) {
+        mTranslationXDuringDrag = translationX;
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            BubbleView view = (BubbleView) getChildAt(i);
+            if (view != mDraggedBubbleView) {
+                view.setBubbleBarTranslationX(translationX);
+            }
+        }
+        if (mBubbleBarBackground != null) {
+            mTempBackgroundBounds.set(mBubbleBarBackground.getBounds());
+            mTempBackgroundBounds.offsetTo((int) translationX, 0);
+            mBubbleBarBackground.setBounds(mTempBackgroundBounds);
+        }
+    }
+
+    /**
+     * Set alpha value for the bar while a bubble is being dragged.
+     * We can not update the alpha on the bar directly because the dragged bubble would be affected
+     * as well. As it is a child view.
+     * Instead, while a bubble is being dragged, set alpha on each child view, that is not the
+     * dragged view. And set an alpha on the background.
+     * This allows for the dragged bubble to remain visible while the bar is hidden during
+     * animation.
+     */
+    private void setAlphaDuringBubbleDrag(float alpha) {
+        mAlphaDuringDrag = alpha;
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View view = getChildAt(i);
+            if (view != mDraggedBubbleView) {
+                view.setAlpha(alpha);
+            }
+        }
+        if (mBubbleBarBackground != null) {
+            mBubbleBarBackground.setAlpha((int) (255 * alpha));
+        }
+    }
+
+    private void resetDragAnimation() {
+        if (mBubbleBarLocationAnimator != null) {
+            mBubbleBarLocationAnimator.removeAllListeners();
+            mBubbleBarLocationAnimator.cancel();
+            mBubbleBarLocationAnimator = null;
+        }
+        setTranslationXDuringBubbleDrag(0f);
+        setAlphaDuringBubbleDrag(1f);
+        setTranslationX(0f);
+        setAlpha(1f);
+    }
+
+    /**
      * Updates the bounds with translation that may have been applied and returns the result.
      */
     public Rect getBubbleBarBounds() {
@@ -558,30 +702,21 @@
         float elevationState = (1 - widthState);
         for (int i = 0; i < bubbleCount; i++) {
             BubbleView bv = (BubbleView) getChildAt(i);
+            if (bv == mDraggedBubbleView) {
+                // Skip the dragged bubble. Its translation is managed by the drag controller.
+                continue;
+            }
             bv.setTranslationY(ty);
 
             // the position of the bubble when the bar is fully expanded
-            final float expandedX;
+            final float expandedX = getExpandedBubbleTranslationX(i, bubbleCount, onLeft);
             // the position of the bubble when the bar is fully collapsed
-            final float collapsedX;
-            if (onLeft) {
-                // If bar is on the left, bubbles are ordered right to left
-                expandedX = (bubbleCount - i - 1) * (mIconSize + mExpandedBarIconsSpacing);
-                // Shift the first bubble only if there are more bubbles in addition to overflow
-                collapsedX = i == 0 && bubbleCount > 2 ? mIconOverlapAmount : 0;
-            } else {
-                // Bubbles ordered left to right, don't move the first bubble
-                expandedX = i * (mIconSize + mExpandedBarIconsSpacing);
-                collapsedX = i == 0 ? 0 : mIconOverlapAmount;
-            }
-            if (bv == mDraggedBubbleView) {
-                // if bubble is dragged set the elevation to bubble drag elevation
-                bv.setZ(mDragElevation);
-            } else {
-                // otherwise slowly animate elevation while keeping correct Z ordering
-                float fullElevationForChild = (MAX_BUBBLES * mBubbleElevation) - i;
-                bv.setZ(fullElevationForChild * elevationState);
-            }
+            final float collapsedX = getCollapsedBubbleTranslationX(i, bubbleCount, onLeft);
+
+            // slowly animate elevation while keeping correct Z ordering
+            float fullElevationForChild = (MAX_BUBBLES * mBubbleElevation) - i;
+            bv.setZ(fullElevationForChild * elevationState);
+
             if (mIsBarExpanded) {
                 // If bar is on the right, account for bubble bar expanding and shifting left
                 final float expandedBarShift = onLeft ? 0 : currentWidth - expandedWidth;
@@ -601,9 +736,10 @@
                 // If we're fully collapsed, hide all bubbles except for the first 2. If there are
                 // only 2 bubbles, hide the second bubble as well because it's the overflow.
                 if (widthState == 0) {
-                    if (i > 1) {
+                    if (i > MAX_VISIBLE_BUBBLES_COLLAPSED - 1) {
                         bv.setAlpha(0);
-                    } else if (i == 1 && bubbleCount == 2) {
+                    } else if (i == MAX_VISIBLE_BUBBLES_COLLAPSED - 1
+                            && bubbleCount == MAX_VISIBLE_BUBBLES_COLLAPSED) {
                         bv.setAlpha(0);
                     }
                 }
@@ -636,6 +772,34 @@
         mBubbleBarBackground.setWidth(interpolatedWidth);
     }
 
+    private float getExpandedBubbleTranslationX(int bubbleIndex, int bubbleCount,
+            boolean onLeft) {
+        if (bubbleIndex < 0 || bubbleIndex >= bubbleCount) {
+            return 0;
+        }
+        if (onLeft) {
+            // If bar is on the left, bubbles are ordered right to left
+            return (bubbleCount - bubbleIndex - 1) * (mIconSize + mExpandedBarIconsSpacing);
+        } else {
+            // Bubbles ordered left to right, don't move the first bubble
+            return bubbleIndex * (mIconSize + mExpandedBarIconsSpacing);
+        }
+    }
+
+    private float getCollapsedBubbleTranslationX(int bubbleIndex, int bubbleCount,
+            boolean onLeft) {
+        if (bubbleIndex < 0 || bubbleIndex >= bubbleCount) {
+            return 0;
+        }
+        if (onLeft) {
+            // Shift the first bubble only if there are more bubbles in addition to overflow
+            return bubbleIndex == 0 && bubbleCount > MAX_VISIBLE_BUBBLES_COLLAPSED
+                    ? mIconOverlapAmount : 0;
+        } else {
+            return bubbleIndex == 0 ? 0 : mIconOverlapAmount;
+        }
+    }
+
     /**
      * Reorders the views to match the provided list.
      */
@@ -685,8 +849,18 @@
      * Sets the dragged bubble view to correctly apply Z order. Dragged view should appear on top
      */
     public void setDraggedBubble(@Nullable BubbleView view) {
+        if (mDraggedBubbleView != null) {
+            mDraggedBubbleView.setZ(0);
+            if (view == null) {
+                // We are clearing the dragged bubble, reset drag
+                resetDragAnimation();
+            }
+        }
         mDraggedBubbleView = view;
-        requestLayout();
+        if (view != null) {
+            view.setZ(mDragElevation);
+        }
+        setIsDragging(view != null);
     }
 
     /**
@@ -745,7 +919,7 @@
         if (bubbleBarLocation.isOnLeft(isLayoutRtl())) {
             // Bubble positions are reversed. First bubble may be shifted, if there are more
             // bubbles than the current bubble and overflow.
-            bubblePosition = index == 0 && getChildCount() > 2 ? 1 : 0;
+            bubblePosition = index == 0 && getChildCount() > MAX_VISIBLE_BUBBLES_COLLAPSED ? 1 : 0;
         } else {
             bubblePosition = index;
         }
@@ -807,7 +981,7 @@
         final int horizontalPadding = getPaddingStart() + getPaddingEnd();
         // If there are more than 2 bubbles, the first 2 should be visible when collapsed.
         // Otherwise just the first bubble should be visible because we don't show the overflow.
-        return childCount > 2
+        return childCount > MAX_VISIBLE_BUBBLES_COLLAPSED
                 ? mIconSize + mIconOverlapAmount + horizontalPadding
                 : mIconSize + horizontalPadding;
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index dc48a66..95dd24b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -20,6 +20,7 @@
 
 import android.content.res.Resources;
 import android.graphics.Point;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.util.DisplayMetrics;
 import android.util.Log;
@@ -468,7 +469,8 @@
      */
     public void onDragStart(@NonNull BubbleView bubbleView) {
         if (bubbleView.getBubble() == null) return;
-        mSystemUiProxy.onBubbleDrag(bubbleView.getBubble().getKey(), /* isBeingDragged = */ true);
+
+        mSystemUiProxy.startBubbleDrag(bubbleView.getBubble().getKey());
         mBarView.setDraggedBubble(bubbleView);
     }
 
@@ -476,19 +478,46 @@
      * Notifies SystemUI to expand the selected bubble when the bubble is released.
      * @param bubbleView dragged bubble view
      */
-    public void onDragRelease(@NonNull BubbleView bubbleView) {
+    public void onDragRelease(@NonNull BubbleView bubbleView, BubbleBarLocation location) {
         if (bubbleView.getBubble() == null) return;
-        mSystemUiProxy.onBubbleDrag(bubbleView.getBubble().getKey(), /* isBeingDragged = */ false);
+        // TODO(b/330585402): send new bubble bar bounds to shell for the animation
+        mSystemUiProxy.stopBubbleDrag(bubbleView.getBubble().getKey(), location);
     }
 
     /**
-     * Removes the dragged bubble view in the bubble bar view
+     * Notifies {@link BubbleBarView} that drag and all animations are finished.
      */
     public void onDragEnd() {
         mBarView.setDraggedBubble(null);
     }
 
     /**
+     * Get translation for bubble bar when drag is released.
+     *
+     * @see BubbleBarView#getBubbleBarDragReleaseTranslation(PointF, BubbleBarLocation)
+     */
+    public PointF getBubbleBarDragReleaseTranslation(PointF initialTranslation,
+            BubbleBarLocation location) {
+        if (location == mBarView.getBubbleBarLocation()) {
+            return initialTranslation;
+        }
+        return mBarView.getBubbleBarDragReleaseTranslation(initialTranslation, location);
+    }
+
+    /**
+     * Get translation for bubble view when drag is released.
+     *
+     * @see BubbleBarView#getDraggedBubbleReleaseTranslation(PointF, BubbleBarLocation)
+     */
+    public PointF getDraggedBubbleReleaseTranslation(PointF initialTranslation,
+            BubbleBarLocation location) {
+        if (location == mBarView.getBubbleBarLocation()) {
+            return initialTranslation;
+        }
+        return mBarView.getDraggedBubbleReleaseTranslation(initialTranslation, location);
+    }
+
+    /**
      * Called when bubble was dragged into the dismiss target. Notifies System
      * @param bubble dismissed bubble item
      */
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
index 90f1be3..295477c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
@@ -30,6 +30,7 @@
     public final BubbleDragController bubbleDragController;
     public final BubbleDismissController bubbleDismissController;
     public final BubbleBarPinController bubbleBarPinController;
+    public final BubblePinController bubblePinController;
 
     private final RunnableList mPostInitRunnables = new RunnableList();
 
@@ -45,7 +46,8 @@
             BubbleStashedHandleViewController bubbleStashedHandleViewController,
             BubbleDragController bubbleDragController,
             BubbleDismissController bubbleDismissController,
-            BubbleBarPinController bubbleBarPinController) {
+            BubbleBarPinController bubbleBarPinController,
+            BubblePinController bubblePinController) {
         this.bubbleBarController = bubbleBarController;
         this.bubbleBarViewController = bubbleBarViewController;
         this.bubbleStashController = bubbleStashController;
@@ -53,6 +55,7 @@
         this.bubbleDragController = bubbleDragController;
         this.bubbleDismissController = bubbleDismissController;
         this.bubbleBarPinController = bubbleBarPinController;
+        this.bubblePinController = bubblePinController;
     }
 
     /**
@@ -68,6 +71,7 @@
         bubbleDragController.init(/* bubbleControllers = */ this);
         bubbleDismissController.init(/* bubbleControllers = */ this);
         bubbleBarPinController.init(this);
+        bubblePinController.init(this);
 
         mPostInitRunnables.executeAllAndDestroy();
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
index d1c9da7..1764f75 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
@@ -43,6 +43,7 @@
     private BubbleBarViewController mBubbleBarViewController;
     private BubbleDismissController mBubbleDismissController;
     private BubbleBarPinController mBubbleBarPinController;
+    private BubblePinController mBubblePinController;
 
     public BubbleDragController(TaskbarActivityContext activity) {
         mActivity = activity;
@@ -58,8 +59,12 @@
         mBubbleBarViewController = bubbleControllers.bubbleBarViewController;
         mBubbleDismissController = bubbleControllers.bubbleDismissController;
         mBubbleBarPinController = bubbleControllers.bubbleBarPinController;
+        mBubblePinController = bubbleControllers.bubblePinController;
         mBubbleDismissController.setListener(
-                stuck -> mBubbleBarPinController.setDropTargetHidden(stuck));
+                stuck -> {
+                    mBubbleBarPinController.setDropTargetHidden(stuck);
+                    mBubblePinController.setDropTargetHidden(stuck);
+                });
     }
 
     /**
@@ -73,19 +78,58 @@
         }
 
         bubbleView.setOnTouchListener(new BubbleTouchListener() {
+
+            private BubbleBarLocation mReleasedLocation = BubbleBarLocation.DEFAULT;
+
+            private final LocationChangeListener mLocationChangeListener =
+                    new LocationChangeListener() {
+                        @Override
+                        public void onChange(@NonNull BubbleBarLocation location) {
+                            mBubbleBarController.animateBubbleBarLocation(location);
+                        }
+
+                        @Override
+                        public void onRelease(@NonNull BubbleBarLocation location) {
+                            mReleasedLocation = location;
+                        }
+                    };
+
             @Override
             void onDragStart() {
+                mBubblePinController.setListener(mLocationChangeListener);
                 mBubbleBarViewController.onDragStart(bubbleView);
+                mBubblePinController.onDragStart(
+                        mBubbleBarViewController.getBubbleBarLocation().isOnLeft(
+                                bubbleView.isLayoutRtl()));
             }
 
             @Override
-            void onDragEnd() {
-                mBubbleBarViewController.onDragEnd();
+            protected void onDragUpdate(float x, float y) {
+                mBubblePinController.onDragUpdate(x, y);
             }
 
             @Override
             protected void onDragRelease() {
-                mBubbleBarViewController.onDragRelease(bubbleView);
+                mBubblePinController.onDragEnd();
+                mBubbleBarViewController.onDragRelease(bubbleView, mReleasedLocation);
+            }
+
+            @Override
+            protected void onDragDismiss() {
+                mBubblePinController.onDragEnd();
+            }
+
+            @Override
+            void onDragEnd() {
+                mBubbleBarController.updateBubbleBarLocation(mReleasedLocation);
+                mBubbleBarViewController.onDragEnd();
+                mBubblePinController.setListener(null);
+            }
+
+            @Override
+            protected PointF getRestingPosition() {
+                return mBubbleBarViewController.getDraggedBubbleReleaseTranslation(
+                        getInitialPosition(), mReleasedLocation);
             }
         });
     }
@@ -98,8 +142,7 @@
         PointF initialRelativePivot = new PointF();
         bubbleBarView.setOnTouchListener(new BubbleTouchListener() {
 
-            @Nullable
-            private BubbleBarLocation mReleasedLocation;
+            private BubbleBarLocation mReleasedLocation = BubbleBarLocation.DEFAULT;
 
             private final LocationChangeListener mLocationChangeListener =
                     new LocationChangeListener() {
@@ -145,20 +188,18 @@
 
             @Override
             void onDragEnd() {
+                // Make sure to update location as the first thing. Pivot update causes a relayout
+                mBubbleBarController.updateBubbleBarLocation(mReleasedLocation);
+                bubbleBarView.setIsDragging(false);
                 // Restoring the initial pivot for the bubble bar view
                 bubbleBarView.setRelativePivot(initialRelativePivot.x, initialRelativePivot.y);
-                bubbleBarView.setIsDragging(false);
-                mBubbleBarController.updateBubbleBarLocation(mReleasedLocation);
+                mBubbleBarPinController.setListener(null);
             }
 
             @Override
             protected PointF getRestingPosition() {
-                if (mReleasedLocation == null
-                        || mReleasedLocation == bubbleBarView.getBubbleBarLocation()) {
-                    return getInitialPosition();
-                }
-                return bubbleBarView.getBubbleBarDragReleaseTranslation(getInitialPosition(),
-                        mReleasedLocation);
+                return mBubbleBarViewController.getBubbleBarDragReleaseTranslation(
+                        getInitialPosition(), mReleasedLocation);
             }
         });
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubblePinController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubblePinController.kt
new file mode 100644
index 0000000..fef7fa1
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubblePinController.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.launcher3.taskbar.bubbles
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Point
+import android.view.Gravity.BOTTOM
+import android.view.Gravity.LEFT
+import android.view.Gravity.RIGHT
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.FrameLayout
+import androidx.core.view.updateLayoutParams
+import com.android.launcher3.R
+import com.android.wm.shell.common.bubbles.BaseBubblePinController
+import com.android.wm.shell.common.bubbles.BubbleBarLocation
+
+/** Controller to manage pinning bubble bar to left or right when dragging starts from a bubble */
+class BubblePinController(
+    private val context: Context,
+    private val container: FrameLayout,
+    screenSizeProvider: () -> Point
+) : BaseBubblePinController(screenSizeProvider) {
+
+    private lateinit var bubbleBarViewController: BubbleBarViewController
+    private lateinit var bubbleStashController: BubbleStashController
+    private var exclRectWidth: Float = 0f
+    private var exclRectHeight: Float = 0f
+
+    private var dropTargetView: View? = null
+    private var dropTargetMargin: Int = 0
+
+    fun init(bubbleControllers: BubbleControllers) {
+        bubbleBarViewController = bubbleControllers.bubbleBarViewController
+        bubbleStashController = bubbleControllers.bubbleStashController
+        exclRectWidth = context.resources.getDimension(R.dimen.bubblebar_dismiss_zone_width)
+        exclRectHeight = context.resources.getDimension(R.dimen.bubblebar_dismiss_zone_height)
+        dropTargetMargin =
+            context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_drop_target_margin)
+    }
+
+    override fun getExclusionRectWidth(): Float {
+        return exclRectWidth
+    }
+
+    override fun getExclusionRectHeight(): Float {
+        return exclRectHeight
+    }
+
+    override fun getDropTargetView(): View? {
+        return dropTargetView
+    }
+
+    override fun removeDropTargetView(view: View) {
+        container.removeView(view)
+        dropTargetView = null
+    }
+
+    override fun createDropTargetView(): View {
+        return LayoutInflater.from(context)
+            .inflate(R.layout.bubble_expanded_view_drop_target, container, false)
+            .also { view ->
+                // TODO(b/330585402): dynamic height for the drop target based on actual height
+                dropTargetView = view
+                container.addView(view)
+            }
+    }
+
+    @SuppressLint("RtlHardcoded")
+    override fun updateLocation(location: BubbleBarLocation) {
+        val onLeft = location.isOnLeft(container.isLayoutRtl)
+
+        val bubbleBarBounds = bubbleBarViewController.bubbleBarBounds
+        dropTargetView?.updateLayoutParams<FrameLayout.LayoutParams> {
+            gravity = BOTTOM or (if (onLeft) LEFT else RIGHT)
+            bottomMargin =
+                -bubbleStashController.bubbleBarTranslationY.toInt() +
+                    bubbleBarBounds.height() +
+                    dropTargetMargin
+        }
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index bcdc718..3dc4ebc 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -66,6 +66,9 @@
     private final ImageView mAppIcon;
     private final int mBubbleSize;
 
+    private float mBubbleBarTranslationX = 0f;
+    private float mTranslationX = 0f;
+
     private DotRenderer mDotRenderer;
     private DotRenderer.DrawParams mDrawParams;
     private int mDotColor;
@@ -127,6 +130,35 @@
     }
 
     @Override
+    public void setTranslationX(float translationX) {
+        // Overriding setting translationX as it can be a combination of the parent translation
+        // and current view translation.
+        // When a BubbleView is being dragged to pin the bubble bar to other side, we animate the
+        // bar to the new location during the drag.
+        // One part of the animation is updating the translation of the bubble bar. But doing
+        // that also updates the translation for the child views, like the dragged bubble.
+        // To get around that, we instead apply translation on each child view of bubble bar. It
+        // is applied as bubble bar translation. This results in BubbleView's translation being a
+        // sum of the translation it has and the parent bubble bar translation.
+        mTranslationX = translationX;
+        applyTranslation();
+    }
+
+    /**
+     * Translation of the bubble bar that hosts this bubble.
+     * Is applied together with translation applied on the view through
+     * {@link #setTranslationX(float)}.
+     */
+    void setBubbleBarTranslationX(float translationX) {
+        mBubbleBarTranslationX = translationX;
+        applyTranslation();
+    }
+
+    private void applyTranslation() {
+        super.setTranslationX(mTranslationX + mBubbleBarTranslationX);
+    }
+
+    @Override
     public void dispatchDraw(Canvas canvas) {
         super.dispatchDraw(canvas);
 
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index fcf5ffc..0ad60b7 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -800,15 +800,29 @@
     /**
      * Tells SysUI when the bubble is being dragged.
      * Should be called only when the bubble bar is expanded.
-     * @param bubbleKey the key of the bubble to collapse/expand
-     * @param isBeingDragged whether the bubble is being dragged
+     * @param bubbleKey key of the bubble being dragged
      */
-    public void onBubbleDrag(@Nullable String bubbleKey, boolean isBeingDragged) {
+    public void startBubbleDrag(@Nullable String bubbleKey) {
         if (mBubbles == null) return;
         try {
-            mBubbles.onBubbleDrag(bubbleKey, isBeingDragged);
+            mBubbles.startBubbleDrag(bubbleKey);
         } catch (RemoteException e) {
-            Log.w(TAG, "Failed call onBubbleDrag");
+            Log.w(TAG, "Failed call startBubbleDrag");
+        }
+    }
+
+    /**
+     * Tells SysUI when the bubble stops being dragged.
+     * Should be called only when the bubble bar is expanded.
+     * @param bubbleKey key of the bubble being dragged
+     * @param location location of the bubble bar
+     */
+    public void stopBubbleDrag(@Nullable String bubbleKey, BubbleBarLocation location) {
+        if (mBubbles == null) return;
+        try {
+            mBubbles.stopBubbleDrag(bubbleKey, location);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Failed call stopBubbleDrag");
         }
     }