Integrate launch-to-side split transition to shell transition

Integrate launch-to-side split transition to shell transition.
Because there is no guarantee for launching activity to split
successfully before actually launching it, trigger enter split
transition after observed new task launching into split.

Bug: 206487881
Test: atest SplitTransitionTests
Test: drag to enter split with shell transition works
Change-Id: Idda7c3b02689753cea1a1fe80264145f1a449f73
This commit is contained in:
Jerry Chang
2021-12-10 18:25:35 +08:00
parent 5ee0a77acf
commit 1b560d8747
4 changed files with 110 additions and 71 deletions

View File

@@ -318,8 +318,15 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
startIntentLegacy(intent, fillInIntent, position, options);
return;
}
mStageCoordinator.startIntent(intent, fillInIntent, STAGE_TYPE_UNDEFINED, position, options,
null /* remote */);
try {
options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options,
null /* wct */);
intent.send(mContext, 0, fillInIntent, null /* onFinished */, null /* handler */,
null /* requiredPermission */, options);
} catch (PendingIntent.CanceledException e) {
Slog.e(TAG, "Failed to launch task", e);
}
}
private void startIntentLegacy(PendingIntent intent, Intent fillInIntent,

View File

@@ -44,7 +44,6 @@ import static com.android.wm.shell.splitscreen.SplitScreenController.exitReasonT
import static com.android.wm.shell.splitscreen.SplitScreenTransitions.FLAG_IS_DIVIDER_BAR;
import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS_SNAP;
import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE;
import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
import static com.android.wm.shell.transition.Transitions.isClosingType;
import static com.android.wm.shell.transition.Transitions.isOpeningType;
@@ -54,10 +53,8 @@ import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.ActivityTaskManager;
import android.app.PendingIntent;
import android.app.WindowConfiguration;
import android.content.Context;
import android.content.Intent;
import android.graphics.Rect;
import android.hardware.devicestate.DeviceStateManager;
import android.os.Bundle;
@@ -436,17 +433,6 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mTaskOrganizer.applyTransaction(wct);
}
public void startIntent(PendingIntent intent, Intent fillInIntent,
@StageType int stage, @SplitPosition int position,
@androidx.annotation.Nullable Bundle options,
@Nullable RemoteTransition remoteTransition) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
options = resolveStartStage(stage, position, options, wct);
wct.sendPendingIntent(intent, fillInIntent, options);
mSplitTransitions.startEnterTransition(
TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct, remoteTransition, this);
}
/**
* Collects all the current child tasks of a specific split and prepares transaction to evict
* them to display.
@@ -1158,12 +1144,14 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mSplitTransitions.mPendingDismiss = transition;
}
} else {
// Not in split mode, so look for an open into a split stage just so we can whine and
// complain about how this isn't a supported operation.
// Not in split mode, so look for an open into a split stage to active split screen.
if ((type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT)) {
if (getStageOfTask(triggerTask) != null) {
throw new IllegalStateException("Entering split implicitly with only one task"
+ " isn't supported.");
// One task is appearing in split, prepare to enter split screen.
out = new WindowContainerTransaction();
mSplitTransitions.mPendingEnter = transition;
mMainStage.activate(getMainStageBounds(), out, true /* includingTopTask */);
mSideStage.moveToTop(getSideStageBounds(), out);
}
}
}
@@ -1232,57 +1220,50 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
private boolean startPendingEnterAnimation(@NonNull IBinder transition,
@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) {
if (info.getType() == TRANSIT_SPLIT_SCREEN_PAIR_OPEN) {
// First, verify that we actually have opened 2 apps in split.
TransitionInfo.Change mainChild = null;
TransitionInfo.Change sideChild = null;
for (int iC = 0; iC < info.getChanges().size(); ++iC) {
final TransitionInfo.Change change = info.getChanges().get(iC);
final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
if (taskInfo == null || !taskInfo.hasParentTask()) continue;
final @StageType int stageType = getStageType(getStageOfTask(taskInfo));
if (stageType == STAGE_TYPE_MAIN) {
mainChild = change;
} else if (stageType == STAGE_TYPE_SIDE) {
sideChild = change;
}
// First, verify that we actually have opened apps in both splits.
TransitionInfo.Change mainChild = null;
TransitionInfo.Change sideChild = null;
for (int iC = 0; iC < info.getChanges().size(); ++iC) {
final TransitionInfo.Change change = info.getChanges().get(iC);
final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
if (taskInfo == null || !taskInfo.hasParentTask()) continue;
final @StageType int stageType = getStageType(getStageOfTask(taskInfo));
if (stageType == STAGE_TYPE_MAIN) {
mainChild = change;
} else if (stageType == STAGE_TYPE_SIDE) {
sideChild = change;
}
if (mainChild == null || sideChild == null) {
throw new IllegalStateException("Launched 2 tasks in split, but didn't receive"
+ " 2 tasks in transition. Possibly one of them failed to launch");
// TODO: fallback logic. Probably start a new transition to exit split before
// applying anything here. Ideally consolidate with transition-merging.
}
// Update local states (before animating).
setDividerVisibility(true);
setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, false /* updateBounds */,
null /* wct */);
setSplitsVisible(true);
addDividerBarToTransition(info, t, true /* show */);
// Make some noise if things aren't totally expected. These states shouldn't effect
// transitions locally, but remotes (like Launcher) may get confused if they were
// depending on listener callbacks. This can happen because task-organizer callbacks
// aren't serialized with transition callbacks.
// TODO(b/184679596): Find a way to either include task-org information in
// the transition, or synchronize task-org callbacks.
if (!mMainStage.containsTask(mainChild.getTaskInfo().taskId)) {
Log.w(TAG, "Expected onTaskAppeared on " + mMainStage
+ " to have been called with " + mainChild.getTaskInfo().taskId
+ " before startAnimation().");
}
if (!mSideStage.containsTask(sideChild.getTaskInfo().taskId)) {
Log.w(TAG, "Expected onTaskAppeared on " + mSideStage
+ " to have been called with " + sideChild.getTaskInfo().taskId
+ " before startAnimation().");
}
return true;
} else {
// TODO: other entry method animations
throw new RuntimeException("Unsupported split-entry");
}
if (mainChild == null || sideChild == null) {
throw new IllegalStateException("Launched 2 tasks in split, but didn't receive"
+ " 2 tasks in transition. Possibly one of them failed to launch");
// TODO: fallback logic. Probably start a new transition to exit split before
// applying anything here. Ideally consolidate with transition-merging.
}
// Update local states (before animating).
setDividerVisibility(true);
setSplitsVisible(true);
addDividerBarToTransition(info, t, true /* show */);
// Make some noise if things aren't totally expected. These states shouldn't effect
// transitions locally, but remotes (like Launcher) may get confused if they were
// depending on listener callbacks. This can happen because task-organizer callbacks
// aren't serialized with transition callbacks.
// TODO(b/184679596): Find a way to either include task-org information in
// the transition, or synchronize task-org callbacks.
if (!mMainStage.containsTask(mainChild.getTaskInfo().taskId)) {
Log.w(TAG, "Expected onTaskAppeared on " + mMainStage
+ " to have been called with " + mainChild.getTaskInfo().taskId
+ " before startAnimation().");
}
if (!mSideStage.containsTask(sideChild.getTaskInfo().taskId)) {
Log.w(TAG, "Expected onTaskAppeared on " + mSideStage
+ " to have been called with " + sideChild.getTaskInfo().taskId
+ " before startAnimation().");
}
return true;
}
private boolean startPendingDismissAnimation(@NonNull IBinder transition,

View File

@@ -24,6 +24,7 @@ import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER;
import static com.android.wm.shell.splitscreen.SplitTestUtils.createMockSurface;
import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
@@ -66,7 +67,6 @@ import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.common.split.SplitLayout;
import com.android.wm.shell.recents.RecentTasksController;
import com.android.wm.shell.transition.Transitions;
import org.junit.Before;
@@ -132,6 +132,40 @@ public class SplitTransitionTests extends ShellTestCase {
.setParentTaskId(mSideStage.mRootTaskInfo.taskId).build();
}
@Test
public void testLaunchToSide() {
ActivityManager.RunningTaskInfo newTask = new TestRunningTaskInfoBuilder()
.setParentTaskId(mSideStage.mRootTaskInfo.taskId).build();
ActivityManager.RunningTaskInfo reparentTask = new TestRunningTaskInfoBuilder()
.setParentTaskId(mMainStage.mRootTaskInfo.taskId).build();
// Create a request to start a new task in side stage
TransitionRequestInfo request = new TransitionRequestInfo(TRANSIT_TO_FRONT, newTask, null);
IBinder transition = mock(IBinder.class);
WindowContainerTransaction result =
mStageCoordinator.handleRequest(transition, request);
// it should handle the transition to enter split screen.
assertNotNull(result);
assertTrue(containsSplitEnter(result));
// simulate the transition
TransitionInfo.Change openChange = createChange(TRANSIT_OPEN, newTask);
TransitionInfo.Change reparentChange = createChange(TRANSIT_CHANGE, reparentTask);
TransitionInfo info = new TransitionInfo(TRANSIT_TO_FRONT, 0);
info.addChange(openChange);
info.addChange(reparentChange);
mSideStage.onTaskAppeared(newTask, createMockSurface());
mMainStage.onTaskAppeared(reparentTask, createMockSurface());
boolean accepted = mStageCoordinator.startAnimation(transition, info,
mock(SurfaceControl.Transaction.class),
mock(SurfaceControl.Transaction.class),
mock(Transitions.TransitionFinishCallback.class));
assertTrue(accepted);
assertTrue(mStageCoordinator.isSplitScreenVisible());
}
@Test
public void testLaunchPair() {
TransitionInfo info = createEnterPairInfo();
@@ -324,6 +358,22 @@ public class SplitTransitionTests extends ShellTestCase {
true /* includingTopTask */);
}
private boolean containsSplitEnter(@NonNull WindowContainerTransaction wct) {
boolean movedMainToFront = false;
boolean movedSideToFront = false;
for (int i = 0; i < wct.getHierarchyOps().size(); ++i) {
WindowContainerTransaction.HierarchyOp op = wct.getHierarchyOps().get(i);
if (op.getType() == HIERARCHY_OP_TYPE_REORDER) {
if (op.getContainer() == mMainStage.mRootTaskInfo.token.asBinder()) {
movedMainToFront = true;
} else if (op.getContainer() == mSideStage.mRootTaskInfo.token.asBinder()) {
movedSideToFront = true;
}
}
}
return movedMainToFront && movedSideToFront;
}
private boolean containsSplitExit(@NonNull WindowContainerTransaction wct) {
// reparenting of child tasks to null constitutes exiting split.
boolean reparentedMain = false;

View File

@@ -1672,7 +1672,8 @@ class ActivityStarter {
}
if (newTransition != null) {
transitionController.requestStartTransition(newTransition,
mTargetTask, remoteTransition, null /* displayChange */);
mTargetTask == null ? r.getTask() : mTargetTask,
remoteTransition, null /* displayChange */);
} else if (started) {
// Make the collecting transition wait until this request is ready.
transitionController.setReady(r, false);