Merge "[Divider] Adjust container bounds based on DividerAttributes" 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
new file mode 100644
index 0000000..100185b
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
@@ -0,0 +1,159 @@
+/*
+ * 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 static android.util.TypedValue.COMPLEX_UNIT_DIP;
+
+import static androidx.window.extensions.embedding.DividerAttributes.RATIO_UNSET;
+import static androidx.window.extensions.embedding.DividerAttributes.WIDTH_UNSET;
+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;
+import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_TOP;
+
+import android.annotation.Nullable;
+import android.app.ActivityThread;
+import android.content.Context;
+import android.util.TypedValue;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.window.flags.Flags;
+
+/**
+ * Manages the rendering and interaction of the divider.
+ */
+class DividerPresenter {
+    // TODO(b/327067596) Update based on UX guidance.
+    @VisibleForTesting static final float DEFAULT_MIN_RATIO = 0.35f;
+    @VisibleForTesting static final float DEFAULT_MAX_RATIO = 0.65f;
+    @VisibleForTesting static final int DEFAULT_DIVIDER_WIDTH_DP = 24;
+
+    static int getDividerWidthPx(@NonNull DividerAttributes dividerAttributes) {
+        int dividerWidthDp = dividerAttributes.getWidthDp();
+
+        // TODO(b/329193115) support divider on secondary display
+        final Context applicationContext = ActivityThread.currentActivityThread().getApplication();
+
+        return (int) TypedValue.applyDimension(
+                COMPLEX_UNIT_DIP,
+                dividerWidthDp,
+                applicationContext.getResources().getDisplayMetrics());
+    }
+
+    /**
+     * Returns the container bound offset that is a result of the presence of a divider.
+     *
+     * The offset is the relative position change for the container edge that is next to the divider
+     * due to the presence of the divider. The value could be negative or positive depending on the
+     * container position. Positive values indicate that the edge is shifting towards the right
+     * (or bottom) and negative values indicate that the edge is shifting towards the left (or top).
+     *
+     * @param splitAttributes the {@link SplitAttributes} of the split container that we want to
+     *                        compute bounds offset.
+     * @param position        the position of the container in the split that we want to compute
+     *                        bounds offset for.
+     * @return the bounds offset in pixels.
+     */
+    static int getBoundsOffsetForDivider(
+            @NonNull SplitAttributes splitAttributes,
+            @SplitPresenter.ContainerPosition int position) {
+        if (!Flags.activityEmbeddingInteractiveDividerFlag()) {
+            return 0;
+        }
+        final DividerAttributes dividerAttributes = splitAttributes.getDividerAttributes();
+        if (dividerAttributes == null) {
+            return 0;
+        }
+        final int dividerWidthPx = getDividerWidthPx(dividerAttributes);
+        return getBoundsOffsetForDivider(
+                dividerWidthPx,
+                splitAttributes.getSplitType(),
+                position);
+    }
+
+    @VisibleForTesting
+    static int getBoundsOffsetForDivider(
+            int dividerWidthPx,
+            @NonNull SplitAttributes.SplitType splitType,
+            @SplitPresenter.ContainerPosition int position) {
+        if (splitType instanceof SplitAttributes.SplitType.ExpandContainersSplitType) {
+            // No divider is needed for the ExpandContainersSplitType.
+            return 0;
+        }
+        int primaryOffset;
+        if (splitType instanceof final SplitAttributes.SplitType.RatioSplitType splitRatio) {
+            // When a divider is present, both containers shrink by an amount proportional to their
+            // split ratio and sum to the width of the divider, so that the ending sizing of the
+            // containers still maintain the same ratio.
+            primaryOffset = (int) (dividerWidthPx * splitRatio.getRatio());
+        } else {
+            // Hinge split type (and other future split types) will have the divider width equally
+            // distributed to both containers.
+            primaryOffset = dividerWidthPx / 2;
+        }
+        final int secondaryOffset = dividerWidthPx - primaryOffset;
+        switch (position) {
+            case CONTAINER_POSITION_LEFT:
+            case CONTAINER_POSITION_TOP:
+                return -primaryOffset;
+            case CONTAINER_POSITION_RIGHT:
+            case CONTAINER_POSITION_BOTTOM:
+                return secondaryOffset;
+            default:
+                throw new IllegalArgumentException("Unknown position:" + position);
+        }
+    }
+
+    /**
+     * Sanitizes and sets default values in the {@link DividerAttributes}.
+     *
+     * Unset values will be set with system default values. See
+     * {@link DividerAttributes#WIDTH_UNSET} and {@link DividerAttributes#RATIO_UNSET}.
+     *
+     * @param dividerAttributes input {@link DividerAttributes}
+     * @return a {@link DividerAttributes} that has all values properly set.
+     */
+    @Nullable
+    static DividerAttributes sanitizeDividerAttributes(
+            @Nullable DividerAttributes dividerAttributes) {
+        if (dividerAttributes == null) {
+            return null;
+        }
+        int widthDp = dividerAttributes.getWidthDp();
+        if (widthDp == WIDTH_UNSET) {
+            widthDp = DEFAULT_DIVIDER_WIDTH_DP;
+        }
+
+        float minRatio = dividerAttributes.getPrimaryMinRatio();
+        if (minRatio == RATIO_UNSET) {
+            minRatio = DEFAULT_MIN_RATIO;
+        }
+
+        float maxRatio = dividerAttributes.getPrimaryMaxRatio();
+        if (maxRatio == RATIO_UNSET) {
+            maxRatio = DEFAULT_MAX_RATIO;
+        }
+
+        return new DividerAttributes.Builder(dividerAttributes)
+                .setWidthDp(widthDp)
+                .setPrimaryMinRatio(minRatio)
+                .setPrimaryMaxRatio(maxRatio)
+                .build();
+    }
+}
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 b53b9c5..f680694 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -18,6 +18,7 @@
 
 import static android.content.pm.PackageManager.MATCH_ALL;
 
