Merge "[Divider] Implement Divider Dragging" into main
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
index cae232e..b8ac191 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
@@ -23,9 +23,11 @@
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
 import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE;
 import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE;
+import static android.window.TaskFragmentOperation.OP_TYPE_SET_DECOR_SURFACE_BOOSTED;
 
 import static androidx.window.extensions.embedding.DividerAttributes.RATIO_UNSET;
 import static androidx.window.extensions.embedding.DividerAttributes.WIDTH_UNSET;
+import static androidx.window.extensions.embedding.SplitAttributesHelper.isReversedLayout;
 import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_BOTTOM;
 import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_LEFT;
 import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_RIGHT;
@@ -45,8 +47,10 @@
 import android.os.IBinder;
 import android.util.TypedValue;
 import android.view.Gravity;
+import android.view.MotionEvent;
 import android.view.SurfaceControl;
 import android.view.SurfaceControlViewHost;
+import android.view.View;
 import android.view.WindowManager;
 import android.view.WindowlessWindowManager;
 import android.widget.FrameLayout;
@@ -56,23 +60,30 @@
 import android.window.TaskFragmentParentInfo;
 import android.window.WindowContainerTransaction;
 
+import androidx.annotation.GuardedBy;
 import androidx.annotation.IdRes;
 import androidx.annotation.NonNull;
+import androidx.window.extensions.core.util.function.Consumer;
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.window.flags.Flags;
 
 import java.util.Objects;
+import java.util.concurrent.Executor;
 
 /**
  * Manages the rendering and interaction of the divider.
  */
