From 1b560d8747beee450dfc14f2696cac5513f97baf Mon Sep 17 00:00:00 2001 From: Jerry Chang Date: Fri, 10 Dec 2021 18:25:35 +0800 Subject: [PATCH] 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 --- .../splitscreen/SplitScreenController.java | 11 +- .../shell/splitscreen/StageCoordinator.java | 115 ++++++++---------- .../splitscreen/SplitTransitionTests.java | 52 +++++++- .../android/server/wm/ActivityStarter.java | 3 +- 4 files changed, 110 insertions(+), 71 deletions(-) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index 46c4a40191be4..afd51171bdd71 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -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, 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 dd27017a37aaa..5e8f29f7cf18d 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 @@ -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, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java index 1eae625233a0c..c853fd350986b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java @@ -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; diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 178e99d375a0a..65f9e83dc5df6 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -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);