diff options
Diffstat (limited to 'libs')
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); - } - } } |