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

Commit 343a4f16 authored by Jeremy Sim's avatar Jeremy Sim Committed by Android (Google) Code Review
Browse files

Merge "Implement base functionality for flexible 2-app split" into main

parents a4585383 7a1f0e47
Loading
Loading
Loading
Loading
+20 −1
Original line number Diff line number Diff line
@@ -61,11 +61,30 @@ public class SplitScreenConstants {
    @IntDef(prefix = {"SPLIT_POSITION_"}, value = {
            SPLIT_POSITION_UNDEFINED,
            SPLIT_POSITION_TOP_OR_LEFT,
            SPLIT_POSITION_BOTTOM_OR_RIGHT
            SPLIT_POSITION_BOTTOM_OR_RIGHT,
    })
    public @interface SplitPosition {
    }

    // These SPLIT_INDEX constants will be used in the same way as the above SPLIT_POSITION ints,
    // but scalable to n apps. Eventually, SPLIT_POSITION can be deprecated and only the below
    // will be used.
    public static final int SPLIT_INDEX_UNDEFINED = -1;
    public static final int SPLIT_INDEX_0 = 0;
    public static final int SPLIT_INDEX_1 = 1;
    public static final int SPLIT_INDEX_2 = 2;
    public static final int SPLIT_INDEX_3 = 3;

    @IntDef(prefix = {"SPLIT_INDEX_"}, value = {
            SPLIT_INDEX_UNDEFINED,
            SPLIT_INDEX_0,
            SPLIT_INDEX_1,
            SPLIT_INDEX_2,
            SPLIT_INDEX_3
    })
    public @interface SplitIndex {
    }

    /**
     * A snap target for two apps, where the split is 33-66. With FLAG_ENABLE_FLEXIBLE_SPLIT,
     * only used on tablets.
+29 −6
Original line number Diff line number Diff line
@@ -19,9 +19,11 @@ package com.android.wm.shell.common.split;
import static android.view.WindowManager.DOCKED_LEFT;
import static android.view.WindowManager.DOCKED_RIGHT;

import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_10_90;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_33_66;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_66_33;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_90_10;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_END_AND_DISMISS;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_MINIMIZE;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_NONE;
@@ -33,6 +35,9 @@ import android.graphics.Rect;

import androidx.annotation.Nullable;

import com.android.wm.shell.Flags;
import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition;

import java.util.ArrayList;

/**
@@ -79,7 +84,9 @@ public class DividerSnapAlgorithm {
    private final int mTaskHeightInMinimizedMode;
    private final float mFixedRatio;
    /** Allows split ratios to calculated dynamically instead of using {@link #mFixedRatio}. */
    private final boolean mAllowFlexibleSplitRatios;
    private final boolean mCalculateRatiosBasedOnAvailableSpace;
    /** Allows split ratios that go offscreen (a.k.a. "flexible split") */
    private final boolean mAllowOffscreenRatios;
    private final boolean mIsHorizontalDivision;

    /** The first target which is still splitting the screen */
@@ -119,8 +126,11 @@ public class DividerSnapAlgorithm {
                com.android.internal.R.fraction.docked_stack_divider_fixed_ratio, 1, 1);
        mMinimalSizeResizableTask = res.getDimensionPixelSize(
                com.android.internal.R.dimen.default_minimal_size_resizable_task);
        mAllowFlexibleSplitRatios = res.getBoolean(
        mCalculateRatiosBasedOnAvailableSpace = res.getBoolean(
                com.android.internal.R.bool.config_flexibleSplitRatios);
        // If this is a small screen or a foldable, use offscreen ratios
        mAllowOffscreenRatios =
                Flags.enableFlexibleTwoAppSplit() && SplitScreenUtils.allowOffscreenRatios(res);
        mTaskHeightInMinimizedMode = isHomeResizable ? res.getDimensionPixelSize(
                com.android.internal.R.dimen.task_height_of_minimized_mode) : 0;
        calculateTargets(isHorizontalDivision, dockSide);
@@ -233,6 +243,11 @@ public class DividerSnapAlgorithm {
        return mFirstSplitTarget.position < position && position < mLastSplitTarget.position;
    }

    /** Returns if we are currently on a device/screen that supports split apps going offscreen. */
    public boolean areOffscreenRatiosSupported() {
        return mAllowOffscreenRatios;
    }

    private SnapTarget snap(int position, boolean hardDismiss) {
        if (shouldApplyFreeSnapMode(position)) {
            return new SnapTarget(position, SNAP_TO_NONE);
@@ -283,10 +298,14 @@ public class DividerSnapAlgorithm {

    private void addNonDismissingTargets(boolean isHorizontalDivision, int topPosition,
            int bottomPosition, int dividerMax) {
        maybeAddTarget(topPosition, topPosition - getStartInset(), SNAP_TO_2_33_66);
        @PersistentSnapPosition int firstTarget =
                mAllowOffscreenRatios ? SNAP_TO_2_10_90 : SNAP_TO_2_33_66;
        @PersistentSnapPosition int lastTarget =
                mAllowOffscreenRatios ? SNAP_TO_2_90_10 : SNAP_TO_2_66_33;
        maybeAddTarget(topPosition, topPosition - getStartInset(), firstTarget);
        addMiddleTarget(isHorizontalDivision);
        maybeAddTarget(bottomPosition,
                dividerMax - getEndInset() - (bottomPosition + mDividerSize), SNAP_TO_2_66_33);
                dividerMax - getEndInset() - (bottomPosition + mDividerSize), lastTarget);
    }

    private void addFixedDivisionTargets(boolean isHorizontalDivision, int dividerMax) {
@@ -295,7 +314,11 @@ public class DividerSnapAlgorithm {
                ? mDisplayHeight - mInsets.bottom
                : mDisplayWidth - mInsets.right;
        int size = (int) (mFixedRatio * (end - start)) - mDividerSize / 2;
        if (mAllowFlexibleSplitRatios) {

        if (mAllowOffscreenRatios) {
            // TODO (b/349828130): This is a placeholder value, real measurements to come
            size = (int) (0.3f * (end - start)) - mDividerSize / 2;
        } else if (mCalculateRatiosBasedOnAvailableSpace) {
            size = Math.max(size, mMinimalSizeResizableTask);
        }
        int topPosition = start + size;
@@ -324,7 +347,7 @@ public class DividerSnapAlgorithm {
     * meets the minimal size requirement.
     */
    private void maybeAddTarget(int position, int smallerSize, @SnapPosition int snapPosition) {
        if (smallerSize >= mMinimalSizeResizableTask) {
        if (smallerSize >= mMinimalSizeResizableTask || mAllowOffscreenRatios) {
            mTargets.add(new SnapTarget(position, snapPosition));
        }
    }
+36 −0
Original line number Diff line number Diff line
@@ -30,6 +30,8 @@ import static com.android.wm.shell.shared.animation.Interpolators.DIM_INTERPOLAT
import static com.android.wm.shell.shared.animation.Interpolators.EMPHASIZED;
import static com.android.wm.shell.shared.animation.Interpolators.LINEAR;
import static com.android.wm.shell.shared.animation.Interpolators.SLOWDOWN_INTERPOLATOR;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_10_90;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_90_10;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_END_AND_DISMISS;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_START_AND_DISMISS;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
@@ -438,12 +440,31 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
            dividerBounds.right = dividerBounds.left + mDividerWindowWidth;
            bounds1.right = position;
            bounds2.left = bounds1.right + mDividerSize;

            // For flexible split, expand app offscreen as well
            if (mDividerSnapAlgorithm.areOffscreenRatiosSupported()) {
                if (position <= mDividerSnapAlgorithm.getMiddleTarget().position) {
                    bounds1.left = bounds1.right - bounds2.width();
                } else {
                    bounds2.right = bounds2.left + bounds1.width();
                }
            }

        } else {
            position += mRootBounds.top;
            dividerBounds.top = position - mDividerInsets;
            dividerBounds.bottom = dividerBounds.top + mDividerWindowWidth;
            bounds1.bottom = position;
            bounds2.top = bounds1.bottom + mDividerSize;

            // For flexible split, expand app offscreen as well
            if (mDividerSnapAlgorithm.areOffscreenRatiosSupported()) {
                if (position <= mDividerSnapAlgorithm.getMiddleTarget().position) {
                    bounds1.top = bounds1.bottom - bounds2.width();
                } else {
                    bounds2.bottom = bounds2.top + bounds1.width();
                }
            }
        }
        DockedDividerUtils.sanitizeStackBounds(bounds1, true /** topLeft */);
        DockedDividerUtils.sanitizeStackBounds(bounds2, false /** topLeft */);
@@ -669,6 +690,21 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
                });
    }

    /**
     * Moves the divider to the other side of the screen. Does nothing if the divider is in the
     * center.
     * TODO (b/349828130): Currently only supports the two-app case. For n-apps,
     *  DividerSnapAlgorithm will need to be refactored, and this function will change as well.
     */
    public void flingDividerToOtherSide(@PersistentSnapPosition int currentSnapPosition) {
        switch (currentSnapPosition) {
            case SNAP_TO_2_10_90 ->
                    snapToTarget(mDividerPosition, mDividerSnapAlgorithm.getLastSplitTarget());
            case SNAP_TO_2_90_10 ->
                    snapToTarget(mDividerPosition, mDividerSnapAlgorithm.getFirstSplitTarget());
        }
    }

    @VisibleForTesting
    void flingDividerPosition(int from, int to, int duration,
            @Nullable Runnable flingFinishedCallback) {
+44 −2
Original line number Diff line number Diff line
@@ -18,6 +18,10 @@ package com.android.wm.shell.common.split;

import static com.android.wm.shell.shared.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
import static com.android.wm.shell.shared.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_10_90;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_90_10;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_3_10_45_45;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_3_45_45_10;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
@@ -38,6 +42,8 @@ import com.android.wm.shell.shared.split.SplitScreenConstants;

/** Helper utility class for split screen components to use. */
public class SplitScreenUtils {
    private static final int LARGE_SCREEN_MIN_EDGE_DP = 600;

    /** Reverse the split position. */
    @SplitScreenConstants.SplitPosition
    public static int reverseSplitPosition(@SplitScreenConstants.SplitPosition int position) {
@@ -110,10 +116,9 @@ public class SplitScreenUtils {
            Configuration config) {
        // Compare the max bounds sizes as on near-square devices, the insets may result in a
        // configuration in the other orientation
        final boolean isLargeScreen = config.smallestScreenWidthDp >= 600;
        final Rect maxBounds = config.windowConfiguration.getMaxBounds();
        final boolean isLandscape = maxBounds.width() >= maxBounds.height();
        return isLeftRightSplit(allowLeftRightSplitInPortrait, isLargeScreen, isLandscape);
        return isLeftRightSplit(allowLeftRightSplitInPortrait, isLargeScreen(config), isLandscape);
    }

    /**
@@ -128,4 +133,41 @@ public class SplitScreenUtils {
            return isLandscape;
        }
    }

    /**
     * Returns whether the current config is a large screen (tablet or unfolded foldable)
     */
    public static boolean isLargeScreen(Configuration config) {
        return config.smallestScreenWidthDp >= LARGE_SCREEN_MIN_EDGE_DP;
    }

    /**
     * Returns whether we should allow split ratios to go offscreen or not. If the device is a phone
     * or a foldable (either screen), we allow it.
     */
    public static boolean allowOffscreenRatios(Resources res) {
        return !isLargeScreen(res.getConfiguration())
                ||
                res.getIntArray(com.android.internal.R.array.config_foldedDeviceStates).length != 0;
    }

    /**
     * Within a particular split layout, we label the stages numerically: 0, 1, 2... from left to
     * right (or top to bottom). This function takes in a stage index (0th, 1st, 2nd...) and a
     * PersistentSnapPosition and returns if that particular stage is offscreen in that layout.
     */
    public static boolean isPartiallyOffscreen(int stageIndex,
            @SplitScreenConstants.PersistentSnapPosition int snapPosition) {
        switch(snapPosition) {
            case SNAP_TO_2_10_90:
            case SNAP_TO_3_10_45_45:
                return stageIndex == 0;
            case SNAP_TO_2_90_10:
                return stageIndex == 1;
            case SNAP_TO_3_45_45_10:
                return stageIndex == 2;
            default:
                return false;
        }
    }
}
+35 −0
Original line number Diff line number Diff line
@@ -35,12 +35,19 @@ import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER;

import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_ALIGN_CENTER;
import static com.android.wm.shell.common.split.SplitScreenUtils.isPartiallyOffscreen;
import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition;
import static com.android.wm.shell.common.split.SplitScreenUtils.splitFailureMessage;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN;
import static com.android.wm.shell.shared.TransitionUtil.isClosingType;
import static com.android.wm.shell.shared.TransitionUtil.isOpeningType;
import static com.android.wm.shell.shared.TransitionUtil.isOrderOnly;
import static com.android.wm.shell.shared.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_10_90;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_90_10;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_0;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_1;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_UNDEFINED;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
@@ -117,6 +124,7 @@ import com.android.internal.logging.InstanceId;
import com.android.internal.policy.FoldLockSettingsObserver;
import com.android.internal.protolog.ProtoLog;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.Flags;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
@@ -2053,6 +2061,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
            mSplitLayout.setDividerInteractive(true, false, "onSplitResizeFinish");
        }, mMainStage.getSplitDecorManager(), mSideStage.getSplitDecorManager());

        if (Flags.enableFlexibleTwoAppSplit()) {
            switch (layout.calculateCurrentSnapPosition()) {
                case SNAP_TO_2_10_90 -> grantFocusToPosition(false /* leftOrTop */);
                case SNAP_TO_2_90_10 -> grantFocusToPosition(true /* leftOrTop */);
            }
        }

        mLogger.logResize(mSplitLayout.getDividerPositionAsFraction());
    }

@@ -2506,6 +2521,26 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
                        mTaskOrganizer.applyTransaction(wct);
                    }
                    continue;
                } else if (Flags.enableFlexibleTwoAppSplit() && isOrderOnly(change)) {
                    int focusedStageIndex = SPLIT_INDEX_UNDEFINED;
                    if (taskInfo.token.equals(mMainStage.mRootTaskInfo.token)) {
                        focusedStageIndex = mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT
                                ? SPLIT_INDEX_0 : SPLIT_INDEX_1;
                    } else if (taskInfo.token.equals(mSideStage.mRootTaskInfo.token)) {
                        focusedStageIndex = mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT
                                ? SPLIT_INDEX_1 : SPLIT_INDEX_0;
                    }

                    if (focusedStageIndex != SPLIT_INDEX_UNDEFINED) {
                        @PersistentSnapPosition int currentSnapPosition =
                                mSplitLayout.calculateCurrentSnapPosition();
                        boolean offscreenTaskFocused =
                                isPartiallyOffscreen(focusedStageIndex, currentSnapPosition);

                        if (offscreenTaskFocused) {
                            mSplitLayout.flingDividerToOtherSide(currentSnapPosition);
                        }
                    }
                }
                final StageTaskListener stage = getStageOfTask(taskInfo);
                if (stage == null) {