Merge "Implement base functionality for flexible 2-app split" into main
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
index 61cd1c3..5a2a723 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java
@@ -61,11 +61,30 @@
@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.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
index 9f100fa..0b2b3e7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
@@ -19,9 +19,11 @@
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 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 @@
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 @@
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 @@
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 @@
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 @@
? 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 @@
* 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));
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index 83ffaf4..c9c3aa0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -30,6 +30,8 @@
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 @@
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 @@
});
}
+ /**
+ * 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) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java
index bdbcb46..65bf389 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java
@@ -18,6 +18,10 @@
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 @@
/** 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 @@
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 @@
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;
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index dc9ed91..83acea2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -35,12 +35,19 @@
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.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 @@
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 @@
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) {