-class DividerPresenter {
+class DividerPresenter implements View.OnTouchListener {
     private static final String WINDOW_NAME = "AE Divider";
+    private static final int VEIL_LAYER = 0;
+    private static final int DIVIDER_LAYER = 1;
 
     // TODO(b/327067596) Update based on UX guidance.
     private static final Color DEFAULT_DIVIDER_COLOR = Color.valueOf(Color.BLACK);
+    private static final Color DEFAULT_PRIMARY_VEIL_COLOR = Color.valueOf(Color.BLACK);
+    private static final Color DEFAULT_SECONDARY_VEIL_COLOR = Color.valueOf(Color.GRAY);
     @VisibleForTesting
     static final float DEFAULT_MIN_RATIO = 0.35f;
     @VisibleForTesting
@@ -80,11 +91,23 @@
     @VisibleForTesting
     static final int DEFAULT_DIVIDER_WIDTH_DP = 24;
 
+    private final int mTaskId;
+
+    @NonNull
+    private final Object mLock = new Object();
+
+    @NonNull
+    private final DragEventCallback mDragEventCallback;
+
+    @NonNull
+    private final Executor mCallbackExecutor;
+
     /**
      * The {@link Properties} of the divider. This field is {@code null} when no divider should be
      * drawn, e.g. when the split doesn't have {@link DividerAttributes} or when the decor surface
      * is not available.
      */
+    @GuardedBy("mLock")
     @Nullable
     @VisibleForTesting
     Properties mProperties;
@@ -94,6 +117,7 @@
      * drawn, i.e. when {@link #mProperties} is {@code null}. The {@link Renderer} is recreated or
      * updated when {@link #mProperties} is changed.
      */
+    @GuardedBy("mLock")
     @Nullable
     @VisibleForTesting
     Renderer mRenderer;
@@ -102,10 +126,26 @@
      * The owner TaskFragment token of the decor surface. The decor surface is placed right above
      * the owner TaskFragment surface and is removed if the owner TaskFragment is destroyed.
      */
+    @GuardedBy("mLock")
     @Nullable
     @VisibleForTesting
     IBinder mDecorSurfaceOwner;
 
+    /**
+     * The current divider position relative to the Task bounds. For vertical split (left-to-right
+     * or right-to-left), it is the x coordinate in the task window, and for horizontal split
+     * (top-to-bottom or bottom-to-top), it is the y coordinate in the task window.
+     */
+    @GuardedBy("mLock")
+    private int mDividerPosition;
+
+    DividerPresenter(int taskId, @NonNull DragEventCallback dragEventCallback,
+            @NonNull Executor callbackExecutor) {
+        mTaskId = taskId;
+        mDragEventCallback = dragEventCallback;
+        mCallbackExecutor = callbackExecutor;
+    }
+
     /** Updates the divider when external conditions are changed. */
     void updateDivider(
             @NonNull WindowContainerTransaction wct,
@@ -115,58 +155,65 @@
             return;
         }
 
-        // Clean up the decor surface if top SplitContainer is null.
-        if (topSplitContainer == null) {
-            removeDecorSurfaceAndDivider(wct);
-            return;
-        }
+        synchronized (mLock) {
+            // Clean up the decor surface if top SplitContainer is null.
+            if (topSplitContainer == null) {
+                removeDecorSurfaceAndDivider(wct);
+                return;
+            }
 
-        // Clean up the decor surface if DividerAttributes is null.
-        final DividerAttributes dividerAttributes =
-                topSplitContainer.getCurrentSplitAttributes().getDividerAttributes();
-        if (dividerAttributes == null) {
-            removeDecorSurfaceAndDivider(wct);
-            return;
-        }
+            // Clean up the decor surface if DividerAttributes is null.
+            final DividerAttributes dividerAttributes =
+                    topSplitContainer.getCurrentSplitAttributes().getDividerAttributes();
+            if (dividerAttributes == null) {
+                removeDecorSurfaceAndDivider(wct);
+                return;
+            }
 
-        if (topSplitContainer.getCurrentSplitAttributes().getSplitType()
-                instanceof SplitAttributes.SplitType.ExpandContainersSplitType) {
-            // No divider is needed for ExpandContainersSplitType.
-            removeDivider();
-            return;
-        }
+            if (topSplitContainer.getCurrentSplitAttributes().getSplitType()
+                    instanceof SplitAttributes.SplitType.ExpandContainersSplitType) {
+                // No divider is needed for ExpandContainersSplitType.
+                removeDivider();
+                return;
+            }
 
-        // Skip updating when the TFs have not been updated to match the SplitAttributes.
-        if (topSplitContainer.getPrimaryContainer().getLastRequestedBounds().isEmpty()
-                || topSplitContainer.getSecondaryContainer().getLastRequestedBounds().isEmpty()) {
-            return;
-        }
+            // Skip updating when the TFs have not been updated to match the SplitAttributes.
+            if (topSplitContainer.getPrimaryContainer().getLastRequestedBounds().isEmpty()
+                    || topSplitContainer.getSecondaryContainer().getLastRequestedBounds()
+                    .isEmpty()) {
+                return;
+            }
 
-        final SurfaceControl decorSurface = parentInfo.getDecorSurface();
-        if (decorSurface == null) {
-            // Clean up when the decor surface is currently unavailable.
-            removeDivider();
-            // Request to create the decor surface
-            createOrMoveDecorSurface(wct, topSplitContainer.getPrimaryContainer());
-            return;
-        }
+            final SurfaceControl decorSurface = parentInfo.getDecorSurface();
+            if (decorSurface == null) {
+                // Clean up when the decor surface is currently unavailable.
+                removeDivider();
+                // Request to create the decor surface
+                createOrMoveDecorSurface(wct, topSplitContainer.getPrimaryContainer());
+                return;
+            }
 
-        // make the top primary container the owner of the decor surface.
-        if (!Objects.equals(mDecorSurfaceOwner,
-                topSplitContainer.getPrimaryContainer().getTaskFragmentToken())) {
-            createOrMoveDecorSurface(wct, topSplitContainer.getPrimaryContainer());
-        }
+            // make the top primary container the owner of the decor surface.
+            if (!Objects.equals(mDecorSurfaceOwner,
+                    topSplitContainer.getPrimaryContainer().getTaskFragmentToken())) {
+                createOrMoveDecorSurface(wct, topSplitContainer.getPrimaryContainer());
+            }
 
-        updateProperties(
-                new Properties(
-                        parentInfo.getConfiguration(),
-                        dividerAttributes,
-                        decorSurface,
-                        getInitialDividerPosition(topSplitContainer),
-                        isVerticalSplit(topSplitContainer),
-                        parentInfo.getDisplayId()));
+            updateProperties(
+                    new Properties(
+                            parentInfo.getConfiguration(),
+                            dividerAttributes,
+                            decorSurface,
+                            getInitialDividerPosition(topSplitContainer),
+                            isVerticalSplit(topSplitContainer),
+                            isReversedLayout(
+                                    topSplitContainer.getCurrentSplitAttributes(),
+                                    parentInfo.getConfiguration()),
+                            parentInfo.getDisplayId()));
+        }
     }
 
+    @GuardedBy("mLock")
     private void updateProperties(@NonNull Properties properties) {
         if (Properties.equalsForDivider(mProperties, properties)) {
             return;
@@ -176,16 +223,16 @@
 
         if (mRenderer == null) {
             // Create a new renderer when a renderer doesn't exist yet.
-            mRenderer = new Renderer();
+            mRenderer = new Renderer(mProperties, this);
         } else if (!Properties.areSameSurfaces(
                 previousProperties.mDecorSurface, mProperties.mDecorSurface)
                 || previousProperties.mDisplayId != mProperties.mDisplayId) {
             // Release and recreate the renderer if the decor surface or the display has changed.
             mRenderer.release();
-            mRenderer = new Renderer();
+            mRenderer = new Renderer(mProperties, this);
         } else {
             // Otherwise, update the renderer for the new properties.
-            mRenderer.update();
+            mRenderer.update(mProperties);
         }
     }
 
@@ -195,6 +242,7 @@
      *
      * See {@link TaskFragmentOperation#OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE}.
      */
+    @GuardedBy("mLock")
     private void createOrMoveDecorSurface(
             @NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) {
         final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
@@ -204,6 +252,7 @@
         mDecorSurfaceOwner = container.getTaskFragmentToken();
     }
 
+    @GuardedBy("mLock")
     private void removeDecorSurfaceAndDivider(@NonNull WindowContainerTransaction wct) {
         if (mDecorSurfaceOwner != null) {
             final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
@@ -215,6 +264,7 @@
         removeDivider();
     }
 
+    @GuardedBy("mLock")
     private void removeDivider() {
         if (mRenderer != null) {
             mRenderer.release();
@@ -238,7 +288,7 @@
 
     private static boolean isVerticalSplit(@NonNull SplitContainer splitContainer) {
         final int layoutDirection = splitContainer.getCurrentSplitAttributes().getLayoutDirection();
-        switch(layoutDirection) {
+        switch (layoutDirection) {
             case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT:
             case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT:
             case SplitAttributes.LayoutDirection.LOCALE:
@@ -251,12 +301,6 @@
         }
     }
 
-    private static void safeReleaseSurfaceControl(@Nullable SurfaceControl sc) {
-        if (sc != null) {
-            sc.release();
-        }
-    }
-
     private static int getDividerWidthPx(@NonNull DividerAttributes dividerAttributes) {
         int dividerWidthDp = dividerAttributes.getWidthDp();
         return convertDpToPixel(dividerWidthDp);
@@ -388,6 +432,227 @@
                 .build();
     }
 
+    @Override
+    public boolean onTouch(@NonNull View view, @NonNull MotionEvent event) {
+        synchronized (mLock) {
+            final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds();
+            mDividerPosition = calculateDividerPosition(
+                    event, taskBounds, mRenderer.mDividerWidthPx, mProperties.mDividerAttributes,
+                    mProperties.mIsVerticalSplit, mProperties.mIsReversedLayout);
+            mRenderer.setDividerPosition(mDividerPosition);
+            switch (event.getAction()) {
+                case MotionEvent.ACTION_DOWN:
+                    onStartDragging();
+                    break;
+                case MotionEvent.ACTION_UP:
+                case MotionEvent.ACTION_CANCEL:
+                    onFinishDragging();
+                    break;
+                case MotionEvent.ACTION_MOVE:
+                    onDrag();
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        // Returns false so that the default button click callback is still triggered, i.e. the
+        // button UI transitions into the "pressed" state.
+        return false;
+    }
+
+    @GuardedBy("mLock")
+    private void onStartDragging() {
+        mRenderer.mIsDragging = true;
+        final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+        mRenderer.updateSurface(t);
+        mRenderer.showVeils(t);
+        final IBinder decorSurfaceOwner = mDecorSurfaceOwner;
+
+        // Callbacks must be executed on the executor to release mLock and prevent deadlocks.
+        mCallbackExecutor.execute(() -> {
+            mDragEventCallback.onStartDragging(
+                    wct -> setDecorSurfaceBoosted(wct, decorSurfaceOwner, true /* boosted */, t));
+        });
+    }
+
+    @GuardedBy("mLock")
+    private void onDrag() {
+        final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+        mRenderer.updateSurface(t);
+        t.apply();
+    }
+
+    @GuardedBy("mLock")
+    private void onFinishDragging() {
+        final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+        mRenderer.updateSurface(t);
+        mRenderer.hideVeils(t);
+        final IBinder decorSurfaceOwner = mDecorSurfaceOwner;
+
+        // Callbacks must be executed on the executor to release mLock and prevent deadlocks.
+        mCallbackExecutor.execute(() -> {
+            mDragEventCallback.onFinishDragging(
+                    mTaskId,
+                    wct -> setDecorSurfaceBoosted(wct, decorSurfaceOwner, false /* boosted */, t));
+        });
+        mRenderer.mIsDragging = false;
+    }
+
+    private static void setDecorSurfaceBoosted(
+            @NonNull WindowContainerTransaction wct,
+            @Nullable IBinder decorSurfaceOwner,
+            boolean boosted,
+            @NonNull SurfaceControl.Transaction clientTransaction) {
+        if (decorSurfaceOwner == null) {
+            return;
+        }
+        wct.addTaskFragmentOperation(
+                decorSurfaceOwner,
+                new TaskFragmentOperation.Builder(OP_TYPE_SET_DECOR_SURFACE_BOOSTED)
+                        .setBooleanValue(boosted)
+                        .setSurfaceTransaction(clientTransaction)
+                        .build()
+        );
+    }
+
+    /** Calculates the new divider position based on the touch event and divider attributes. */
+    @VisibleForTesting
+    static int calculateDividerPosition(@NonNull MotionEvent event, @NonNull Rect taskBounds,
+            int dividerWidthPx, @NonNull DividerAttributes dividerAttributes,
+            boolean isVerticalSplit, boolean isReversedLayout) {
+        // The touch event is in display space. Converting it into the task window space.
+        final int touchPositionInTaskSpace = isVerticalSplit
+                ? (int) (event.getRawX()) - taskBounds.left
+                : (int) (event.getRawY()) - taskBounds.top;
+
+        // Assuming that the touch position is at the center of the divider bar, so the divider
+        // position is offset by half of the divider width.
+        int dividerPosition = touchPositionInTaskSpace - dividerWidthPx / 2;
+
+        // Limit the divider position to the min and max ratios set in DividerAttributes.
+        // TODO(b/327536303) Handle when the divider is dragged to the edge.
+        dividerPosition = Math.max(dividerPosition, calculateMinPosition(
+                taskBounds, dividerWidthPx, dividerAttributes, isVerticalSplit, isReversedLayout));
+        dividerPosition = Math.min(dividerPosition, calculateMaxPosition(
+                taskBounds, dividerWidthPx, dividerAttributes, isVerticalSplit, isReversedLayout));
+        return dividerPosition;
+    }
+
+    /** Calculates the min position of the divider that the user is allowed to drag to. */
+    @VisibleForTesting
+    static int calculateMinPosition(@NonNull Rect taskBounds, int dividerWidthPx,
+            @NonNull DividerAttributes dividerAttributes, boolean isVerticalSplit,
+            boolean isReversedLayout) {
+        // The usable size is the task window size minus the divider bar width. This is shared
+        // between the primary and secondary containers based on the split ratio.
+        final int usableSize = isVerticalSplit
+                ? taskBounds.width() - dividerWidthPx
+                : taskBounds.height() - dividerWidthPx;
+        return (int) (isReversedLayout
+                ? usableSize - usableSize * dividerAttributes.getPrimaryMaxRatio()
+                : usableSize * dividerAttributes.getPrimaryMinRatio());
+    }
+
+    /** Calculates the max position of the divider that the user is allowed to drag to. */
+    @VisibleForTesting
+    static int calculateMaxPosition(@NonNull Rect taskBounds, int dividerWidthPx,
+            @NonNull DividerAttributes dividerAttributes, boolean isVerticalSplit,
+            boolean isReversedLayout) {
+        // The usable size is the task window size minus the divider bar width. This is shared
+        // between the primary and secondary containers based on the split ratio.
+        final int usableSize = isVerticalSplit
+                ? taskBounds.width() - dividerWidthPx
+                : taskBounds.height() - dividerWidthPx;
+        return (int) (isReversedLayout
+                ? usableSize - usableSize * dividerAttributes.getPrimaryMinRatio()
+                : usableSize * dividerAttributes.getPrimaryMaxRatio());
+    }
+
+    /**
+     * Returns the new split ratio of the {@link SplitContainer} based on the current divider
+     * position.
+     */
+    float calculateNewSplitRatio(@NonNull SplitContainer topSplitContainer) {
+        synchronized (mLock) {
+            return calculateNewSplitRatio(
+                    topSplitContainer,
+                    mDividerPosition,
+                    mProperties.mConfiguration.windowConfiguration.getBounds(),
+                    mRenderer.mDividerWidthPx,
+                    mProperties.mIsVerticalSplit,
+                    mProperties.mIsReversedLayout);
+        }
+    }
+
+    /**
+     * Returns the new split ratio of the {@link SplitContainer} based on the current divider
+     * position.
+     * @param topSplitContainer the {@link SplitContainer} for which to compute the split ratio.
+     * @param dividerPosition the divider position. See {@link #mDividerPosition}.
+     * @param taskBounds the task bounds
+     * @param dividerWidthPx the width of the divider in pixels.
+     * @param isVerticalSplit if {@code true}, the split is a vertical split. If {@code false}, the
+     *                        split is a horizontal split. See
+     *                        {@link #isVerticalSplit(SplitContainer)}.
+     * @param isReversedLayout if {@code true}, the split layout is reversed, i.e. right-to-left or
+     *                         bottom-to-top. If {@code false}, the split is not reversed, i.e.
+     *                         left-to-right or top-to-bottom. See
+     *                         {@link SplitAttributesHelper#isReversedLayout}
+     * @return the computed split ratio of the primary container.
+     */
+    @VisibleForTesting
+    static float calculateNewSplitRatio(
+            @NonNull SplitContainer topSplitContainer,
+            int dividerPosition,
+            @NonNull Rect taskBounds,
+            int dividerWidthPx,
+            boolean isVerticalSplit,
+            boolean isReversedLayout) {
+        final int usableSize = isVerticalSplit
+                ? taskBounds.width() - dividerWidthPx
+                : taskBounds.height() - dividerWidthPx;
+
+        final TaskFragmentContainer primaryContainer = topSplitContainer.getPrimaryContainer();
+        final Rect origPrimaryBounds = primaryContainer.getLastRequestedBounds();
+
+        float newRatio;
+        if (isVerticalSplit) {
+            final int newPrimaryWidth = isReversedLayout
+                    ? (origPrimaryBounds.right - (dividerPosition + dividerWidthPx))
+                    : (dividerPosition - origPrimaryBounds.left);
+            newRatio = 1.0f * newPrimaryWidth / usableSize;
+        } else {
+            final int newPrimaryHeight = isReversedLayout
+                    ? (origPrimaryBounds.bottom - (dividerPosition + dividerWidthPx))
+                    : (dividerPosition - origPrimaryBounds.top);
+            newRatio = 1.0f * newPrimaryHeight / usableSize;
+        }
+        return newRatio;
+    }
+
+    /** Callbacks for drag events */
+    interface DragEventCallback {
+        /**
+         * Called when the user starts dragging the divider. Callbacks are executed on
+         * {@link #mCallbackExecutor}.
+         *
+         * @param action additional action that should be applied to the
+         *               {@link WindowContainerTransaction}
+         */
+        void onStartDragging(@NonNull Consumer<WindowContainerTransaction> action);
+
+        /**
+         * Called when the user finishes dragging the divider. Callbacks are executed on
+         * {@link #mCallbackExecutor}.
+         *
+         * @param taskId the Task id of the {@link TaskContainer} that this divider belongs to.
+         * @param action additional action that should be applied to the
+         *               {@link WindowContainerTransaction}
+         */
+        void onFinishDragging(int taskId, @NonNull Consumer<WindowContainerTransaction> action);
+    }
+
     /**
      * Properties for the {@link DividerPresenter}. The rendering of the divider solely depends on
      * these properties. When any value is updated, the divider is re-rendered. The Properties
@@ -411,6 +676,7 @@
         private final boolean mIsVerticalSplit;
 
         private final int mDisplayId;
+        private final boolean mIsReversedLayout;
 
         @VisibleForTesting
         Properties(
@@ -419,12 +685,14 @@
                 @NonNull SurfaceControl decorSurface,
                 int initialDividerPosition,
                 boolean isVerticalSplit,
+                boolean isReversedLayout,
                 int displayId) {
             mConfiguration = configuration;
             mDividerAttributes = dividerAttributes;
             mDecorSurface = decorSurface;
             mInitialDividerPosition = initialDividerPosition;
             mIsVerticalSplit = isVerticalSplit;
+            mIsReversedLayout = isReversedLayout;
             mDisplayId = displayId;
         }
 
@@ -445,7 +713,8 @@
                     && areConfigurationsEqualForDivider(a.mConfiguration, b.mConfiguration)
                     && a.mInitialDividerPosition == b.mInitialDividerPosition
                     && a.mIsVerticalSplit == b.mIsVerticalSplit
-                    && a.mDisplayId == b.mDisplayId;
+                    && a.mDisplayId == b.mDisplayId
+                    && a.mIsReversedLayout == b.mIsReversedLayout;
         }
 
         private static boolean areSameSurfaces(
@@ -472,7 +741,7 @@
      * recreated. When other fields in the Properties are changed, the renderer is updated.
      */
     @VisibleForTesting
-    class Renderer {
+    static class Renderer {
         @NonNull
         private final SurfaceControl mDividerSurface;
         @NonNull
@@ -481,10 +750,21 @@
         private final SurfaceControlViewHost mViewHost;
         @NonNull
         private final FrameLayout mDividerLayout;
-        private final int mDividerWidthPx;
+        @NonNull
+        private final View.OnTouchListener mListener;
+        @NonNull
+        private Properties mProperties;
+        private int mDividerWidthPx;
+        @Nullable
+        private SurfaceControl mPrimaryVeil;
+        @Nullable
+        private SurfaceControl mSecondaryVeil;
+        private boolean mIsDragging;
+        private int mDividerPosition;
 
-        private Renderer() {
-            mDividerWidthPx = getDividerWidthPx(mProperties.mDividerAttributes);
+        private Renderer(@NonNull Properties properties, @NonNull View.OnTouchListener listener) {
+            mProperties = properties;
+            mListener = listener;
 
             mDividerSurface = createChildSurface("DividerSurface", true /* visible */);
             mWindowlessWindowManager = new WindowlessWindowManager(
@@ -503,36 +783,63 @@
         }
 
         /** Updates the divider when properties are changed */
+        private void update(@NonNull Properties newProperties) {
+            mProperties = newProperties;
+            update();
+        }
+
+        /** Updates the divider when initializing or when properties are changed */
         @VisibleForTesting
         void update() {
+            mDividerWidthPx = getDividerWidthPx(mProperties.mDividerAttributes);
+            mDividerPosition = mProperties.mInitialDividerPosition;
             mWindowlessWindowManager.setConfiguration(mProperties.mConfiguration);
-            updateSurface();
+            // TODO handle synchronization between surface transactions and WCT.
+            final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+            updateSurface(t);
             updateLayout();
-            updateDivider();
+            updateDivider(t);
+            t.apply();
         }
 
         @VisibleForTesting
         void release() {
             mViewHost.release();
             // TODO handle synchronization between surface transactions and WCT.
-            new SurfaceControl.Transaction().remove(mDividerSurface).apply();
-            safeReleaseSurfaceControl(mDividerSurface);
-        }
-
-        private void updateSurface() {
-            final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds();
-            // TODO handle synchronization between surface transactions and WCT.
             final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
-            if (mProperties.mIsVerticalSplit) {
-                t.setPosition(mDividerSurface, mProperties.mInitialDividerPosition, 0.0f);
-                t.setWindowCrop(mDividerSurface, mDividerWidthPx, taskBounds.height());
-            } else {
-                t.setPosition(mDividerSurface, 0.0f, mProperties.mInitialDividerPosition);
-                t.setWindowCrop(mDividerSurface, taskBounds.width(), mDividerWidthPx);
-            }
+            t.remove(mDividerSurface);
+            removeVeils(t);
             t.apply();
         }
 
+        private void setDividerPosition(int dividerPosition) {
+            mDividerPosition = dividerPosition;
+        }
+
+        /**
+         * Updates the positions and crops of the divider surface and veil surfaces. This method
+         * should be called when {@link #mProperties} is changed or while dragging to update the
+         * position of the divider surface and the veil surfaces.
+         */
+        private void updateSurface(@NonNull SurfaceControl.Transaction t) {
+            final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds();
+            if (mProperties.mIsVerticalSplit) {
+                t.setPosition(mDividerSurface, mDividerPosition, 0.0f);
+                t.setWindowCrop(mDividerSurface, mDividerWidthPx, taskBounds.height());
+            } else {
+                t.setPosition(mDividerSurface, 0.0f, mDividerPosition);
+                t.setWindowCrop(mDividerSurface, taskBounds.width(), mDividerWidthPx);
+            }
+            if (mIsDragging) {
+                updateVeils(t);
+            }
+        }
+
+        /**
+         * Updates the layout parameters of the layout used to host the divider. This method should
+         * be called only when {@link #mProperties} is changed. This should not be called while
+         * dragging, because the layout parameters are not changed during dragging.
+         */
         private void updateLayout() {
             final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds();
             final WindowManager.LayoutParams lp = mProperties.mIsVerticalSplit
@@ -552,12 +859,21 @@
             mViewHost.setView(mDividerLayout, lp);
         }
 
-        private void updateDivider() {
+        /**
+         * Updates the UI component of the divider, including the drag handle and the veils. This
+         * method should be called only when {@link #mProperties} is changed. This should not be
+         * called while dragging, because the UI components are not changed during dragging and
+         * only their surface positions are changed.
+         */
+        private void updateDivider(@NonNull SurfaceControl.Transaction t) {
             mDividerLayout.removeAllViews();
             mDividerLayout.setBackgroundColor(DEFAULT_DIVIDER_COLOR.toArgb());
             if (mProperties.mDividerAttributes.getDividerType()
                     == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) {
+                createVeils();
                 drawDragHandle();
+            } else {
+                removeVeils(t);
             }
             mViewHost.getView().invalidate();
         }
@@ -580,7 +896,7 @@
             button.setLayoutParams(params);
             button.setBackgroundColor(R.color.transparent);
 
-            final Drawable handle =  context.getResources().getDrawable(
+            final Drawable handle = context.getResources().getDrawable(
                     R.drawable.activity_embedding_divider_handle, context.getTheme());
             if (mProperties.mIsVerticalSplit) {
                 button.setImageDrawable(handle);
@@ -598,6 +914,8 @@
 
                 button.setImageDrawable(rotatedHandle);
             }
+
+            button.setOnTouchListener(mListener);
             mDividerLayout.addView(button);
         }
 
@@ -613,5 +931,69 @@
                     .setColorLayer()
                     .build();
         }
+
+        private void createVeils() {
+            if (mPrimaryVeil == null) {
+                mPrimaryVeil = createChildSurface("DividerPrimaryVeil", false /* visible */);
+            }
+            if (mSecondaryVeil == null) {
+                mSecondaryVeil = createChildSurface("DividerSecondaryVeil", false /* visible */);
+            }
+        }
+
+        private void removeVeils(@NonNull SurfaceControl.Transaction t) {
+            if (mPrimaryVeil != null) {
+                t.remove(mPrimaryVeil);
+            }
+            if (mSecondaryVeil != null) {
+                t.remove(mSecondaryVeil);
+            }
+            mPrimaryVeil = null;
+            mSecondaryVeil = null;
+        }
+
+        private void showVeils(@NonNull SurfaceControl.Transaction t) {
+            t.setColor(mPrimaryVeil, colorToFloatArray(DEFAULT_PRIMARY_VEIL_COLOR))
+                    .setColor(mSecondaryVeil, colorToFloatArray(DEFAULT_SECONDARY_VEIL_COLOR))
+                    .setLayer(mDividerSurface, DIVIDER_LAYER)
+                    .setLayer(mPrimaryVeil, VEIL_LAYER)
+                    .setLayer(mSecondaryVeil, VEIL_LAYER)
+                    .setVisibility(mPrimaryVeil, true)
+                    .setVisibility(mSecondaryVeil, true);
+            updateVeils(t);
+        }
+
+        private void hideVeils(@NonNull SurfaceControl.Transaction t) {
+            t.setVisibility(mPrimaryVeil, false).setVisibility(mSecondaryVeil, false);
+        }
+
+        private void updateVeils(@NonNull SurfaceControl.Transaction t) {
+            final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds();
+
+            // Relative bounds of the primary and secondary containers in the Task.
+            Rect primaryBounds;
+            Rect secondaryBounds;
+            if (mProperties.mIsVerticalSplit) {
+                final Rect boundsLeft = new Rect(0, 0, mDividerPosition, taskBounds.height());
+                final Rect boundsRight = new Rect(mDividerPosition + mDividerWidthPx, 0,
+                        taskBounds.width(), taskBounds.height());
+                primaryBounds = mProperties.mIsReversedLayout ? boundsRight : boundsLeft;
+                secondaryBounds = mProperties.mIsReversedLayout ? boundsLeft : boundsRight;
+            } else {
+                final Rect boundsTop = new Rect(0, 0, taskBounds.width(), mDividerPosition);
+                final Rect boundsBottom = new Rect(0, mDividerPosition + mDividerWidthPx,
+                        taskBounds.width(), taskBounds.height());
+                primaryBounds = mProperties.mIsReversedLayout ? boundsBottom : boundsTop;
+                secondaryBounds = mProperties.mIsReversedLayout ? boundsTop : boundsBottom;
+            }
+            t.setWindowCrop(mPrimaryVeil, primaryBounds.width(), primaryBounds.height());
+            t.setWindowCrop(mSecondaryVeil, secondaryBounds.width(), secondaryBounds.height());
+            t.setPosition(mPrimaryVeil, primaryBounds.left, primaryBounds.top);
+            t.setPosition(mSecondaryVeil, secondaryBounds.left, secondaryBounds.top);
+        }
+
+        private static float[] colorToFloatArray(@NonNull Color color) {
+            return new float[]{color.red(), color.green(), color.blue()};
+        }
     }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index 3f4dddf..32f2d67 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -165,7 +165,7 @@
     /**
      * Expands an existing TaskFragment to fill parent.
      * @param wct WindowContainerTransaction in which the task fragment should be resized.
-     * @param fragmentToken token of an existing TaskFragment.
+     * @param container the {@link TaskFragmentContainer} to be expanded.
      */
     void expandTaskFragment(@NonNull WindowContainerTransaction wct,
             @NonNull TaskFragmentContainer container) {
@@ -174,8 +174,6 @@
         clearAdjacentTaskFragments(wct, fragmentToken);
         updateWindowingMode(wct, fragmentToken, WINDOWING_MODE_UNDEFINED);
         updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT);
-
-        container.getTaskContainer().updateDivider(wct);
     }
 
     /**
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitAttributesHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitAttributesHelper.java
new file mode 100644
index 0000000..042a68a6
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitAttributesHelper.java
@@ -0,0 +1,46 @@
+/*
+ * 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 androidx.window.extensions.embedding;
+
+import android.content.res.Configuration;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+/** Helper functions for {@link SplitAttributes} */
+class SplitAttributesHelper {
+    /**
+     * Returns whether the split layout direction is reversed. Right-to-left and bottom-to-top are
+     * considered reversed.
+     */
+    static boolean isReversedLayout(
+            @NonNull SplitAttributes splitAttributes, @NonNull Configuration configuration) {
+        switch (splitAttributes.getLayoutDirection()) {
+            case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT:
+            case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM:
+                return false;
+            case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT:
+            case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP:
+                return true;
+            case SplitAttributes.LayoutDirection.LOCALE:
+                return configuration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+            default:
+                throw new IllegalArgumentException(
+                        "Invalid layout direction:" + splitAttributes.getLayoutDirection());
+        }
+    }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 1bc8264..b9b86f0 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -110,7 +110,7 @@
  * Main controller class that manages split states and presentation.
  */
 public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback,
-        ActivityEmbeddingComponent {
+        ActivityEmbeddingComponent, DividerPresenter.DragEventCallback {
     static final String TAG = "SplitController";
     static final boolean ENABLE_SHELL_TRANSITIONS =
             SystemProperties.getBoolean("persist.wm.debug.shell_transit", true);
@@ -163,6 +163,10 @@
     @GuardedBy("mLock")
     final SparseArray<TaskContainer> mTaskContainers = new SparseArray<>();
 
+    /** Map from Task id to {@link DividerPresenter} which manages the divider in the Task. */
+    @GuardedBy("mLock")
+    private final SparseArray<DividerPresenter> mDividerPresenters = new SparseArray<>();
+
     /** Callback to Jetpack to notify about changes to split states. */
     @GuardedBy("mLock")
     @Nullable
@@ -195,15 +199,16 @@
                     : null;
 
     private final Handler mHandler;
+    private final MainThreadExecutor mExecutor;
     final Object mLock = new Object();
     private final ActivityStartMonitor mActivityStartMonitor;
 
     public SplitController(@NonNull WindowLayoutComponentImpl windowLayoutComponent,
             @NonNull DeviceStateManagerFoldingFeatureProducer foldingFeatureProducer) {
         Log.i(TAG, "Initializing Activity Embedding Controller.");
-        final MainThreadExecutor executor = new MainThreadExecutor();
-        mHandler = executor.mHandler;
-        mPresenter = new SplitPresenter(executor, windowLayoutComponent, this);
+        mExecutor = new MainThreadExecutor();
+        mHandler = mExecutor.mHandler;
+        mPresenter = new SplitPresenter(mExecutor, windowLayoutComponent, this);
         mTransactionManager = new TransactionManager(mPresenter);
         final ActivityThread activityThread = ActivityThread.currentActivityThread();
         final Application application = activityThread.getApplication();
@@ -844,7 +849,11 @@
         // Checks if container should be updated before apply new parentInfo.
         final boolean shouldUpdateContainer = taskContainer.shouldUpdateContainer(parentInfo);
         taskContainer.updateTaskFragmentParentInfo(parentInfo);
-        taskContainer.updateDivider(wct);
+
+        // The divider need to be updated even if shouldUpdateContainer is false, because the decor
+        // surface may change in TaskFragmentParentInfo, which requires divider update but not
+        // container update.
+        updateDivider(wct, taskContainer);
 
         // If the last direct activity of the host task is dismissed and the overlay container is
         // the only taskFragment, the overlay container should also be dismissed.
@@ -1007,6 +1016,7 @@
             if (taskContainer.isEmpty()) {
                 // Cleanup the TaskContainer if it becomes empty.
                 mTaskContainers.remove(taskContainer.getTaskId());
+                mDividerPresenters.remove(taskContainer.getTaskId());
             }
             return;
         }
@@ -1759,6 +1769,7 @@
         }
         if (!mTaskContainers.contains(taskId)) {
             mTaskContainers.put(taskId, new TaskContainer(taskId, activityInTask));
+            mDividerPresenters.put(taskId, new DividerPresenter(taskId, this, mExecutor));
         }
         final TaskContainer taskContainer = mTaskContainers.get(taskId);
         final TaskFragmentContainer container = new TaskFragmentContainer(pendingAppearedActivity,
@@ -3065,4 +3076,46 @@
         return configuration != null
                 && configuration.windowConfiguration.getWindowingMode() == WINDOWING_MODE_PINNED;
     }
+
+    @GuardedBy("mLock")
+    void updateDivider(
+            @NonNull WindowContainerTransaction wct, @NonNull TaskContainer taskContainer) {
+        final DividerPresenter dividerPresenter = mDividerPresenters.get(taskContainer.getTaskId());
+        final TaskFragmentParentInfo parentInfo = taskContainer.getTaskFragmentParentInfo();
+        if (parentInfo != null) {
+            dividerPresenter.updateDivider(
+                    wct, parentInfo, taskContainer.getTopNonFinishingSplitContainer());
+        }
+    }
+
+    @Override
+    public void onStartDragging(@NonNull Consumer<WindowContainerTransaction> action) {
+        synchronized (mLock) {
+            final TransactionRecord transactionRecord =
+                    mTransactionManager.startNewTransaction();
+            final WindowContainerTransaction wct = transactionRecord.getTransaction();
+            action.accept(wct);
+            transactionRecord.apply(false /* shouldApplyIndependently */);
+        }
+    }
+
+    @Override
+    public void onFinishDragging(
+            int taskId,
+            @NonNull Consumer<WindowContainerTransaction> action) {
+        synchronized (mLock) {
+            final TransactionRecord transactionRecord =
+                    mTransactionManager.startNewTransaction();
+            final WindowContainerTransaction wct = transactionRecord.getTransaction();
+            final TaskContainer taskContainer = mTaskContainers.get(taskId);
+            if (taskContainer != null) {
+                final DividerPresenter dividerPresenter =
+                        mDividerPresenters.get(taskContainer.getTaskId());
+                taskContainer.updateTopSplitContainerForDivider(dividerPresenter);
+                updateContainersInTask(wct, taskContainer);
+            }
+            action.accept(wct);
+            transactionRecord.apply(false /* shouldApplyIndependently */);
+        }
+    }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index 20bc820..0d31266 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -19,6 +19,7 @@
 import static android.content.pm.PackageManager.MATCH_ALL;
 
 import static androidx.window.extensions.embedding.DividerPresenter.getBoundsOffsetForDivider;
+import static androidx.window.extensions.embedding.SplitAttributesHelper.isReversedLayout;
 import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK;
 
 import android.app.Activity;
@@ -33,7 +34,6 @@
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.IBinder;
-import android.util.LayoutDirection;
 import android.util.Pair;
 import android.util.Size;
 import android.view.View;
@@ -368,7 +368,7 @@
         updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode);
         updateAnimationParams(wct, primaryContainer.getTaskFragmentToken(), splitAttributes);
         updateAnimationParams(wct, secondaryContainer.getTaskFragmentToken(), splitAttributes);
-        taskContainer.updateDivider(wct);
+        mController.updateDivider(wct, taskContainer);
     }
 
     private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
@@ -697,6 +697,17 @@
         return RESULT_NOT_EXPANDED;
     }
 
+    /**
+     * Expands an existing TaskFragment to fill parent.
+     * @param wct WindowContainerTransaction in which the task fragment should be resized.
+     * @param container the {@link TaskFragmentContainer} to be expanded.
+     */
+    void expandTaskFragment(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentContainer container) {
+        super.expandTaskFragment(wct, container);
+        mController.updateDivider(wct, container.getTaskContainer());
+    }
+
     static boolean shouldShowSplit(@NonNull SplitContainer splitContainer) {
         return shouldShowSplit(splitContainer.getCurrentSplitAttributes());
     }
@@ -1108,7 +1119,6 @@
      */
     private SplitType computeSplitType(@NonNull SplitAttributes splitAttributes,
             @NonNull Configuration taskConfiguration, @Nullable FoldingFeature foldingFeature) {
-        final int layoutDirection = splitAttributes.getLayoutDirection();
         final SplitType splitType = splitAttributes.getSplitType();
         if (splitType instanceof ExpandContainersSplitType) {
             return splitType;
@@ -1117,19 +1127,9 @@
             // Reverse the ratio for RIGHT_TO_LEFT and BOTTOM_TO_TOP to make the boundary
             // computation have the same direction, which is from (top, left) to (bottom, right).
             final SplitType reversedSplitType = new RatioSplitType(1 - splitRatio.getRatio());
-            switch (layoutDirection) {
-                case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT:
-                case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM:
-                    return splitType;
-                case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT:
-                case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP:
-                    return reversedSplitType;
-                case LayoutDirection.LOCALE: {
-                    boolean isLtr = taskConfiguration.getLayoutDirection()
-                            == View.LAYOUT_DIRECTION_LTR;
-                    return isLtr ? splitType : reversedSplitType;
-                }
-            }
+            return isReversedLayout(splitAttributes, taskConfiguration)
+                    ? reversedSplitType
+                    : splitType;
         } else if (splitType instanceof HingeSplitType) {
             final HingeSplitType hinge = (HingeSplitType) splitType;
             @WindowingMode
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index e75a317..a215bdf 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -88,10 +88,6 @@
      */
     final Set<IBinder> mFinishedContainer = new ArraySet<>();
 
-    // TODO(b/293654166): move DividerPresenter to SplitController.
-    @NonNull
-    final DividerPresenter mDividerPresenter;
-
     /**
      * The {@link TaskContainer} constructor
      *
@@ -113,7 +109,6 @@
         // the host task is visible and has an activity in the task.
         mIsVisible = true;
         mHasDirectActivity = true;
-        mDividerPresenter = new DividerPresenter();
     }
 
     int getTaskId() {
@@ -151,6 +146,11 @@
         mTaskFragmentParentInfo = info;
     }
 
+    @Nullable
+    TaskFragmentParentInfo getTaskFragmentParentInfo() {
+        return mTaskFragmentParentInfo;
+    }
+
     /**
      * Returns {@code true} if the container should be updated with {@code info}.
      */
@@ -398,16 +398,22 @@
         return mContainers;
     }
 
-    void updateDivider(@NonNull WindowContainerTransaction wct) {
-        if (mTaskFragmentParentInfo != null) {
-            // Update divider only if TaskFragmentParentInfo is available.
-            mDividerPresenter.updateDivider(
-                    wct, mTaskFragmentParentInfo, getTopNonFinishingSplitContainer());
+    void updateTopSplitContainerForDivider(@NonNull DividerPresenter dividerPresenter) {
+        final SplitContainer topSplitContainer = getTopNonFinishingSplitContainer();
+        if (topSplitContainer == null) {
+            return;
         }
+
+        final float newRatio = dividerPresenter.calculateNewSplitRatio(topSplitContainer);
+        topSplitContainer.updateDefaultSplitAttributes(
+                new SplitAttributes.Builder(topSplitContainer.getDefaultSplitAttributes())
+                        .setSplitType(new SplitAttributes.SplitType.RatioSplitType(newRatio))
+                        .build()
+        );
     }
 
     @Nullable
-    private SplitContainer getTopNonFinishingSplitContainer() {
+    SplitContainer getTopNonFinishingSplitContainer() {
         for (int i = mSplitContainers.size() - 1; i >= 0; i--) {
             final SplitContainer splitContainer = mSplitContainers.get(i);
             if (!splitContainer.getPrimaryContainer().isFinished()
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
index 4d1d807..47d01da 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
@@ -42,6 +42,7 @@
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.view.Display;
+import android.view.MotionEvent;
 import android.view.SurfaceControl;
 import android.window.TaskFragmentOperation;
 import android.window.TaskFragmentParentInfo;
@@ -60,6 +61,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.concurrent.Executor;
+
 /**
  * Test class for {@link DividerPresenter}.
  *
@@ -73,6 +76,8 @@
     @Rule
     public final SetFlagsRule mSetFlagRule = new SetFlagsRule();
 
+    private static final int MOCK_TASK_ID = 1234;
+
     @Mock
     private DividerPresenter.Renderer mRenderer;
 
@@ -83,6 +88,12 @@
     private TaskFragmentParentInfo mParentInfo;
 
     @Mock
+    private TaskContainer mTaskContainer;
+
+    @Mock
+    private DividerPresenter.DragEventCallback mDragEventCallback;
+
+    @Mock
     private SplitContainer mSplitContainer;
 
     @Mock
@@ -110,6 +121,8 @@
         MockitoAnnotations.initMocks(this);
         mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_INTERACTIVE_DIVIDER_FLAG);
 
+        when(mTaskContainer.getTaskId()).thenReturn(MOCK_TASK_ID);
+
         when(mParentInfo.getDisplayId()).thenReturn(Display.DEFAULT_DISPLAY);
         when(mParentInfo.getConfiguration()).thenReturn(new Configuration());
         when(mParentInfo.getDecorSurface()).thenReturn(mSurfaceControl);
@@ -133,9 +146,11 @@
                 mSurfaceControl,
                 getInitialDividerPosition(mSplitContainer),
                 true /* isVerticalSplit */,
+                false /* isReversedLayout */,
                 Display.DEFAULT_DISPLAY);
 
-        mDividerPresenter = new DividerPresenter();
+        mDividerPresenter = new DividerPresenter(
+                MOCK_TASK_ID, mDragEventCallback, mock(Executor.class));
         mDividerPresenter.mProperties = mProperties;
         mDividerPresenter.mRenderer = mRenderer;
         mDividerPresenter.mDecorSurfaceOwner = mPrimaryContainerToken;
@@ -311,6 +326,184 @@
                 dividerWidthPx, splitType, expectedTopLeftOffset, expectedBottomRightOffset);
     }
 
+    @Test
+    public void testCalculateDividerPosition() {
+        final MotionEvent event = mock(MotionEvent.class);
+        final Rect taskBounds = new Rect(100, 200, 1000, 2000);
+        final int dividerWidthPx = 50;
+        final DividerAttributes dividerAttributes =
+                new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE)
+                        .setPrimaryMinRatio(0.3f)
+                        .setPrimaryMaxRatio(0.8f)
+                        .build();
+
+        // Left-to-right split
+        when(event.getRawX()).thenReturn(500f); // Touch event is in display space
+        assertEquals(
+                // Touch position is in task space is 400, then minus half of divider width.
+                375,
+                DividerPresenter.calculateDividerPosition(
+                        event,
+                        taskBounds,
+                        dividerWidthPx,
+                        dividerAttributes,
+                        true /* isVerticalSplit */,
+                        false /* isReversedLayout */));
+
+        // Top-to-bottom split
+        when(event.getRawY()).thenReturn(1000f); // Touch event is in display space
+        assertEquals(
+                // Touch position is in task space is 800, then minus half of divider width.
+                775,
+                DividerPresenter.calculateDividerPosition(
+                        event,
+                        taskBounds,
+                        dividerWidthPx,
+                        dividerAttributes,
+                        false /* isVerticalSplit */,
+                        false /* isReversedLayout */));
+    }
+
+    @Test
+    public void testCalculateMinPosition() {
+        final Rect taskBounds = new Rect(100, 200, 1000, 2000);
+        final int dividerWidthPx = 50;
+        final DividerAttributes dividerAttributes =
+                new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE)
+                        .setPrimaryMinRatio(0.3f)
+                        .setPrimaryMaxRatio(0.8f)
+                        .build();
+
+        // Left-to-right split
+        assertEquals(
+                255, /* (1000 - 100 - 50) * 0.3 */
+                DividerPresenter.calculateMinPosition(
+                        taskBounds,
+                        dividerWidthPx,
+                        dividerAttributes,
+                        true /* isVerticalSplit */,
+                        false /* isReversedLayout */));
+
+        // Top-to-bottom split
+        assertEquals(
+                525, /* (2000 - 200 - 50) * 0.3 */
+                DividerPresenter.calculateMinPosition(
+                        taskBounds,
+                        dividerWidthPx,
+                        dividerAttributes,
+                        false /* isVerticalSplit */,
+                        false /* isReversedLayout */));
+
+        // Right-to-left split
+        assertEquals(
+                170, /* (1000 - 100 - 50) * (1 - 0.8) */
+                DividerPresenter.calculateMinPosition(
+                        taskBounds,
+                        dividerWidthPx,
+                        dividerAttributes,
+                        true /* isVerticalSplit */,
+                        true /* isReversedLayout */));
+    }
+
+    @Test
+    public void testCalculateMaxPosition() {
+        final Rect taskBounds = new Rect(100, 200, 1000, 2000);
+        final int dividerWidthPx = 50;
+        final DividerAttributes dividerAttributes =
+                new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE)
+                        .setPrimaryMinRatio(0.3f)
+                        .setPrimaryMaxRatio(0.8f)
+                        .build();
+
+        // Left-to-right split
+        assertEquals(
+                680, /* (1000 - 100 - 50) * 0.8 */
+                DividerPresenter.calculateMaxPosition(
+                        taskBounds,
+                        dividerWidthPx,
+                        dividerAttributes,
+                        true /* isVerticalSplit */,
+                        false /* isReversedLayout */));
+
+        // Top-to-bottom split
+        assertEquals(
+                1400, /* (2000 - 200 - 50) * 0.8 */
+                DividerPresenter.calculateMaxPosition(
+                        taskBounds,
+                        dividerWidthPx,
+                        dividerAttributes,
+                        false /* isVerticalSplit */,
+                        false /* isReversedLayout */));
+
+        // Right-to-left split
+        assertEquals(
+                595, /* (1000 - 100 - 50) * (1 - 0.3) */
+                DividerPresenter.calculateMaxPosition(
+                        taskBounds,
+                        dividerWidthPx,
+                        dividerAttributes,
+                        true /* isVerticalSplit */,
+                        true /* isReversedLayout */));
+    }
+
+    @Test
+    public void testCalculateNewSplitRatio_leftToRight() {
+        // primary=500px; secondary=500px; divider=100px; total=1100px.
+        final Rect taskBounds = new Rect(0, 0, 1100, 2000);
+        final Rect primaryBounds = new Rect(0, 0, 500, 2000);
+        final Rect secondaryBounds = new Rect(600, 0, 1100, 2000);
+        final int dividerWidthPx = 100;
+        final int dividerPosition = 300;
+
+        final TaskFragmentContainer mockPrimaryContainer =
+                createMockTaskFragmentContainer(mPrimaryContainerToken, primaryBounds);
+        final TaskFragmentContainer mockSecondaryContainer =
+                createMockTaskFragmentContainer(mSecondaryContainerToken, secondaryBounds);
+        when(mSplitContainer.getPrimaryContainer()).thenReturn(mockPrimaryContainer);
+        when(mSplitContainer.getSecondaryContainer()).thenReturn(mockSecondaryContainer);
+
+        assertEquals(
+                0.3f, // Primary is 300px after dragging.
+                DividerPresenter.calculateNewSplitRatio(
+                        mSplitContainer,
+                        dividerPosition,
+                        taskBounds,
+                        dividerWidthPx,
+                        true /* isVerticalSplit */,
+                        false /* isReversedLayout */),
+                0.0001 /* delta */);
+    }
+
+    @Test
+    public void testCalculateNewSplitRatio_bottomToTop() {
+        // Primary is at bottom. Secondary is at top.
+        // primary=500px; secondary=500px; divider=100px; total=1100px.
+        final Rect taskBounds = new Rect(0, 0, 2000, 1100);
+        final Rect primaryBounds = new Rect(0, 0, 2000, 1100);
+        final Rect secondaryBounds = new Rect(0, 0, 2000, 500);
+        final int dividerWidthPx = 100;
+        final int dividerPosition = 300;
+
+        final TaskFragmentContainer mockPrimaryContainer =
+                createMockTaskFragmentContainer(mPrimaryContainerToken, primaryBounds);
+        final TaskFragmentContainer mockSecondaryContainer =
+                createMockTaskFragmentContainer(mSecondaryContainerToken, secondaryBounds);
+        when(mSplitContainer.getPrimaryContainer()).thenReturn(mockPrimaryContainer);
+        when(mSplitContainer.getSecondaryContainer()).thenReturn(mockSecondaryContainer);
+
+        assertEquals(
+                // After dragging, secondary is [0, 0, 2000, 300]. Primary is [0, 400, 2000, 1100].
+                0.7f,
+                DividerPresenter.calculateNewSplitRatio(
+                        mSplitContainer,
+                        dividerPosition,
+                        taskBounds,
+                        dividerWidthPx,
+                        false /* isVerticalSplit */,
+                        true /* isReversedLayout */),
+                0.0001 /* delta */);
+    }
+
     private TaskFragmentContainer createMockTaskFragmentContainer(
             @NonNull IBinder token, @NonNull Rect bounds) {
         final TaskFragmentContainer container = mock(TaskFragmentContainer.class);