+import static androidx.window.extensions.embedding.DividerPresenter.getBoundsOffsetForDivider;
 import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK;
 
 import android.app.Activity;
@@ -85,10 +86,10 @@
     })
     private @interface Position {}
 
-    private static final int CONTAINER_POSITION_LEFT = 0;
-    private static final int CONTAINER_POSITION_TOP = 1;
-    private static final int CONTAINER_POSITION_RIGHT = 2;
-    private static final int CONTAINER_POSITION_BOTTOM = 3;
+    static final int CONTAINER_POSITION_LEFT = 0;
+    static final int CONTAINER_POSITION_TOP = 1;
+    static final int CONTAINER_POSITION_RIGHT = 2;
+    static final int CONTAINER_POSITION_BOTTOM = 3;
 
     @IntDef(value = {
             CONTAINER_POSITION_LEFT,
@@ -96,7 +97,7 @@
             CONTAINER_POSITION_RIGHT,
             CONTAINER_POSITION_BOTTOM,
     })
-    private @interface ContainerPosition {}
+    @interface ContainerPosition {}
 
     /**
      * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer,
@@ -738,6 +739,15 @@
     private SplitAttributes sanitizeSplitAttributes(@NonNull TaskProperties taskProperties,
             @NonNull SplitAttributes splitAttributes,
             @Nullable Pair<Size, Size> minDimensionsPair) {
+        // Sanitize the DividerAttributes and set default values.
+        if (splitAttributes.getDividerAttributes() != null) {
+            splitAttributes = new SplitAttributes.Builder(splitAttributes)
+                    .setDividerAttributes(
+                            DividerPresenter.sanitizeDividerAttributes(
+                                    splitAttributes.getDividerAttributes())
+                    ).build();
+        }
+
         if (minDimensionsPair == null) {
             return splitAttributes;
         }
@@ -930,18 +940,18 @@
      */
     private static SplitAttributes updateSplitAttributesType(
             @NonNull SplitAttributes splitAttributes, @NonNull SplitType splitTypeToUpdate) {
-        return new SplitAttributes.Builder()
+        return new SplitAttributes.Builder(splitAttributes)
                 .setSplitType(splitTypeToUpdate)
-                .setLayoutDirection(splitAttributes.getLayoutDirection())
-                .setAnimationBackground(splitAttributes.getAnimationBackground())
                 .build();
     }
 
     @NonNull
     private Rect getLeftContainerBounds(@NonNull Configuration taskConfiguration,
             @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) {
+        final int dividerOffset = getBoundsOffsetForDivider(
+                splitAttributes, CONTAINER_POSITION_LEFT);
         final int right = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes,
-                CONTAINER_POSITION_LEFT, foldingFeature);
+                CONTAINER_POSITION_LEFT, foldingFeature) + dividerOffset;
         final Rect taskBounds = taskConfiguration.windowConfiguration.getBounds();
         return new Rect(taskBounds.left, taskBounds.top, right, taskBounds.bottom);
     }
