Support multiple tasks per split screen via SplitRemoteTargetHandles

It enables support for assigning SplitRemoteTargetHandles to multiple tasks within a split-screen.

The changes include:

- Retrieving the list of split-screen tasks directly from `SplitBounds`.
- Iterating through the task list and setting the remoteTargetHandle for each individual task.
- General code refactoring to set the remoteTargetHandle.

Bug: 346295292
Bug: 360782849
Flag: com.android.wm.shell.enable_flexible_two_app_split
Test: manual test.
Video: Before: http://recall/-/gvQq7Z7JECwxnxKrBmr3wt/fnHVbaVwMWfvrOaez5eO6L
Video: After: http://recall/-/gvQq7Z7JECwxnxKrBmr3wt/dzv6tZMlAYG1bBBtYdsWxF

Change-Id: I1e8c8316947b0d4fd72c9cf36dff8ecd99d2f170
This commit is contained in:
Simon (Qiong) Sun
2025-05-21 17:51:09 +00:00
parent 10b72a252d
commit 00063bcf2f
7 changed files with 184 additions and 99 deletions
@@ -16,8 +16,8 @@
package com.android.quickstep;
import static com.android.wm.shell.shared.split.SplitBounds.KEY_EXTRA_SPLIT_BOUNDS;
import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.enableMultipleDesktops;
import static com.android.wm.shell.shared.split.SplitBounds.KEY_EXTRA_SPLIT_BOUNDS;
import android.app.WindowConfiguration;
import android.content.Context;
@@ -36,6 +36,8 @@ import com.android.quickstep.util.TransformParams;
import com.android.wm.shell.shared.GroupedTaskInfo;
import com.android.wm.shell.shared.split.SplitBounds;
import kotlin.collections.CollectionsKt;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -50,8 +52,7 @@ public class RemoteTargetGluer {
// This is the default number of handles to create when we don't know how many tasks are running
// (e.g. if we're in split screen). Allocate extra for potential tasks overlaid, like volume.
private static final int DEFAULT_NUM_HANDLES = 4;
private static final int DEFAULT_NUM_HANDLES = 10;
private RemoteTargetHandle[] mRemoteTargetHandles;
private SplitBounds mSplitBounds;
@@ -60,7 +61,8 @@ public class RemoteTargetGluer {
*/
public RemoteTargetGluer(Context context, BaseContainerInterface sizingStrategy,
RemoteAnimationTargets targets, boolean forDesktop) {
init(context, sizingStrategy, targets.apps.length, forDesktop);
mRemoteTargetHandles = createHandles(context, sizingStrategy, forDesktop,
targets.apps.length);
}
/**
@@ -75,7 +77,8 @@ public class RemoteTargetGluer {
if (groupedTaskInfo != null && groupedTaskInfo.isBaseType(GroupedTaskInfo.TYPE_DESK)) {
// Allocate +1 to account for the DesktopWallpaperActivity added to the desk.
int numHandles = groupedTaskInfo.getTaskInfoList().size() + 1;
init(context, sizingStrategy, numHandles, /* forDesktop = */ true);
mRemoteTargetHandles = createHandles(context, sizingStrategy,
/* forDesktop = */ true, numHandles);
return;
}
} else {
@@ -84,32 +87,35 @@ public class RemoteTargetGluer {
if (visibleTasksCount > 0) {
// Allocate +1 to account for the DesktopWallpaperActivity added to the desk.
int numHandles = visibleTasksCount + 1;
init(context, sizingStrategy, numHandles, /* forDesktop = */ true);
mRemoteTargetHandles = createHandles(context, sizingStrategy,
/* forDesktop = */ true, numHandles);
return;
}
}
// Assume 2 handles needed for split, scale down as needed later on when we actually
// get remote targets
init(context, sizingStrategy, DEFAULT_NUM_HANDLES, /* forDesktop = */ false);
}
private void init(Context context, BaseContainerInterface sizingStrategy, int numHandles,
boolean forDesktop) {
mRemoteTargetHandles = createHandles(context, sizingStrategy, numHandles, forDesktop);
mRemoteTargetHandles = createHandles(context, sizingStrategy, /* forDesktop = */ false,
DEFAULT_NUM_HANDLES);
}
private RemoteTargetHandle[] createHandles(Context context,
BaseContainerInterface sizingStrategy, int numHandles, boolean forDesktop) {
BaseContainerInterface sizingStrategy, boolean forDesktop, int numHandles) {
RemoteTargetHandle[] handles = new RemoteTargetHandle[numHandles];
for (int i = 0; i < numHandles; i++) {
TaskViewSimulator tvs = new TaskViewSimulator(context, sizingStrategy, forDesktop , i);
TransformParams transformParams = new TransformParams();
handles[i] = new RemoteTargetHandle(tvs, transformParams);
handles[i] = createHandle(context, sizingStrategy, forDesktop, i);
}
return handles;
}
private RemoteTargetHandle createHandle(Context context,
BaseContainerInterface sizingStrategy, boolean forDesktop, int taskIndex) {
TaskViewSimulator tvs = new TaskViewSimulator(
context, sizingStrategy, forDesktop , taskIndex);
TransformParams transformParams = new TransformParams();
return new RemoteTargetHandle(tvs, transformParams);
}
/**
* Pairs together {@link TaskViewSimulator}s and {@link TransformParams} into a
* {@link RemoteTargetHandle}
@@ -145,8 +151,6 @@ public class RemoteTargetGluer {
* the left/top task, index 1 right/bottom.
*/
public RemoteTargetHandle[] assignTargetsForSplitScreen(RemoteAnimationTargets targets) {
resizeRemoteTargetHandles(targets);
// If we are in a true split screen case (2 apps running on screen), either:
// a) mSplitBounds was already set (from the clicked GroupedTaskView)
// b) A SplitBounds was passed up from shell (via AbsSwipeUpHandler)
@@ -160,72 +164,78 @@ public class RemoteTargetGluer {
mRemoteTargetHandles.length + " appsLength: " + targets.apps.length);
if (mRemoteTargetHandles.length == 1) {
resizeRemoteTargetHandles(targets);
// Single fullscreen app
// If we're not in split screen, the splitIds count doesn't really matter since we
// should always hit this case.
mRemoteTargetHandles[0].mTransformParams.setTargetSet(targets);
if (targets.apps.length > 0) {
// Unclear why/when target.apps length == 0, but it sure does happen :(
mRemoteTargetHandles[0].mTaskViewSimulator.setPreview(targets.apps[0], null);
}
setRemoteTargetHandle(targets,
targets.apps.length > 0 ? targets.apps[0] : null,
/* targetsToExclude = */ null, /* transitionInfo = */ null,
/* splitBounds = */ null, /* taskIndex = */ 0);
} else if (!containsSplitTargets) {
resizeRemoteTargetHandles(targets);
// Single App + Assistant
for (int i = 0; i < mRemoteTargetHandles.length; i++) {
mRemoteTargetHandles[i].mTransformParams.setTargetSet(targets);
mRemoteTargetHandles[i].mTaskViewSimulator.setPreview(targets.apps[i], null);
}
} else {
// Split apps (+ maybe assistant)
RemoteAnimationTarget topLeftTarget = targets.findTask(mSplitBounds.leftTopTaskId);
RemoteAnimationTarget bottomRightTarget = targets.findTask(
mSplitBounds.rightBottomTaskId);
if (topLeftTarget == null) {
Log.e(TAG, "topLeftTarget not found. mSplitBounds: " + mSplitBounds);
}
if (bottomRightTarget == null) {
Log.e(TAG, "bottomRightTarget not found. mSplitBounds: " + mSplitBounds);
}
List<RemoteAnimationTarget> overlayTargets = Arrays.stream(targets.apps).filter(
target -> target.windowConfiguration.getWindowingMode()
!= WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW).toList();
// remoteTargetHandle[0] denotes topLeft task, so we pass in the bottomRight to exclude,
// vice versa
mRemoteTargetHandles[0].mTransformParams.setTargetSet(
createRemoteAnimationTargetsForTarget(targets,
Collections.singletonList(bottomRightTarget)));
mRemoteTargetHandles[0].mTaskViewSimulator.setPreview(topLeftTarget, mSplitBounds);
mRemoteTargetHandles[1].mTransformParams.setTargetSet(
createRemoteAnimationTargetsForTarget(targets,
Collections.singletonList(topLeftTarget)));
mRemoteTargetHandles[1].mTaskViewSimulator.setPreview(bottomRightTarget, mSplitBounds);
// Set the remaining overlay tasks to be their own TaskViewSimulator as fullscreen tasks
if (!overlayTargets.isEmpty()) {
ArrayList<RemoteAnimationTarget> targetsToExclude = new ArrayList<>();
targetsToExclude.add(topLeftTarget);
targetsToExclude.add(bottomRightTarget);
// Start i at 2 to account for top/left and bottom/right split handles already made
for (int i = 2; i < targets.apps.length; i++) {
if (i >= mRemoteTargetHandles.length) {
Log.e(TAG, String.format("Attempting to animate an untracked target"
+ " (%d handles allocated, but %d want to animate)",
mRemoteTargetHandles.length, targets.apps.length));
break;
}
mRemoteTargetHandles[i].mTransformParams.setTargetSet(
createRemoteAnimationTargetsForTarget(targets, targetsToExclude));
mRemoteTargetHandles[i].mTaskViewSimulator.setPreview(
overlayTargets.get(i - 2));
}
setRemoteTargetHandle(targets, targets.apps[i], /* targetsToExclude = */ null,
/* transitionInfo = */ null, /* splitBounds = */ null, /* taskIndex = */ i);
}
} else if (mSplitBounds != null) {
setSplitRemoteTargetHandles(targets);
}
return mRemoteTargetHandles;
}
private void setSplitRemoteTargetHandles(RemoteAnimationTargets targets) {
// Split apps (+ maybe assistant)
final List<Integer> leftTopTargetIds = mSplitBounds.leftTopTaskIds;
final List<Integer> rightBottomTargetIds = mSplitBounds.rightBottomTaskIds;
if (leftTopTargetIds.isEmpty() || rightBottomTargetIds.isEmpty()) {
throw new IllegalStateException("The target ids is invalid: mSplitBounds = "
+ mSplitBounds);
}
final List<RemoteAnimationTarget> leftTopTargets =
CollectionsKt.mapNotNull(leftTopTargetIds, targets::findTask);
final List<RemoteAnimationTarget> rightBottomTargets =
CollectionsKt.mapNotNull(rightBottomTargetIds, targets::findTask);
final List<RemoteAnimationTarget> overlayTargets = Arrays.stream(targets.apps).filter(
target -> isOverlayTarget(target, leftTopTargets,
rightBottomTargets)).toList();
final int handleCount = leftTopTargets.size() + rightBottomTargets.size()
+ overlayTargets.size();
if (handleCount > targets.apps.length) {
throw new IllegalStateException("Attempting to animate app count:" + handleCount
+ "but the total app count: " + targets.apps.length);
}
if (handleCount > mRemoteTargetHandles.length) {
throw new IllegalStateException("Attempting to animate app count:" + handleCount
+ "but the max handle count: " + mRemoteTargetHandles.length);
}
if (handleCount < mRemoteTargetHandles.length) {
reduceRemoteTargetHandles(handleCount);
}
int taskIndex = 0;
for (final RemoteAnimationTarget target : leftTopTargets) {
setRemoteTargetHandle(targets, target, rightBottomTargets, /* transitionInfo = */ null,
mSplitBounds, taskIndex++);
}
for (final RemoteAnimationTarget target : rightBottomTargets) {
setRemoteTargetHandle(targets, target, leftTopTargets, /* transitionInfo = */ null,
mSplitBounds, taskIndex++);
}
// Set the remaining overlay tasks to be their own TaskViewSimulator as fullscreen tasks
if (!overlayTargets.isEmpty()) {
List<RemoteAnimationTarget> targetsToExclude = new ArrayList<>(leftTopTargets);
targetsToExclude.addAll(rightBottomTargets);
for (final RemoteAnimationTarget target : overlayTargets) {
setRemoteTargetHandle(targets, target, targetsToExclude,
/* transitionInfo = */ null, /* splitBounds = */ null, taskIndex++);
}
}
}
/**
* Similar to {@link #assignTargets(RemoteAnimationTargets)}, except this creates distinct
* transform params per app in {@code targets.apps} list.
@@ -238,28 +248,67 @@ public class RemoteTargetGluer {
RemoteAnimationTarget primaryTaskTarget = targets.apps[i];
List<RemoteAnimationTarget> excludeTargets = Arrays.stream(targets.apps)
.filter(target -> target.taskId != primaryTaskTarget.taskId).toList();
mRemoteTargetHandles[i].mTransformParams.setTargetSet(
createRemoteAnimationTargetsForTarget(targets, excludeTargets));
mRemoteTargetHandles[i].mTransformParams.setTransitionInfo(transitionInfo);
mRemoteTargetHandles[i].mTaskViewSimulator.setPreview(primaryTaskTarget, null);
setRemoteTargetHandle(targets, primaryTaskTarget, excludeTargets, transitionInfo,
/* splitBounds = */ null, i);
}
return mRemoteTargetHandles;
}
private boolean isOverlayTarget(@NonNull RemoteAnimationTarget target,
@NonNull List<RemoteAnimationTarget> leftTopTargets,
@NonNull List<RemoteAnimationTarget> rightBottomTargets) {
return target.windowConfiguration.getWindowingMode()
!= WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
&& !leftTopTargets.contains(target)
&& !rightBottomTargets.contains(target);
}
/**
* Resize the `mRemoteTargetHandles` array since we assumed initial size, but
* `targets.apps` is the ultimate source of truth here
*/
private void resizeRemoteTargetHandles(RemoteAnimationTargets targets) {
long appCount = Arrays.stream(targets.apps)
int handleCount = (int) Arrays.stream(targets.apps)
.filter(app -> app.mode == targets.targetMode)
.count();
Log.d(TAG, "appCount: " + appCount + " handleLength: " + mRemoteTargetHandles.length);
if (appCount < mRemoteTargetHandles.length) {
Log.d(TAG, "resizing handles");
RemoteTargetHandle[] newHandles = new RemoteTargetHandle[(int) appCount];
System.arraycopy(mRemoteTargetHandles, 0/*src*/, newHandles, 0/*dst*/, (int) appCount);
mRemoteTargetHandles = newHandles;
Log.d(TAG, "appCount: " + handleCount + " handleLength: " + mRemoteTargetHandles.length);
if (handleCount < mRemoteTargetHandles.length) {
reduceRemoteTargetHandles(handleCount);
}
}
/**
* Reduces the number of remote target handles to a specified count.
* The caller is responsible for ensuring that the target {@code handleCount}
* is always less than the current number of remote target handles
* ({@link #mRemoteTargetHandles}'s current size).
*
* @param handleCount The desired number of remote target handles after reduction.
* This value should be non-negative and less than the current size of the handle list.
*/
private void reduceRemoteTargetHandles(int handleCount) {
Log.d(TAG, "Reduce handles, count: " + handleCount);
RemoteTargetHandle[] newHandles = new RemoteTargetHandle[(int) handleCount];
System.arraycopy(mRemoteTargetHandles, 0/*src*/, newHandles, 0/*dst*/, (int) handleCount);
mRemoteTargetHandles = newHandles;
}
private void setRemoteTargetHandle(@NonNull RemoteAnimationTargets targets,
@Nullable RemoteAnimationTarget target,
@Nullable List<RemoteAnimationTarget> targetsToExclude,
@Nullable TransitionInfo transitionInfo,
@Nullable SplitBounds splitBounds, int taskIndex) {
if (targetsToExclude != null) {
mRemoteTargetHandles[taskIndex].mTransformParams.setTargetSet(
createRemoteAnimationTargetsForTarget(targets, targetsToExclude));
} else {
mRemoteTargetHandles[taskIndex].mTransformParams.setTargetSet(targets);
}
if (transitionInfo != null) {
mRemoteTargetHandles[taskIndex].mTransformParams.setTransitionInfo(transitionInfo);
}
if (target != null) {
mRemoteTargetHandles[taskIndex].mTaskViewSimulator.setPreview(target, splitBounds);
}
}
@@ -313,8 +313,8 @@ class KeyboardQuickSwitchControllerTest {
SplitBounds(
/* leftTopBounds = */ Rect(),
/* rightBottomBounds = */ Rect(),
/* leftTopTaskId = */ -1,
/* rightBottomTaskId = */ -1,
/* leftTopTaskId = */ 1,
/* rightBottomTaskId = */ 2,
/* snapPosition = */ SplitScreenConstants.SNAP_TO_2_50_50,
),
)
@@ -1196,8 +1196,8 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() {
SplitBounds(
/* leftTopBounds = */ Rect(),
/* rightBottomBounds = */ Rect(),
/* leftTopTaskId = */ -1,
/* rightBottomTaskId = */ -1,
/* leftTopTaskId = */ 1,
/* rightBottomTaskId = */ 2,
/* snapPosition = */ SplitScreenConstants.SNAP_TO_2_50_50,
),
)
@@ -132,6 +132,42 @@ public abstract class AbsSwipeUpHandlerTestCase<
/* taskInfo= */ mRunningTaskInfo,
/* allowEnterPip= */ false);
protected final RemoteAnimationTarget mRemoteAnimationLeftTop = new RemoteAnimationTarget(
/* taskId= */ 1,
/* mode= */ RemoteAnimationTarget.MODE_CLOSING,
/* leash= */ new SurfaceControl(),
/* isTranslucent= */ false,
/* clipRect= */ null,
/* contentInsets= */ null,
/* prefixOrderIndex= */ 0,
/* position= */ null,
/* localBounds= */ null,
/* screenSpaceBounds= */ null,
new Configuration().windowConfiguration,
/* isNotInRecents= */ false,
/* startLeash= */ null,
/* startBounds= */ null,
/* taskInfo= */ mRunningTaskInfo,
/* allowEnterPip= */ false);
protected final RemoteAnimationTarget mRemoteAnimationRightBottom = new RemoteAnimationTarget(
/* taskId= */ 2,
/* mode= */ RemoteAnimationTarget.MODE_CLOSING,
/* leash= */ new SurfaceControl(),
/* isTranslucent= */ false,
/* clipRect= */ null,
/* contentInsets= */ null,
/* prefixOrderIndex= */ 0,
/* position= */ null,
/* localBounds= */ null,
/* screenSpaceBounds= */ null,
new Configuration().windowConfiguration,
/* isNotInRecents= */ false,
/* startLeash= */ null,
/* startBounds= */ null,
/* taskInfo= */ mRunningTaskInfo,
/* allowEnterPip= */ false);
protected RecentsAnimationTargets mRecentsAnimationTargets;
protected TaskAnimationManager mTaskAnimationManager;
protected StateManager<STATE_TYPE, RECENTS_CONTAINER> mStateManager;
@@ -157,12 +193,12 @@ public abstract class AbsSwipeUpHandlerTestCase<
extras.putParcelable(KEY_EXTRA_SPLIT_BOUNDS, new SplitBounds(
/* leftTopBounds = */ new Rect(),
/* rightBottomBounds = */ new Rect(),
/* leftTopTaskId = */ -1,
/* rightBottomTaskId = */ -1,
/* leftTopTaskId = */ mRemoteAnimationLeftTop.taskId,
/* rightBottomTaskId = */ mRemoteAnimationRightBottom.taskId,
/* snapPosition = */ SNAP_TO_2_50_50));
mRecentsAnimationTargets = new RecentsAnimationTargets(
new RemoteAnimationTarget[] {mRemoteAnimationTarget},
new RemoteAnimationTarget[] {mRemoteAnimationTarget},
new RemoteAnimationTarget[] {mRemoteAnimationLeftTop},
new RemoteAnimationTarget[] {mRemoteAnimationRightBottom},
new RemoteAnimationTarget[] {mRemoteAnimationTarget},
/* homeContentInsets= */ new Rect(),
/* minimizedHomeBounds= */ null,
@@ -121,8 +121,8 @@ public class RecentTasksListTest {
new RecentTaskInfo(), new RecentTaskInfo(), new SplitBounds(
/* leftTopBounds = */ new Rect(),
/* rightBottomBounds = */ new Rect(),
/* leftTopTaskId = */ -1,
/* rightBottomTaskId = */ -1,
/* leftTopTaskId = */ 1,
/* rightBottomTaskId = */ 2,
/* snapPosition = */ SplitScreenConstants.SNAP_TO_2_50_50));
when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
.thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
@@ -217,8 +217,8 @@ public class RecentTasksListTest {
new SplitBounds(
/* leftTopBounds = */ new Rect(),
/* rightBottomBounds = */ new Rect(),
/* leftTopTaskId = */ -1,
/* rightBottomTaskId = */ -1,
/* leftTopTaskId = */ 1,
/* rightBottomTaskId = */ 2,
/* snapPosition = */ SplitScreenConstants.SNAP_TO_2_50_50));
when(mSystemUiProxy.getRecentTasks(anyInt(), anyInt()))
.thenReturn(new ArrayList<>(Collections.singletonList(recentTaskInfos)));
@@ -247,8 +247,8 @@ public class RecentsModelTest {
new SplitTask(task1, task2, new SplitBounds(
/* leftTopBounds = */ new Rect(),
/* rightBottomBounds = */ new Rect(),
/* leftTopTaskId = */ -1,
/* rightBottomTaskId = */ -1,
/* leftTopTaskId = */ 1,
/* rightBottomTaskId = */ 2,
/* snapPosition = */ SplitScreenConstants.SNAP_TO_2_50_50)));
return allTasks;
}
@@ -60,8 +60,8 @@ class TasksRepositoryTest {
SplitBounds(
/* leftTopBounds = */ Rect(),
/* rightBottomBounds = */ Rect(),
/* leftTopTaskId = */ -1,
/* rightBottomTaskId = */ -1,
/* leftTopTaskId = */ 1,
/* rightBottomTaskId = */ 2,
/* snapPosition = */ SNAP_TO_2_50_50,
),
),