diff options
| author | 2021-03-29 13:56:51 -0700 | |
|---|---|---|
| committer | 2021-04-09 13:13:56 -0700 | |
| commit | 2bc5bd5168c84c66e8fae6a98fef25d70f17f0e3 (patch) | |
| tree | 78411bcfe565bb5b966cf63b9ed11e90321a8792 | |
| parent | 298bb6c1c7989f1d4b9b7c4c3631e44cebbd0316 (diff) | |
Add shell transition handling for staged split-screen.
This makes StageCoordinator implement the TransitionHandler
interface.
In general, this currently expects 'enter' transitions to
contain 2 tasks (one in each split). The current UX is undefined
when one only one of the splits is occupied, so for now it will
throw an exception if that case is hit.
There is a split-screen API called startTasks which takes a list
of tasks (currently only supports 2) and associated options and
creates a transition to both enter split-screen and launch those
tasks into their respective stages.
These are the currently accounted-for entrypoints into
the handler interface:
- Core-initiated (handleRequest)
  - in split: trigger=HOME that is opening -> full dismiss.
  - in split: trigger=task with a stage parent that is last closing in
                    that stage -> dismiss with other stage onTop
  - NOT split: trigger=task with stage parent -> exception
- Shell-initiated
  - NOT split: startTasks -> enter split with 2 tasks
  - in split: snap-to-dismiss -> dismiss with other stage on top
Bug: 182002789
Test: atest SplitTransitionTests
      Or use the experimental pair-launch split-screen and observe
      protologs to see clean transition-infos.
Change-Id: I4f4dd431ad5642cf98b4a01c32eb1d09e5b9a11e
17 files changed, 1268 insertions, 76 deletions
| diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 4b56fd740e8d..cca8257645fe 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -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. diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index 3b35b6dfc889..f1515276e2c6 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -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) { 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); -        } -    }  } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java index 4f3f86aeca97..26ef1455be7d 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java @@ -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));              }          };      } diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java index 45b0b59cd1d9..12d562d742f1 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java @@ -334,10 +334,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();          } diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 75be444567ff..e4ac1f6c5b50 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -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(); |