@@ -949,8 +959,10 @@
     @NonNull
     private Rect getRightContainerBounds(@NonNull Configuration taskConfiguration,
             @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) {
+        final int dividerOffset = getBoundsOffsetForDivider(
+                splitAttributes, CONTAINER_POSITION_RIGHT);
         final int left = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes,
-                CONTAINER_POSITION_RIGHT, foldingFeature);
+                CONTAINER_POSITION_RIGHT, foldingFeature) + dividerOffset;
         final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds();
         return new Rect(left, parentBounds.top, parentBounds.right, parentBounds.bottom);
     }
@@ -958,8 +970,10 @@
     @NonNull
     private Rect getTopContainerBounds(@NonNull Configuration taskConfiguration,
             @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) {
+        final int dividerOffset = getBoundsOffsetForDivider(
+                splitAttributes, CONTAINER_POSITION_TOP);
         final int bottom = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes,
-                CONTAINER_POSITION_TOP, foldingFeature);
+                CONTAINER_POSITION_TOP, foldingFeature) + dividerOffset;
         final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds();
         return new Rect(parentBounds.left, parentBounds.top, parentBounds.right, bottom);
     }
@@ -967,8 +981,10 @@
     @NonNull
     private Rect getBottomContainerBounds(@NonNull Configuration taskConfiguration,
             @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) {
+        final int dividerOffset = getBoundsOffsetForDivider(
+                splitAttributes, CONTAINER_POSITION_BOTTOM);
         final int top = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes,
-                CONTAINER_POSITION_BOTTOM, foldingFeature);
+                CONTAINER_POSITION_BOTTOM, foldingFeature) + dividerOffset;
         final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds();
         return new Rect(parentBounds.left, top, parentBounds.right, parentBounds.bottom);
     }
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
new file mode 100644
index 0000000..2a277f4
--- /dev/null
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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 static androidx.window.extensions.embedding.DividerPresenter.getBoundsOffsetForDivider;
+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;
+import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_TOP;
+
+import static org.junit.Assert.assertEquals;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test class for {@link DividerPresenter}.
+ *
+ * Build/Install/Run:
+ *  atest WMJetpackUnitTests:DividerPresenterTest
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DividerPresenterTest {
+    @Test
+    public void testSanitizeDividerAttributes_setDefaultValues() {
+        DividerAttributes attributes =
+                new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE).build();
+        DividerAttributes sanitized = DividerPresenter.sanitizeDividerAttributes(attributes);
+
+        assertEquals(DividerAttributes.DIVIDER_TYPE_DRAGGABLE, sanitized.getDividerType());
+        assertEquals(DividerPresenter.DEFAULT_DIVIDER_WIDTH_DP, sanitized.getWidthDp());
+        assertEquals(DividerPresenter.DEFAULT_MIN_RATIO, sanitized.getPrimaryMinRatio(),
+                0.0f /* delta */);
+        assertEquals(DividerPresenter.DEFAULT_MAX_RATIO, sanitized.getPrimaryMaxRatio(),
+                0.0f /* delta */);
+    }
+
+    @Test
+    public void testSanitizeDividerAttributes_notChangingValidValues() {
+        DividerAttributes attributes =
+                new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE)
+                        .setWidthDp(10)
+                        .setPrimaryMinRatio(0.3f)
+                        .setPrimaryMaxRatio(0.7f)
+                        .build();
+        DividerAttributes sanitized = DividerPresenter.sanitizeDividerAttributes(attributes);
+
+        assertEquals(attributes, sanitized);
+    }
+
+    @Test
+    public void testGetBoundsOffsetForDivider_ratioSplitType() {
+        final int dividerWidthPx = 100;
+        final float splitRatio = 0.25f;
+        final SplitAttributes.SplitType splitType =
+                new SplitAttributes.SplitType.RatioSplitType(splitRatio);
+        final int expectedTopLeftOffset = 25;
+        final int expectedBottomRightOffset = 75;
+
+        assertDividerOffsetEquals(
+                dividerWidthPx, splitType, expectedTopLeftOffset, expectedBottomRightOffset);
+    }
+
+    @Test
+    public void testGetBoundsOffsetForDivider_ratioSplitType_withRounding() {
+        final int dividerWidthPx = 101;
+        final float splitRatio = 0.25f;
+        final SplitAttributes.SplitType splitType =
+                new SplitAttributes.SplitType.RatioSplitType(splitRatio);
+        final int expectedTopLeftOffset = 25;
+        final int expectedBottomRightOffset = 76;
+
+        assertDividerOffsetEquals(
+                dividerWidthPx, splitType, expectedTopLeftOffset, expectedBottomRightOffset);
+    }
+
+    @Test
+    public void testGetBoundsOffsetForDivider_hingeSplitType() {
+        final int dividerWidthPx = 100;
+        final SplitAttributes.SplitType splitType =
+                new SplitAttributes.SplitType.HingeSplitType(
+                        new SplitAttributes.SplitType.RatioSplitType(0.5f));
+
+        final int expectedTopLeftOffset = 50;
+        final int expectedBottomRightOffset = 50;
+
+        assertDividerOffsetEquals(
+                dividerWidthPx, splitType, expectedTopLeftOffset, expectedBottomRightOffset);
+    }
+
+    @Test
+    public void testGetBoundsOffsetForDivider_expandContainersSplitType() {
+        final int dividerWidthPx = 100;
+        final SplitAttributes.SplitType splitType =
+                new SplitAttributes.SplitType.ExpandContainersSplitType();
+        // Always return 0 for ExpandContainersSplitType as divider is not needed.
+        final int expectedTopLeftOffset = 0;
+        final int expectedBottomRightOffset = 0;
+
+        assertDividerOffsetEquals(
+                dividerWidthPx, splitType, expectedTopLeftOffset, expectedBottomRightOffset);
+    }
+
+    private void assertDividerOffsetEquals(
+            int dividerWidthPx,
+            @NonNull SplitAttributes.SplitType splitType,
+            int expectedTopLeftOffset,
+            int expectedBottomRightOffset) {
+        int offset = getBoundsOffsetForDivider(
+                dividerWidthPx,
+                splitType,
+                CONTAINER_POSITION_LEFT
+        );
+        assertEquals(-expectedTopLeftOffset, offset);
+
+        offset = getBoundsOffsetForDivider(
+                dividerWidthPx,
+                splitType,
+                CONTAINER_POSITION_RIGHT
+        );
+        assertEquals(expectedBottomRightOffset, offset);
+
+        offset = getBoundsOffsetForDivider(
+                dividerWidthPx,
+                splitType,
+                CONTAINER_POSITION_TOP
+        );
+        assertEquals(-expectedTopLeftOffset, offset);
+
+        offset = getBoundsOffsetForDivider(
+                dividerWidthPx,
+                splitType,
+                CONTAINER_POSITION_BOTTOM
+        );
+        assertEquals(expectedBottomRightOffset, offset);
+    }
+}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index bdeeb73..cdb37ac 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -294,7 +294,10 @@
         doReturn(tf).when(splitContainer).getPrimaryContainer();
         doReturn(tf).when(splitContainer).getSecondaryContainer();
         doReturn(createTestTaskContainer()).when(splitContainer).getTaskContainer();
-        doReturn(createSplitRule(mActivity, mActivity)).when(splitContainer).getSplitRule();
+        final SplitRule splitRule = createSplitRule(mActivity, mActivity);
+        doReturn(splitRule).when(splitContainer).getSplitRule();
+        doReturn(splitRule.getDefaultSplitAttributes())
+                .when(splitContainer).getDefaultSplitAttributes();
         taskContainer = mSplitController.getTaskContainer(TASK_ID);
         taskContainer.addSplitContainer(splitContainer);
         // Add a mock SplitContainer on top of splitContainer