Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d44492f0 authored by Jiaming Liu's avatar Jiaming Liu Committed by Android (Google) Code Review
Browse files

Merge "[Divider] Adjust container bounds based on DividerAttributes" into main

parents 87365831 12e3a3aa
Loading
Loading
Loading
Loading
+159 −0
Original line number Diff line number Diff line
/*
 * 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();
    }
}
+28 −12
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package androidx.window.extensions.embedding;

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 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
    })
    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 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
            CONTAINER_POSITION_RIGHT,
            CONTAINER_POSITION_BOTTOM,
    })
    private @interface ContainerPosition {}
    @interface ContainerPosition {}

    /**
     * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer,
@@ -738,6 +739,15 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
    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 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
     */
    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 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
    @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 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
    @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 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
    @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);
    }
+159 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}
+4 −1
Original line number Diff line number Diff line
@@ -294,7 +294,10 @@ public class SplitControllerTest {
        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