Merge "Add shell transition handling for staged split-screen." into sc-dev
This commit is contained in:
@@ -921,6 +921,30 @@ public interface WindowManager extends ViewManager {
|
||||
default void setForceCrossWindowBlurDisabled(boolean disable) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
static String transitTypeToString(@TransitionType int type) {
|
||||
switch (type) {
|
||||
case TRANSIT_NONE: return "NONE";
|
||||
case TRANSIT_OPEN: return "OPEN";
|
||||
case TRANSIT_CLOSE: return "CLOSE";
|
||||
case TRANSIT_TO_FRONT: return "TO_FRONT";
|
||||
case TRANSIT_TO_BACK: return "TO_BACK";
|
||||
case TRANSIT_RELAUNCH: return "RELAUNCH";
|
||||
case TRANSIT_CHANGE: return "CHANGE";
|
||||
case TRANSIT_KEYGUARD_GOING_AWAY: return "KEYGUARD_GOING_AWAY";
|
||||
case TRANSIT_KEYGUARD_OCCLUDE: return "KEYGUARD_OCCLUDE";
|
||||
case TRANSIT_KEYGUARD_UNOCCLUDE: return "KEYGUARD_UNOCCLUDE";
|
||||
case TRANSIT_FIRST_CUSTOM: return "FIRST_CUSTOM";
|
||||
default:
|
||||
if (type > TRANSIT_FIRST_CUSTOM) {
|
||||
return "FIRST_CUSTOM+" + (type - TRANSIT_FIRST_CUSTOM);
|
||||
}
|
||||
return "UNKNOWN(" + type + ")";
|
||||
}
|
||||
}
|
||||
|
||||
public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {
|
||||
/**
|
||||
* X position for this window. With the default gravity it is ignored.
|
||||
|
||||
@@ -23,6 +23,7 @@ import static android.view.WindowManager.TRANSIT_NONE;
|
||||
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 android.annotation.IntDef;
|
||||
import android.annotation.NonNull;
|
||||
@@ -205,7 +206,7 @@ public final class TransitionInfo implements Parcelable {
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("{t=" + mType + " f=" + Integer.toHexString(mFlags)
|
||||
sb.append("{t=" + transitTypeToString(mType) + " f=" + Integer.toHexString(mFlags)
|
||||
+ " ro=" + mRootOffset + " c=[");
|
||||
for (int i = 0; i < mChanges.size(); ++i) {
|
||||
if (i > 0) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,6 +415,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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.util.DataClass;
|
||||
import com.android.systemui.shared.recents.model.ThumbnailData;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Wrapper to expose RemoteTransition (shell transitions) to Launcher.
|
||||
*
|
||||
@@ -59,7 +61,8 @@ public class RemoteTransitionCompat implements Parcelable {
|
||||
mTransition = transition;
|
||||
}
|
||||
|
||||
public RemoteTransitionCompat(RemoteTransitionRunner runner) {
|
||||
public RemoteTransitionCompat(@NonNull RemoteTransitionRunner runner,
|
||||
@NonNull Executor executor) {
|
||||
mTransition = new IRemoteTransition.Stub() {
|
||||
@Override
|
||||
public void startAnimation(TransitionInfo info, SurfaceControl.Transaction t,
|
||||
@@ -71,7 +74,7 @@ public class RemoteTransitionCompat implements Parcelable {
|
||||
Log.e(TAG, "Failed to call transition finished callback", e);
|
||||
}
|
||||
};
|
||||
runner.startAnimation(info, t, finishAdapter);
|
||||
executor.execute(() -> runner.startAnimation(info, t, finishAdapter));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -335,10 +335,12 @@ public abstract class WMShellBaseModule {
|
||||
SyncTransactionQueue syncQueue, Context context,
|
||||
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
|
||||
@ShellMainThread ShellExecutor mainExecutor,
|
||||
DisplayImeController displayImeController) {
|
||||
DisplayImeController displayImeController, Transitions transitions,
|
||||
TransactionPool transactionPool) {
|
||||
if (ActivityTaskManager.supportsSplitScreenMultiWindow(context)) {
|
||||
return Optional.of(new SplitScreenController(shellTaskOrganizer, syncQueue, context,
|
||||
rootTaskDisplayAreaOrganizer, mainExecutor, displayImeController));
|
||||
rootTaskDisplayAreaOrganizer, mainExecutor, displayImeController, transitions,
|
||||
transactionPool));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import static android.view.WindowManager.TRANSIT_NONE;
|
||||
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 android.window.TransitionInfo.FLAG_IS_WALLPAPER;
|
||||
import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER;
|
||||
import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
|
||||
@@ -403,7 +404,7 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe
|
||||
sb.append("TransitionRecord{");
|
||||
sb.append(Integer.toHexString(System.identityHashCode(this)));
|
||||
sb.append(" id=" + mSyncId);
|
||||
sb.append(" type=" + mType);
|
||||
sb.append(" type=" + transitTypeToString(mType));
|
||||
sb.append(" flags=" + mFlags);
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
|
||||
Reference in New Issue
Block a user