summaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
Diffstat (limited to 'libs')
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java20
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java23
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java296
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java423
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java36
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java12
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java25
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java80
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java342
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java32
12 files changed, 1231 insertions, 70 deletions
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
index 0c46eaba18ae..8f0892fdcbba 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
@@ -20,6 +20,7 @@ import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import android.os.UserHandle;
+import android.window.IRemoteTransition;
import com.android.wm.shell.splitscreen.ISplitScreenListener;
@@ -74,4 +75,11 @@ interface ISplitScreen {
*/
oneway void startIntent(in PendingIntent intent, in Intent fillInIntent, int stage,
int position, in Bundle options) = 9;
+
+ /**
+ * Starts tasks simultaneously in one transition. The first task in the list will be in the
+ * main-stage and on the left/top.
+ */
+ oneway void startTasks(int mainTaskId, in Bundle mainOptions, int sideTaskId,
+ in Bundle sideOptions, int sidePosition, in IRemoteTransition remoteTransition) = 10;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
index 2f2e325aafad..66a4a60d4be6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
@@ -16,10 +16,7 @@
package com.android.wm.shell.splitscreen;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
-import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import android.graphics.Rect;
import android.window.WindowContainerToken;
@@ -52,6 +49,7 @@ class MainStage extends StageTaskListener {
final WindowContainerToken rootToken = mRootTaskInfo.token;
wct.setBounds(rootToken, rootBounds)
+ .setWindowingMode(rootToken, WINDOWING_MODE_MULTI_WINDOW)
.setLaunchRoot(
rootToken,
CONTROLLED_WINDOWING_MODES,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
index 340b55d7f446..d4506fd32c86 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
@@ -17,18 +17,8 @@
package com.android.wm.shell.splitscreen;
import android.annotation.IntDef;
-import android.app.ActivityManager;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Rect;
-import android.os.Bundle;
-import android.os.UserHandle;
-
-import androidx.annotation.Nullable;
import com.android.wm.shell.common.annotations.ExternalThread;
-import com.android.wm.shell.draganddrop.DragAndDropPolicy;
/**
* Interface to engage split-screen feature.
@@ -95,4 +85,14 @@ public interface SplitScreen {
default ISplitScreen createExternalInterface() {
return null;
}
+
+ /** Get a string representation of a stage type */
+ static String stageTypeToString(@StageType int stage) {
+ switch (stage) {
+ case STAGE_TYPE_UNDEFINED: return "UNDEFINED";
+ case STAGE_TYPE_MAIN: return "MAIN";
+ case STAGE_TYPE_SIDE: return "SIDE";
+ default: return "UNKNOWN(" + stage + ")";
+ }
+ }
}
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 d4362efe462d..5aa59f283434 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
@@ -39,6 +39,7 @@ import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Slog;
+import android.window.IRemoteTransition;
import androidx.annotation.BinderThread;
import androidx.annotation.NonNull;
@@ -50,9 +51,11 @@ import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.RemoteCallable;
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.annotations.ExternalThread;
import com.android.wm.shell.draganddrop.DragAndDropPolicy;
import com.android.wm.shell.splitscreen.ISplitScreenListener;
+import com.android.wm.shell.transition.Transitions;
import java.io.PrintWriter;
@@ -72,19 +75,24 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
private final ShellExecutor mMainExecutor;
private final SplitScreenImpl mImpl = new SplitScreenImpl();
private final DisplayImeController mDisplayImeController;
+ private final Transitions mTransitions;
+ private final TransactionPool mTransactionPool;
private StageCoordinator mStageCoordinator;
public SplitScreenController(ShellTaskOrganizer shellTaskOrganizer,
SyncTransactionQueue syncQueue, Context context,
RootTaskDisplayAreaOrganizer rootTDAOrganizer,
- ShellExecutor mainExecutor, DisplayImeController displayImeController) {
+ ShellExecutor mainExecutor, DisplayImeController displayImeController,
+ Transitions transitions, TransactionPool transactionPool) {
mTaskOrganizer = shellTaskOrganizer;
mSyncQueue = syncQueue;
mContext = context;
mRootTDAOrganizer = rootTDAOrganizer;
mMainExecutor = mainExecutor;
mDisplayImeController = displayImeController;
+ mTransitions = transitions;
+ mTransactionPool = transactionPool;
}
public SplitScreen asSplitScreen() {
@@ -105,7 +113,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
if (mStageCoordinator == null) {
// TODO: Multi-display
mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
- mRootTDAOrganizer, mTaskOrganizer, mDisplayImeController);
+ mRootTDAOrganizer, mTaskOrganizer, mDisplayImeController, mTransitions,
+ mTransactionPool);
}
}
@@ -407,6 +416,16 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
}
@Override
+ public void startTasks(int mainTaskId, @Nullable Bundle mainOptions,
+ int sideTaskId, @Nullable Bundle sideOptions,
+ @SplitScreen.StagePosition int sidePosition,
+ @Nullable IRemoteTransition remoteTransition) {
+ executeRemoteCallWithTaskPermission(mController, "startTasks",
+ (controller) -> controller.mStageCoordinator.startTasks(mainTaskId, mainOptions,
+ sideTaskId, sideOptions, sidePosition, remoteTransition));
+ }
+
+ @Override
public void startShortcut(String packageName, String shortcutId, int stage, int position,
@Nullable Bundle options, UserHandle user) {
executeRemoteCallWithTaskPermission(mController, "startShortcut",
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
new file mode 100644
index 000000000000..c37789ecbc9d
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.splitscreen;
+
+import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_CLOSE;
+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.TransitionInfo.FLAG_FIRST_CUSTOM;
+
+import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS_SNAP;
+import static com.android.wm.shell.transition.Transitions.isOpeningType;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.IRemoteTransition;
+import android.window.TransitionInfo;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.transition.OneShotRemoteHandler;
+import com.android.wm.shell.transition.Transitions;
+
+import java.util.ArrayList;
+
+/** Manages transition animations for split-screen. */
+class SplitScreenTransitions {
+ private static final String TAG = "SplitScreenTransitions";
+
+ /** Flag applied to a transition change to identify it as a divider bar for animation. */
+ public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM;
+
+ private final TransactionPool mTransactionPool;
+ private final Transitions mTransitions;
+ private final Runnable mOnFinish;
+
+ IBinder mPendingDismiss = null;
+ IBinder mPendingEnter = null;
+
+ private IBinder mAnimatingTransition = null;
+ private OneShotRemoteHandler mRemoteHandler = null;
+
+ private Transitions.TransitionFinishCallback mRemoteFinishCB = (wct, wctCB) -> {
+ if (wct != null || wctCB != null) {
+ throw new UnsupportedOperationException("finish transactions not supported yet.");
+ }
+ onFinish();
+ };
+
+ /** Keeps track of currently running animations */
+ private final ArrayList<Animator> mAnimations = new ArrayList<>();
+
+ private Transitions.TransitionFinishCallback mFinishCallback = null;
+ private SurfaceControl.Transaction mFinishTransaction;
+
+ SplitScreenTransitions(@NonNull TransactionPool pool, @NonNull Transitions transitions,
+ @NonNull Runnable onFinishCallback) {
+ mTransactionPool = pool;
+ mTransitions = transitions;
+ mOnFinish = onFinishCallback;
+ }
+
+ void playAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction t,
+ @NonNull Transitions.TransitionFinishCallback finishCallback,
+ @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot) {
+ mFinishCallback = finishCallback;
+ mAnimatingTransition = transition;
+ if (mRemoteHandler != null) {
+ mRemoteHandler.startAnimation(transition, info, t, mRemoteFinishCB);
+ mRemoteHandler = null;
+ return;
+ }
+ playInternalAnimation(transition, info, t, mainRoot, sideRoot);
+ }
+
+ private void playInternalAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction t, @NonNull WindowContainerToken mainRoot,
+ @NonNull WindowContainerToken sideRoot) {
+ mFinishTransaction = mTransactionPool.acquire();
+
+ // Play some place-holder fade animations
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ final SurfaceControl leash = change.getLeash();
+ final int mode = info.getChanges().get(i).getMode();
+
+ if (mode == TRANSIT_CHANGE) {
+ if (change.getParent() != null) {
+ // This is probably reparented, so we want the parent to be immediately visible
+ final TransitionInfo.Change parentChange = info.getChange(change.getParent());
+ t.show(parentChange.getLeash());
+ t.setAlpha(parentChange.getLeash(), 1.f);
+ // and then animate this layer outside the parent (since, for example, this is
+ // the home task animating from fullscreen to part-screen).
+ t.reparent(leash, info.getRootLeash());
+ t.setLayer(leash, info.getChanges().size() - i);
+ // build the finish reparent/reposition
+ mFinishTransaction.reparent(leash, parentChange.getLeash());
+ mFinishTransaction.setPosition(leash,
+ change.getEndRelOffset().x, change.getEndRelOffset().y);
+ }
+ // TODO(shell-transitions): screenshot here
+ final Rect startBounds = new Rect(change.getStartAbsBounds());
+ if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) {
+ // Dismissing split via snap which means the still-visible task has been
+ // dragged to its end position at animation start so reflect that here.
+ startBounds.offsetTo(change.getEndAbsBounds().left,
+ change.getEndAbsBounds().top);
+ }
+ final Rect endBounds = new Rect(change.getEndAbsBounds());
+ startBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y);
+ endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y);
+ startExampleResizeAnimation(leash, startBounds, endBounds);
+ }
+ if (change.getParent() != null) {
+ continue;
+ }
+
+ if (transition == mPendingEnter && (mainRoot.equals(change.getContainer())
+ || sideRoot.equals(change.getContainer()))) {
+ t.setWindowCrop(leash, change.getStartAbsBounds().width(),
+ change.getStartAbsBounds().height());
+ }
+ boolean isOpening = isOpeningType(info.getType());
+ if (isOpening && (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) {
+ // fade in
+ startExampleAnimation(leash, true /* show */);
+ } else if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) {
+ // fade out
+ if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) {
+ // Dismissing via snap-to-top/bottom means that the dismissed task is already
+ // not-visible (usually cropped to oblivion) so immediately set its alpha to 0
+ // and don't animate it so it doesn't pop-in when reparented.
+ t.setAlpha(leash, 0.f);
+ } else {
+ startExampleAnimation(leash, false /* show */);
+ }
+ }
+ }
+ t.apply();
+ onFinish();
+ }
+
+ /** Starts a transition to enter split with a remote transition animator. */
+ IBinder startEnterTransition(@WindowManager.TransitionType int transitType,
+ @NonNull WindowContainerTransaction wct, @Nullable IRemoteTransition remoteTransition,
+ @NonNull Transitions.TransitionHandler handler) {
+ if (remoteTransition != null) {
+ // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff)
+ mRemoteHandler = new OneShotRemoteHandler(
+ mTransitions.getMainExecutor(), remoteTransition);
+ }
+ final IBinder transition = mTransitions.startTransition(transitType, wct, handler);
+ mPendingEnter = transition;
+ if (mRemoteHandler != null) {
+ mRemoteHandler.setTransition(transition);
+ }
+ return transition;
+ }
+
+ /** Starts a transition for dismissing split after dragging the divider to a screen edge */
+ IBinder startSnapToDismiss(@NonNull WindowContainerTransaction wct,
+ @NonNull Transitions.TransitionHandler handler) {
+ final IBinder transition = mTransitions.startTransition(
+ TRANSIT_SPLIT_DISMISS_SNAP, wct, handler);
+ mPendingDismiss = transition;
+ return transition;
+ }
+
+ void onFinish() {
+ if (!mAnimations.isEmpty()) return;
+ mOnFinish.run();
+ if (mFinishTransaction != null) {
+ mFinishTransaction.apply();
+ mTransactionPool.release(mFinishTransaction);
+ mFinishTransaction = null;
+ }
+ mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
+ mFinishCallback = null;
+ if (mAnimatingTransition == mPendingEnter) {
+ mPendingEnter = null;
+ }
+ if (mAnimatingTransition == mPendingDismiss) {
+ mPendingDismiss = null;
+ }
+ mAnimatingTransition = null;
+ }
+
+ // TODO(shell-transitions): real animations
+ private void startExampleAnimation(@NonNull SurfaceControl leash, boolean show) {
+ final float end = show ? 1.f : 0.f;
+ final float start = 1.f - end;
+ final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+ final ValueAnimator va = ValueAnimator.ofFloat(start, end);
+ va.setDuration(500);
+ va.addUpdateListener(animation -> {
+ float fraction = animation.getAnimatedFraction();
+ transaction.setAlpha(leash, start * (1.f - fraction) + end * fraction);
+ transaction.apply();
+ });
+ final Runnable finisher = () -> {
+ transaction.setAlpha(leash, end);
+ transaction.apply();
+ mTransactionPool.release(transaction);
+ mTransitions.getMainExecutor().execute(() -> {
+ mAnimations.remove(va);
+ onFinish();
+ });
+ };
+ va.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) { }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ finisher.run();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ finisher.run();
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) { }
+ });
+ mAnimations.add(va);
+ mTransitions.getAnimExecutor().execute(va::start);
+ }
+
+ // TODO(shell-transitions): real animations
+ private void startExampleResizeAnimation(@NonNull SurfaceControl leash,
+ @NonNull Rect startBounds, @NonNull Rect endBounds) {
+ final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+ final ValueAnimator va = ValueAnimator.ofFloat(0.f, 1.f);
+ va.setDuration(500);
+ va.addUpdateListener(animation -> {
+ float fraction = animation.getAnimatedFraction();
+ transaction.setWindowCrop(leash,
+ (int) (startBounds.width() * (1.f - fraction) + endBounds.width() * fraction),
+ (int) (startBounds.height() * (1.f - fraction)
+ + endBounds.height() * fraction));
+ transaction.setPosition(leash,
+ startBounds.left * (1.f - fraction) + endBounds.left * fraction,
+ startBounds.top * (1.f - fraction) + endBounds.top * fraction);
+ transaction.apply();
+ });
+ final Runnable finisher = () -> {
+ transaction.setWindowCrop(leash, 0, 0);
+ transaction.setPosition(leash, endBounds.left, endBounds.top);
+ transaction.apply();
+ mTransactionPool.release(transaction);
+ mTransitions.getMainExecutor().execute(() -> {
+ mAnimations.remove(va);
+ onFinish();
+ });
+ };
+ va.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ finisher.run();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ finisher.run();
+ }
+ });
+ mAnimations.add(va);
+ mTransitions.getAnimExecutor().execute(va::start);
+ }
+}
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 b180bb52e3e3..c91a92ad3242 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
@@ -17,31 +17,54 @@
package com.android.wm.shell.splitscreen;
import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+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.view.WindowManager.transitTypeToString;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
-
+import static com.android.wm.shell.splitscreen.SplitScreen.stageTypeToString;
+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_PAIR_OPEN;
+import static com.android.wm.shell.transition.Transitions.isClosingType;
+import static com.android.wm.shell.transition.Transitions.isOpeningType;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Rect;
import android.os.Bundle;
+import android.os.IBinder;
+import android.util.Log;
import android.view.SurfaceControl;
+import android.view.WindowManager;
import android.window.DisplayAreaInfo;
+import android.window.IRemoteTransition;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
import android.window.WindowContainerTransaction;
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayImeController;
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.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.transition.Transitions;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -61,10 +84,13 @@ import java.util.List;
* {@link #onStageHasChildrenChanged(StageListenerImpl).}
*/
class StageCoordinator implements SplitLayout.LayoutChangeListener,
- RootTaskDisplayAreaOrganizer.RootTaskDisplayAreaListener {
+ RootTaskDisplayAreaOrganizer.RootTaskDisplayAreaListener, Transitions.TransitionHandler {
private static final String TAG = StageCoordinator.class.getSimpleName();
+ /** internal value for mDismissTop that represents no dismiss */
+ private static final int NO_DISMISS = -2;
+
private final MainStage mMainStage;
private final StageListenerImpl mMainStageListener = new StageListenerImpl();
private final SideStage mSideStage;
@@ -81,11 +107,25 @@ class StageCoordinator implements SplitLayout.LayoutChangeListener,
private final Context mContext;
private final List<SplitScreen.SplitScreenListener> mListeners = new ArrayList<>();
private final DisplayImeController mDisplayImeController;
+ private final SplitScreenTransitions mSplitTransitions;
private boolean mExitSplitScreenOnHide = true;
+ @SplitScreen.StageType int mDismissTop = NO_DISMISS;
+ private final Runnable mOnTransitionAnimationComplete = () -> {
+ // If still playing, let it finish.
+ if (!isSplitScreenVisible()) {
+ // Update divider state after animation so that it is still around and positioned
+ // properly for the animation itself.
+ setDividerVisibility(false);
+ mSplitLayout.resetDividerPosition();
+ }
+ mDismissTop = NO_DISMISS;
+ };
+
StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer,
- DisplayImeController displayImeController) {
+ DisplayImeController displayImeController, Transitions transitions,
+ TransactionPool transactionPool) {
mContext = context;
mDisplayId = displayId;
mSyncQueue = syncQueue;
@@ -95,12 +135,16 @@ class StageCoordinator implements SplitLayout.LayoutChangeListener,
mSideStage = new SideStage(mTaskOrganizer, mDisplayId, mSideStageListener, mSyncQueue);
mDisplayImeController = displayImeController;
mRootTDAOrganizer.registerListener(displayId, this);
+ mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions,
+ mOnTransitionAnimationComplete);
+ transitions.addHandler(this);
}
@VisibleForTesting
StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer,
- MainStage mainStage, SideStage sideStage, DisplayImeController displayImeController) {
+ MainStage mainStage, SideStage sideStage, DisplayImeController displayImeController,
+ SplitLayout splitLayout, Transitions transitions, TransactionPool transactionPool) {
mContext = context;
mDisplayId = displayId;
mSyncQueue = syncQueue;
@@ -110,6 +154,15 @@ class StageCoordinator implements SplitLayout.LayoutChangeListener,
mSideStage = sideStage;
mDisplayImeController = displayImeController;
mRootTDAOrganizer.registerListener(displayId, this);
+ mSplitLayout = splitLayout;
+ mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions,
+ mOnTransitionAnimationComplete);
+ transitions.addHandler(this);
+ }
+
+ @VisibleForTesting
+ SplitScreenTransitions getSplitTransitions() {
+ return mSplitTransitions;
}
boolean isSplitScreenVisible() {
@@ -140,6 +193,32 @@ class StageCoordinator implements SplitLayout.LayoutChangeListener,
return result;
}
+ /** Starts 2 tasks in one transition. */
+ void startTasks(int mainTaskId, @Nullable Bundle mainOptions, int sideTaskId,
+ @Nullable Bundle sideOptions, @SplitScreen.StagePosition int sidePosition,
+ @Nullable IRemoteTransition remoteTransition) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ mainOptions = mainOptions != null ? mainOptions : new Bundle();
+ sideOptions = sideOptions != null ? sideOptions : new Bundle();
+ setSideStagePosition(sidePosition);
+
+ // Build a request WCT that will launch both apps such that task 0 is on the main stage
+ // while task 1 is on the side stage.
+ mMainStage.activate(getMainStageBounds(), wct);
+ mSideStage.setBounds(getSideStageBounds(), wct);
+
+ // Make sure the launch options will put tasks in the corresponding split roots
+ addActivityOptions(mainOptions, mMainStage);
+ addActivityOptions(sideOptions, mSideStage);
+
+ // Add task launch requests
+ wct.startTask(mainTaskId, mainOptions);
+ wct.startTask(sideTaskId, sideOptions);
+
+ mSplitTransitions.startEnterTransition(
+ TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this);
+ }
+
@SplitScreen.StagePosition int getSideStagePosition() {
return mSideStagePosition;
}
@@ -150,14 +229,18 @@ class StageCoordinator implements SplitLayout.LayoutChangeListener,
}
void setSideStagePosition(@SplitScreen.StagePosition int sideStagePosition) {
- if (mSideStagePosition == sideStagePosition) return;
+ setSideStagePosition(sideStagePosition, true /* updateVisibility */);
+ }
+ private void setSideStagePosition(@SplitScreen.StagePosition int sideStagePosition,
+ boolean updateVisibility) {
+ if (mSideStagePosition == sideStagePosition) return;
mSideStagePosition = sideStagePosition;
- if (mSideStageListener.mVisible) {
+ sendOnStagePositionChanged();
+
+ if (mSideStageListener.mVisible && updateVisibility) {
onStageVisibilityChanged(mSideStageListener);
}
-
- sendOnStagePositionChanged();
}
void setSideStageVisibility(boolean visible) {
@@ -185,14 +268,23 @@ class StageCoordinator implements SplitLayout.LayoutChangeListener,
mSplitLayout.resetDividerPosition();
}
+ private void prepareExitSplitScreen(@SplitScreen.StageType int stageToTop,
+ @NonNull WindowContainerTransaction wct) {
+ mSideStage.removeAllTasks(wct, stageToTop == STAGE_TYPE_SIDE);
+ mMainStage.deactivate(wct, stageToTop == STAGE_TYPE_MAIN);
+ }
+
void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) {
outTopOrLeftBounds.set(mSplitLayout.getBounds1());
outBottomOrRightBounds.set(mSplitLayout.getBounds2());
}
- void updateActivityOptions(Bundle opts, @SplitScreen.StagePosition int position) {
- final StageTaskListener stage = position == mSideStagePosition ? mSideStage : mMainStage;
+ private void addActivityOptions(Bundle opts, StageTaskListener stage) {
opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, stage.mRootTaskInfo.token);
+ }
+
+ void updateActivityOptions(Bundle opts, @SplitScreen.StagePosition int position) {
+ addActivityOptions(opts, position == mSideStagePosition ? mSideStage : mMainStage);
if (!mMainStage.isActive()) {
// Activate the main stage in anticipation of an app launch.
@@ -258,20 +350,21 @@ class StageCoordinator implements SplitLayout.LayoutChangeListener,
}
}
+ private void setDividerVisibility(boolean visible) {
+ if (mDividerVisible == visible) return;
+ mDividerVisible = visible;
+ if (visible) {
+ mSplitLayout.init();
+ } else {
+ mSplitLayout.release();
+ }
+ }
+
private void onStageVisibilityChanged(StageListenerImpl stageListener) {
final boolean sideStageVisible = mSideStageListener.mVisible;
final boolean mainStageVisible = mMainStageListener.mVisible;
// Divider is only visible if both the main stage and side stages are visible
- final boolean dividerVisible = sideStageVisible && mainStageVisible;
-
- if (mDividerVisible != dividerVisible) {
- mDividerVisible = dividerVisible;
- if (mDividerVisible) {
- mSplitLayout.init();
- } else {
- mSplitLayout.release();
- }
- }
+ setDividerVisibility(isSplitScreenVisible());
if (mExitSplitScreenOnHide && !mainStageVisible && !sideStageVisible) {
// Exit split-screen if both stage are not visible.
@@ -360,11 +453,22 @@ class StageCoordinator implements SplitLayout.LayoutChangeListener,
}
}
+ @VisibleForTesting
+ IBinder onSnappedToDismissTransition(boolean mainStageToTop) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ prepareExitSplitScreen(mainStageToTop ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE, wct);
+ return mSplitTransitions.startSnapToDismiss(wct, this);
+ }
+
@Override
public void onSnappedToDismiss(boolean bottomOrRight) {
final boolean mainStageToTop =
bottomOrRight ? mSideStagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT
: mSideStagePosition == STAGE_POSITION_TOP_OR_LEFT;
+ if (ENABLE_SHELL_TRANSITIONS) {
+ onSnappedToDismissTransition(mainStageToTop);
+ return;
+ }
exitSplitScreen(mainStageToTop ? mMainStage : mSideStage);
}
@@ -455,6 +559,271 @@ class StageCoordinator implements SplitLayout.LayoutChangeListener,
? mSplitLayout.getBounds2() : mSplitLayout.getBounds1();
}
+ /**
+ * Get the stage that should contain this `taskInfo`. The stage doesn't necessarily contain
+ * this task (yet) so this can also be used to identify which stage to put a task into.
+ */
+ private StageTaskListener getStageOfTask(ActivityManager.RunningTaskInfo taskInfo) {
+ // TODO(b/184679596): Find a way to either include task-org information in the transition,
+ // or synchronize task-org callbacks so we can use stage.containsTask
+ if (mMainStage.mRootTaskInfo != null
+ && taskInfo.parentTaskId == mMainStage.mRootTaskInfo.taskId) {
+ return mMainStage;
+ } else if (mSideStage.mRootTaskInfo != null
+ && taskInfo.parentTaskId == mSideStage.mRootTaskInfo.taskId) {
+ return mSideStage;
+ }
+ return null;
+ }
+
+ @SplitScreen.StageType
+ private int getStageType(StageTaskListener stage) {
+ return stage == mMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
+ }
+
+ @Override
+ public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+ @Nullable TransitionRequestInfo request) {
+ final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask();
+ if (triggerTask == null) {
+ // still want to monitor everything while in split-screen, so return non-null.
+ return isSplitScreenVisible() ? new WindowContainerTransaction() : null;
+ }
+
+ WindowContainerTransaction out = null;
+ final @WindowManager.TransitionType int type = request.getType();
+ if (isSplitScreenVisible()) {
+ // try to handle everything while in split-screen, so return a WCT even if it's empty.
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " split is active so using split"
+ + "Transition to handle request. triggerTask=%d type=%s mainChildren=%d"
+ + " sideChildren=%d", triggerTask.taskId, transitTypeToString(type),
+ mMainStage.getChildCount(), mSideStage.getChildCount());
+ out = new WindowContainerTransaction();
+ final StageTaskListener stage = getStageOfTask(triggerTask);
+ if (stage != null) {
+ // dismiss split if the last task in one of the stages is going away
+ if (isClosingType(type) && stage.getChildCount() == 1) {
+ // The top should be the opposite side that is closing:
+ mDismissTop = getStageType(stage) == STAGE_TYPE_MAIN
+ ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN;
+ }
+ } else {
+ if (triggerTask.getActivityType() == ACTIVITY_TYPE_HOME && isOpeningType(type)) {
+ // Going home so dismiss both.
+ mDismissTop = STAGE_TYPE_UNDEFINED;
+ }
+ }
+ if (mDismissTop != NO_DISMISS) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition "
+ + " deduced Dismiss from request. toTop=%s",
+ stageTypeToString(mDismissTop));
+ prepareExitSplitScreen(mDismissTop, out);
+ 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.
+ 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.");
+ }
+ }
+ }
+ return out;
+ }
+
+ @Override
+ public boolean startAnimation(@NonNull IBinder transition,
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction t,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ if (transition != mSplitTransitions.mPendingDismiss
+ && transition != mSplitTransitions.mPendingEnter) {
+ // Not entering or exiting, so just do some house-keeping and validation.
+
+ // If we're not in split-mode, just abort so something else can handle it.
+ if (!isSplitScreenVisible()) return false;
+
+ 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 StageTaskListener stage = getStageOfTask(taskInfo);
+ if (stage == null) continue;
+ if (isOpeningType(change.getMode())) {
+ if (!stage.containsTask(taskInfo.taskId)) {
+ Log.w(TAG, "Expected onTaskAppeared on " + stage + " to have been called"
+ + " with " + taskInfo.taskId + " before startAnimation().");
+ }
+ } else if (isClosingType(change.getMode())) {
+ if (stage.containsTask(taskInfo.taskId)) {
+ Log.w(TAG, "Expected onTaskVanished on " + stage + " to have been called"
+ + " with " + taskInfo.taskId + " before startAnimation().");
+ }
+ }
+ }
+ if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) {
+ // TODO(shell-transitions): Implement a fallback behavior for now.
+ throw new IllegalStateException("Somehow removed the last task in a stage"
+ + " outside of a proper transition");
+ // This can happen in some pathological cases. For example:
+ // 1. main has 2 tasks [Task A (Single-task), Task B], side has one task [Task C]
+ // 2. Task B closes itself and starts Task A in LAUNCH_ADJACENT at the same time
+ // In this case, the result *should* be that we leave split.
+ // TODO(b/184679596): Find a way to either include task-org information in
+ // the transition, or synchronize task-org callbacks.
+ }
+
+ // Use normal animations.
+ return false;
+ }
+
+ boolean shouldAnimate = true;
+ if (mSplitTransitions.mPendingEnter == transition) {
+ shouldAnimate = startPendingEnterAnimation(transition, info, t);
+ } else if (mSplitTransitions.mPendingDismiss == transition) {
+ shouldAnimate = startPendingDismissAnimation(transition, info, t);
+ }
+ if (!shouldAnimate) return false;
+
+ mSplitTransitions.playAnimation(transition, info, t, finishCallback,
+ mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
+ return true;
+ }
+
+ 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 @SplitScreen.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(STAGE_POSITION_BOTTOM_OR_RIGHT, false /* updateVisibility */);
+ 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");
+ }
+ }
+
+ private boolean startPendingDismissAnimation(@NonNull IBinder transition,
+ @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) {
+ // 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.getChildCount() != 0) {
+ final StringBuilder tasksLeft = new StringBuilder();
+ for (int i = 0; i < mMainStage.getChildCount(); ++i) {
+ tasksLeft.append(i != 0 ? ", " : "");
+ tasksLeft.append(mMainStage.mChildrenTaskInfo.keyAt(i));
+ }
+ Log.w(TAG, "Expected onTaskVanished on " + mMainStage
+ + " to have been called with [" + tasksLeft.toString()
+ + "] before startAnimation().");
+ }
+ if (mSideStage.getChildCount() != 0) {
+ final StringBuilder tasksLeft = new StringBuilder();
+ for (int i = 0; i < mSideStage.getChildCount(); ++i) {
+ tasksLeft.append(i != 0 ? ", " : "");
+ tasksLeft.append(mSideStage.mChildrenTaskInfo.keyAt(i));
+ }
+ Log.w(TAG, "Expected onTaskVanished on " + mSideStage
+ + " to have been called with [" + tasksLeft.toString()
+ + "] before startAnimation().");
+ }
+
+ // Update local states.
+ setSplitsVisible(false);
+ // Wait until after animation to update divider
+
+ if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) {
+ // Reset crops so they don't interfere with subsequent launches
+ t.setWindowCrop(mMainStage.mRootLeash, null);
+ t.setWindowCrop(mSideStage.mRootLeash, null);
+ }
+
+ if (mDismissTop == STAGE_TYPE_UNDEFINED) {
+ // Going home (dismissing both splits)
+
+ // TODO: Have a proper remote for this. Until then, though, reset state and use the
+ // normal animation stuff (which falls back to the normal launcher remote).
+ t.hide(mSplitLayout.getDividerLeash());
+ setDividerVisibility(false);
+ mSplitTransitions.mPendingDismiss = null;
+ return false;
+ }
+
+ addDividerBarToTransition(info, t, false /* show */);
+ // We're dismissing split by moving the other one to fullscreen.
+ // Since we don't have any animations for this yet, just use the internal example
+ // animations.
+ return true;
+ }
+
+ private void addDividerBarToTransition(@NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction t, boolean show) {
+ final SurfaceControl leash = mSplitLayout.getDividerLeash();
+ final TransitionInfo.Change barChange = new TransitionInfo.Change(null /* token */, leash);
+ final Rect bounds = mSplitLayout.getDividerBounds();
+ barChange.setStartAbsBounds(bounds);
+ barChange.setEndAbsBounds(bounds);
+ barChange.setMode(show ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK);
+ barChange.setFlags(FLAG_IS_DIVIDER_BAR);
+ // Technically this should be order-0, but this is running after layer assignment
+ // and it's a special case, so just add to end.
+ info.addChange(barChange);
+ // Be default, make it visible. The remote animator can adjust alpha if it plans to animate.
+ if (show) {
+ t.setAlpha(leash, 1.f);
+ t.setLayer(leash, Integer.MAX_VALUE);
+ t.setPosition(leash, bounds.left, bounds.top);
+ t.show(leash);
+ }
+ }
+
@Override
public void dump(@NonNull PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
@@ -469,6 +838,16 @@ class StageCoordinator implements SplitLayout.LayoutChangeListener,
pw.println(innerPrefix + "mSplitLayout=" + mSplitLayout);
}
+ /**
+ * Directly set the visibility of both splits. This assumes hasChildren matches visibility.
+ * This is intended for batch use, so it assumes other state management logic is already
+ * handled.
+ */
+ private void setSplitsVisible(boolean visible) {
+ mMainStageListener.mVisible = mSideStageListener.mVisible = visible;
+ mMainStageListener.mHasChildren = mSideStageListener.mHasChildren = visible;
+ }
+
class StageListenerImpl implements StageTaskListener.StageListenerCallbacks {
boolean mHasRootTask = false;
boolean mVisible = false;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
index b8cdc4ab4d75..1da0a2d82766 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
@@ -21,6 +21,8 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
+
import android.annotation.CallSuper;
import android.app.ActivityManager;
import android.graphics.Point;
@@ -33,7 +35,6 @@ import androidx.annotation.NonNull;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.transition.Transitions;
import java.io.PrintWriter;
@@ -76,6 +77,14 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this);
}
+ int getChildCount() {
+ return mChildrenTaskInfo.size();
+ }
+
+ boolean containsTask(int taskId) {
+ return mChildrenTaskInfo.contains(taskId);
+ }
+
@Override
@CallSuper
public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
@@ -83,17 +92,22 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
mRootLeash = leash;
mRootTaskInfo = taskInfo;
mCallbacks.onRootTaskAppeared();
+ sendStatusChanged();
} else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) {
final int taskId = taskInfo.taskId;
mChildrenLeashes.put(taskId, leash);
mChildrenTaskInfo.put(taskId, taskInfo);
updateChildTaskSurface(taskInfo, leash, true /* firstAppeared */);
mCallbacks.onChildTaskStatusChanged(taskId, true /* present */, taskInfo.isVisible);
+ if (ENABLE_SHELL_TRANSITIONS) {
+ // Status is managed/synchronized by the transition lifecycle.
+ return;
+ }
+ sendStatusChanged();
} else {
throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
+ "\n mRootTaskInfo: " + mRootTaskInfo);
}
- sendStatusChanged();
}
@Override
@@ -103,14 +117,20 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
mRootTaskInfo = taskInfo;
} else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) {
mChildrenTaskInfo.put(taskInfo.taskId, taskInfo);
- updateChildTaskSurface(
- taskInfo, mChildrenLeashes.get(taskInfo.taskId), false /* firstAppeared */);
mCallbacks.onChildTaskStatusChanged(taskInfo.taskId, true /* present */,
taskInfo.isVisible);
+ if (!ENABLE_SHELL_TRANSITIONS) {
+ updateChildTaskSurface(
+ taskInfo, mChildrenLeashes.get(taskInfo.taskId), false /* firstAppeared */);
+ }
} else {
throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
+ "\n mRootTaskInfo: " + mRootTaskInfo);
}
+ if (ENABLE_SHELL_TRANSITIONS) {
+ // Status is managed/synchronized by the transition lifecycle.
+ return;
+ }
sendStatusChanged();
}
@@ -124,8 +144,12 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
} else if (mChildrenTaskInfo.contains(taskId)) {
mChildrenTaskInfo.remove(taskId);
mChildrenLeashes.remove(taskId);
- sendStatusChanged();
mCallbacks.onChildTaskStatusChanged(taskId, false /* present */, taskInfo.isVisible);
+ if (ENABLE_SHELL_TRANSITIONS) {
+ // Status is managed/synchronized by the transition lifecycle.
+ return;
+ }
+ sendStatusChanged();
} else {
throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
+ "\n mRootTaskInfo: " + mRootTaskInfo);
@@ -166,7 +190,7 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
mSyncQueue.runInSync(t -> {
t.setWindowCrop(leash, null);
t.setPosition(leash, taskPositionInParent.x, taskPositionInParent.y);
- if (firstAppeared && !Transitions.ENABLE_SHELL_TRANSITIONS) {
+ if (firstAppeared && !ENABLE_SHELL_TRANSITIONS) {
t.setAlpha(leash, 1f);
t.setMatrix(leash, 1, 0, 0, 1);
t.show(leash);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index ca1b53d4d46b..64dc47f5549a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -18,6 +18,7 @@ package com.android.wm.shell.transition;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
@@ -70,6 +71,12 @@ public class Transitions implements RemoteCallable<Transitions> {
public static final boolean ENABLE_SHELL_TRANSITIONS =
SystemProperties.getBoolean("persist.debug.shell_transit", false);
+ /** Transition type for dismissing split-screen via dragging the divider off the screen. */
+ public static final int TRANSIT_SPLIT_DISMISS_SNAP = TRANSIT_FIRST_CUSTOM + 1;
+
+ /** Transition type for launching 2 tasks simultaneously. */
+ public static final int TRANSIT_SPLIT_SCREEN_PAIR_OPEN = TRANSIT_FIRST_CUSTOM + 2;
+
private final WindowOrganizer mOrganizer;
private final Context mContext;
private final ShellExecutor mMainExecutor;
@@ -209,6 +216,11 @@ public class Transitions implements RemoteCallable<Transitions> {
|| type == WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
}
+ /** @return true if the transition was triggered by closing something vs opening something */
+ public static boolean isClosingType(@WindowManager.TransitionType int type) {
+ return type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK;
+ }
+
/**
* Reparents all participants into a shared parent and orders them based on: the global transit
* type, their transit mode, and their destination z-order.
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java
index 1f58a8546796..b0de02922f74 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java
@@ -17,18 +17,32 @@
package com.android.wm.shell;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
import android.app.ActivityManager;
+import android.app.WindowConfiguration;
import android.graphics.Rect;
+import android.os.IBinder;
import android.window.IWindowContainerToken;
import android.window.WindowContainerToken;
public final class TestRunningTaskInfoBuilder {
static int sNextTaskId = 500;
private Rect mBounds = new Rect(0, 0, 100, 100);
- private WindowContainerToken mToken =
- new WindowContainerToken(new IWindowContainerToken.Default());
+
+ private WindowContainerToken mToken = createMockWCToken();
private int mParentTaskId = INVALID_TASK_ID;
+ private @WindowConfiguration.ActivityType int mActivityType = ACTIVITY_TYPE_STANDARD;
+
+ public static WindowContainerToken createMockWCToken() {
+ final IWindowContainerToken itoken = mock(IWindowContainerToken.class);
+ final IBinder asBinder = mock(IBinder.class);
+ doReturn(asBinder).when(itoken).asBinder();
+ return new WindowContainerToken(itoken);
+ }
public TestRunningTaskInfoBuilder setBounds(Rect bounds) {
mBounds.set(bounds);
@@ -40,12 +54,19 @@ public final class TestRunningTaskInfoBuilder {
return this;
}
+ public TestRunningTaskInfoBuilder setActivityType(
+ @WindowConfiguration.ActivityType int activityType) {
+ mActivityType = activityType;
+ return this;
+ }
+
public ActivityManager.RunningTaskInfo build() {
final ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo();
info.parentTaskId = INVALID_TASK_ID;
info.taskId = sNextTaskId++;
info.parentTaskId = mParentTaskId;
info.configuration.windowConfiguration.setBounds(mBounds);
+ info.configuration.windowConfiguration.setActivityType(mActivityType);
info.token = mToken;
info.isResizeable = true;
return info;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
new file mode 100644
index 000000000000..ab6f76996398
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.splitscreen;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.SurfaceControl;
+import android.window.DisplayAreaInfo;
+import android.window.IWindowContainerToken;
+import android.window.WindowContainerToken;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayImeController;
+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.transition.Transitions;
+
+public class SplitTestUtils {
+
+ static SplitLayout createMockSplitLayout() {
+ final Rect dividerBounds = new Rect(48, 0, 52, 100);
+ final SurfaceControl leash = createMockSurface();
+ SplitLayout out = mock(SplitLayout.class);
+ doReturn(dividerBounds).when(out).getDividerBounds();
+ doReturn(leash).when(out).getDividerLeash();
+ return out;
+ }
+
+ static SurfaceControl createMockSurface() {
+ return createMockSurface(true);
+ }
+
+ static SurfaceControl createMockSurface(boolean valid) {
+ SurfaceControl sc = mock(SurfaceControl.class);
+ ExtendedMockito.doReturn(valid).when(sc).isValid();
+ return sc;
+ }
+
+ static class TestStageCoordinator extends StageCoordinator {
+ final DisplayAreaInfo mDisplayAreaInfo;
+
+ TestStageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
+ RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer,
+ MainStage mainStage, SideStage sideStage, DisplayImeController imeController,
+ SplitLayout splitLayout, Transitions transitions, TransactionPool transactionPool) {
+ super(context, displayId, syncQueue, rootTDAOrganizer, taskOrganizer, mainStage,
+ sideStage, imeController, splitLayout, transitions, transactionPool);
+
+ // Prepare default TaskDisplayArea for testing.
+ mDisplayAreaInfo = new DisplayAreaInfo(
+ new WindowContainerToken(new IWindowContainerToken.Default()),
+ DEFAULT_DISPLAY,
+ FEATURE_DEFAULT_TASK_CONTAINER);
+ this.onDisplayAreaAppeared(mDisplayAreaInfo);
+ }
+ }
+}
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
new file mode 100644
index 000000000000..18642fcfef6c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.splitscreen;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_CLOSE;
+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 com.android.wm.shell.splitscreen.SplitTestUtils.createMockSurface;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+import android.annotation.NonNull;
+import android.app.ActivityManager;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.view.SurfaceControl;
+import android.window.IRemoteTransition;
+import android.window.IRemoteTransitionFinishedCallback;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerTransaction;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TestRunningTaskInfoBuilder;
+import com.android.wm.shell.common.DisplayImeController;
+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.transition.Transitions;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
+
+/** Tests for {@link StageCoordinator} */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SplitTransitionTests extends ShellTestCase {
+ @Mock private ShellTaskOrganizer mTaskOrganizer;
+ @Mock private SyncTransactionQueue mSyncQueue;
+ @Mock private RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
+ @Mock private DisplayImeController mDisplayImeController;
+ @Mock private TransactionPool mTransactionPool;
+ @Mock private Transitions mTransitions;
+ private SplitLayout mSplitLayout;
+ private MainStage mMainStage;
+ private SideStage mSideStage;
+ private StageCoordinator mStageCoordinator;
+ private SplitScreenTransitions mSplitScreenTransitions;
+
+ private ActivityManager.RunningTaskInfo mMainChild;
+ private ActivityManager.RunningTaskInfo mSideChild;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ final ShellExecutor mockExecutor = mock(ShellExecutor.class);
+ doReturn(mockExecutor).when(mTransitions).getMainExecutor();
+ doReturn(mockExecutor).when(mTransitions).getAnimExecutor();
+ doReturn(mock(SurfaceControl.Transaction.class)).when(mTransactionPool).acquire();
+ mSplitLayout = SplitTestUtils.createMockSplitLayout();
+ mMainStage = new MainStage(mTaskOrganizer, DEFAULT_DISPLAY, mock(
+ StageTaskListener.StageListenerCallbacks.class), mSyncQueue);
+ mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
+ mSideStage = new SideStage(mTaskOrganizer, DEFAULT_DISPLAY, mock(
+ StageTaskListener.StageListenerCallbacks.class), mSyncQueue);
+ mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
+ mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
+ mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage,
+ mDisplayImeController, mSplitLayout, mTransitions, mTransactionPool);
+ mSplitScreenTransitions = mStageCoordinator.getSplitTransitions();
+ doAnswer((Answer<IBinder>) invocation -> mock(IBinder.class))
+ .when(mTransitions).startTransition(anyInt(), any(), any());
+
+ mMainChild = new TestRunningTaskInfoBuilder()
+ .setParentTaskId(mMainStage.mRootTaskInfo.taskId).build();
+ mSideChild = new TestRunningTaskInfoBuilder()
+ .setParentTaskId(mSideStage.mRootTaskInfo.taskId).build();
+ }
+
+ @Test
+ public void testLaunchPair() {
+ TransitionInfo info = createEnterPairInfo();
+
+ TestRemoteTransition testRemote = new TestRemoteTransition();
+
+ IBinder transition = mSplitScreenTransitions.startEnterTransition(
+ TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(), testRemote,
+ mStageCoordinator);
+ mMainStage.onTaskAppeared(mMainChild, createMockSurface());
+ mSideStage.onTaskAppeared(mSideChild, createMockSurface());
+ boolean accepted = mStageCoordinator.startAnimation(transition, info,
+ mock(SurfaceControl.Transaction.class),
+ mock(Transitions.TransitionFinishCallback.class));
+ assertTrue(accepted);
+
+ // Make sure split-screen is now visible
+ assertTrue(mStageCoordinator.isSplitScreenVisible());
+ assertTrue(testRemote.mCalled);
+ }
+
+ @Test
+ public void testMonitorInSplit() {
+ enterSplit();
+
+ ActivityManager.RunningTaskInfo newTask = new TestRunningTaskInfoBuilder()
+ .setParentTaskId(mSideStage.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);
+
+ // while in split, it should handle everything:
+ assertNotNull(result);
+
+ // Not exiting, just opening up another side-stage task.
+ assertFalse(containsSplitExit(result));
+
+ // simulate the transition
+ TransitionInfo.Change openChange = createChange(TRANSIT_TO_FRONT, newTask);
+ TransitionInfo.Change hideChange = createChange(TRANSIT_TO_BACK, mSideChild);
+
+ TransitionInfo info = new TransitionInfo(TRANSIT_TO_FRONT, 0);
+ info.addChange(openChange);
+ info.addChange(hideChange);
+ mSideStage.onTaskAppeared(newTask, createMockSurface());
+ boolean accepted = mStageCoordinator.startAnimation(transition, info,
+ mock(SurfaceControl.Transaction.class),
+ mock(Transitions.TransitionFinishCallback.class));
+ assertFalse(accepted);
+ assertTrue(mStageCoordinator.isSplitScreenVisible());
+
+ // same, but create request to close the new task
+ request = new TransitionRequestInfo(TRANSIT_CLOSE, newTask, null);
+ transition = mock(IBinder.class);
+ result = mStageCoordinator.handleRequest(transition, request);
+ assertNotNull(result);
+ assertFalse(containsSplitExit(result));
+
+ TransitionInfo.Change showChange = createChange(TRANSIT_TO_FRONT, mSideChild);
+ TransitionInfo.Change closeChange = createChange(TRANSIT_CLOSE, newTask);
+
+ info = new TransitionInfo(TRANSIT_CLOSE, 0);
+ info.addChange(showChange);
+ info.addChange(closeChange);
+ mSideStage.onTaskVanished(newTask);
+ accepted = mStageCoordinator.startAnimation(transition, info,
+ mock(SurfaceControl.Transaction.class),
+ mock(Transitions.TransitionFinishCallback.class));
+ assertFalse(accepted);
+ assertTrue(mStageCoordinator.isSplitScreenVisible());
+ }
+
+ @Test
+ public void testDismissToHome() {
+ enterSplit();
+
+ ActivityManager.RunningTaskInfo homeTask = new TestRunningTaskInfoBuilder()
+ .setActivityType(ACTIVITY_TYPE_HOME).build();
+
+ // Create a request to bring home forward
+ TransitionRequestInfo request = new TransitionRequestInfo(TRANSIT_TO_FRONT, homeTask, null);
+ IBinder transition = mock(IBinder.class);
+ WindowContainerTransaction result = mStageCoordinator.handleRequest(transition, request);
+
+ assertTrue(containsSplitExit(result));
+
+ // make sure we haven't made any local changes yet (need to wait until transition is ready)
+ assertTrue(mStageCoordinator.isSplitScreenVisible());
+
+ // simulate the transition
+ TransitionInfo.Change homeChange = createChange(TRANSIT_TO_FRONT, homeTask);
+ TransitionInfo.Change mainChange = createChange(TRANSIT_TO_BACK, mMainChild);
+ TransitionInfo.Change sideChange = createChange(TRANSIT_TO_BACK, mSideChild);
+
+ TransitionInfo info = new TransitionInfo(TRANSIT_TO_FRONT, 0);
+ info.addChange(homeChange);
+ info.addChange(mainChange);
+ info.addChange(sideChange);
+ mMainStage.onTaskVanished(mMainChild);
+ mSideStage.onTaskVanished(mSideChild);
+ mStageCoordinator.startAnimation(transition, info,
+ mock(SurfaceControl.Transaction.class),
+ mock(Transitions.TransitionFinishCallback.class));
+ assertFalse(mStageCoordinator.isSplitScreenVisible());
+ }
+
+ @Test
+ public void testDismissSnap() {
+ enterSplit();
+
+ // simulate the transition
+ TransitionInfo.Change mainChange = createChange(TRANSIT_TO_BACK, mMainChild);
+ TransitionInfo.Change sideChange = createChange(TRANSIT_CHANGE, mSideChild);
+
+ TransitionInfo info = new TransitionInfo(TRANSIT_TO_BACK, 0);
+ info.addChange(mainChange);
+ info.addChange(sideChange);
+ IBinder transition = mStageCoordinator.onSnappedToDismissTransition(
+ false /* mainStageToTop */);
+ mMainStage.onTaskVanished(mMainChild);
+ mSideStage.onTaskVanished(mSideChild);
+ boolean accepted = mStageCoordinator.startAnimation(transition, info,
+ mock(SurfaceControl.Transaction.class),
+ mock(Transitions.TransitionFinishCallback.class));
+ assertTrue(accepted);
+ assertFalse(mStageCoordinator.isSplitScreenVisible());
+ }
+
+ @Test
+ public void testDismissFromAppFinish() {
+ enterSplit();
+
+ // Create a request to exit the "last" task on side stage
+ TransitionRequestInfo request = new TransitionRequestInfo(TRANSIT_CLOSE, mSideChild, null);
+ IBinder transition = mock(IBinder.class);
+ WindowContainerTransaction result = mStageCoordinator.handleRequest(transition, request);
+
+ assertTrue(containsSplitExit(result));
+
+ // make sure we haven't made any local changes yet (need to wait until transition is ready)
+ assertTrue(mStageCoordinator.isSplitScreenVisible());
+
+ // simulate the transition
+ TransitionInfo.Change mainChange = createChange(TRANSIT_CHANGE, mMainChild);
+ TransitionInfo.Change sideChange = createChange(TRANSIT_CLOSE, mSideChild);
+
+ TransitionInfo info = new TransitionInfo(TRANSIT_CLOSE, 0);
+ info.addChange(mainChange);
+ info.addChange(sideChange);
+ mMainStage.onTaskVanished(mMainChild);
+ mSideStage.onTaskVanished(mSideChild);
+ boolean accepted = mStageCoordinator.startAnimation(transition, info,
+ mock(SurfaceControl.Transaction.class),
+ mock(Transitions.TransitionFinishCallback.class));
+ assertTrue(accepted);
+ assertFalse(mStageCoordinator.isSplitScreenVisible());
+ }
+
+ private TransitionInfo createEnterPairInfo() {
+ TransitionInfo.Change mainChange = createChange(TRANSIT_OPEN, mMainChild);
+ TransitionInfo.Change sideChange = createChange(TRANSIT_OPEN, mSideChild);
+
+ TransitionInfo info = new TransitionInfo(TRANSIT_SPLIT_SCREEN_PAIR_OPEN, 0);
+ info.addChange(mainChange);
+ info.addChange(sideChange);
+ return info;
+ }
+
+ private void enterSplit() {
+ TransitionInfo enterInfo = createEnterPairInfo();
+ IBinder enterTransit = mSplitScreenTransitions.startEnterTransition(
+ TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(),
+ new TestRemoteTransition(), mStageCoordinator);
+ mMainStage.onTaskAppeared(mMainChild, createMockSurface());
+ mSideStage.onTaskAppeared(mSideChild, createMockSurface());
+ mStageCoordinator.startAnimation(enterTransit, enterInfo,
+ mock(SurfaceControl.Transaction.class),
+ mock(Transitions.TransitionFinishCallback.class));
+ mMainStage.activate(new Rect(0, 0, 100, 100), new WindowContainerTransaction());
+ }
+
+ private boolean containsSplitExit(@NonNull WindowContainerTransaction wct) {
+ // reparenting of child tasks to null constitutes exiting split.
+ boolean reparentedMain = false;
+ boolean reparentedSide = false;
+ for (int i = 0; i < wct.getHierarchyOps().size(); ++i) {
+ WindowContainerTransaction.HierarchyOp op = wct.getHierarchyOps().get(i);
+ if (op.getType() == HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT) {
+ if (op.getContainer() == mMainStage.mRootTaskInfo.token.asBinder()
+ && op.getNewParent() == null) {
+ reparentedMain = true;
+ } else if (op.getContainer() == mSideStage.mRootTaskInfo.token.asBinder()
+ && op.getNewParent() == null) {
+ reparentedSide = true;
+ }
+ }
+ }
+ return reparentedMain && reparentedSide;
+ }
+
+ private static TransitionInfo.Change createChange(@TransitionInfo.TransitionMode int mode,
+ ActivityManager.RunningTaskInfo taskInfo) {
+ TransitionInfo.Change out = new TransitionInfo.Change(taskInfo.token, createMockSurface());
+ out.setMode(mode);
+ out.setTaskInfo(taskInfo);
+ return out;
+ }
+
+ class TestRemoteTransition extends IRemoteTransition.Stub {
+ boolean mCalled = false;
+ final WindowContainerTransaction mRemoteFinishWCT = new WindowContainerTransaction();
+
+ @Override
+ public void startAnimation(TransitionInfo info, SurfaceControl.Transaction t,
+ IRemoteTransitionFinishedCallback finishCallback) throws RemoteException {
+ mCalled = true;
+ finishCallback.onTransitionFinished(mRemoteFinishWCT);
+ }
+ }
+
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index 74753aac4a24..924e94679831 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -17,7 +17,6 @@
package com.android.wm.shell.splitscreen;
import static android.view.Display.DEFAULT_DISPLAY;
-import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_POSITION_BOTTOM_OR_RIGHT;
@@ -27,11 +26,7 @@ import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
import android.app.ActivityManager;
-import android.content.Context;
import android.graphics.Rect;
-import android.window.DisplayAreaInfo;
-import android.window.IWindowContainerToken;
-import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -43,6 +38,8 @@ import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestRunningTaskInfoBuilder;
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.transition.Transitions;
import org.junit.Before;
import org.junit.Test;
@@ -60,13 +57,16 @@ public class StageCoordinatorTests extends ShellTestCase {
@Mock private MainStage mMainStage;
@Mock private SideStage mSideStage;
@Mock private DisplayImeController mDisplayImeController;
+ @Mock private Transitions mTransitions;
+ @Mock private TransactionPool mTransactionPool;
private StageCoordinator mStageCoordinator;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
- mStageCoordinator = new TestStageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
- mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage, mDisplayImeController);
+ mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
+ mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage,
+ mDisplayImeController, null /* splitLayout */, mTransitions, mTransactionPool);
}
@Test
@@ -90,22 +90,4 @@ public class StageCoordinatorTests extends ShellTestCase {
verify(mSideStage).removeTask(
eq(task.taskId), any(), any(WindowContainerTransaction.class));
}
-
- private static class TestStageCoordinator extends StageCoordinator {
- final DisplayAreaInfo mDisplayAreaInfo;
-
- TestStageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
- RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer,
- MainStage mainStage, SideStage sideStage, DisplayImeController imeController) {
- super(context, displayId, syncQueue, rootTDAOrganizer, taskOrganizer, mainStage,
- sideStage, imeController);
-
- // Prepare default TaskDisplayArea for testing.
- mDisplayAreaInfo = new DisplayAreaInfo(
- new WindowContainerToken(new IWindowContainerToken.Default()),
- DEFAULT_DISPLAY,
- FEATURE_DEFAULT_TASK_CONTAINER);
- this.onDisplayAreaAppeared(mDisplayAreaInfo);
- }
- }
}