diff options
Diffstat (limited to 'libs')
24 files changed, 812 insertions, 492 deletions
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index df5f921f3a62..c6197c8a730b 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -111,4 +111,8 @@ <!-- Whether to dim a split-screen task when the other is the IME target --> <bool name="config_dimNonImeAttachedSide">true</bool> + + <!-- Components support to launch multiple instances into split-screen --> + <string-array name="config_componentsSupportMultiInstancesSplit"> + </string-array> </resources> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index 43679364b443..e58e785850fa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -16,14 +16,12 @@ package com.android.wm.shell; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; -import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; @@ -48,7 +46,6 @@ import android.window.StartingWindowInfo; import android.window.StartingWindowRemovalInfo; import android.window.TaskAppearedInfo; import android.window.TaskOrganizer; -import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; @@ -567,6 +564,22 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } } + /** + * Return list of {@link RunningTaskInfo}s for the given display. + * + * @return filtered list of tasks or empty list + */ + public ArrayList<RunningTaskInfo> getRunningTasks(int displayId) { + ArrayList<RunningTaskInfo> result = new ArrayList<>(); + for (int i = 0; i < mTasks.size(); i++) { + RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo(); + if (taskInfo.displayId == displayId) { + result.add(taskInfo); + } + } + return result; + } + /** Gets running task by taskId. Returns {@code null} if no such task observed. */ @Nullable public RunningTaskInfo getRunningTaskInfo(int taskId) { @@ -693,57 +706,6 @@ public class ShellTaskOrganizer extends TaskOrganizer implements taskListener.reparentChildSurfaceToTask(taskId, sc, t); } - /** - * Create a {@link WindowContainerTransaction} to clear task bounds. - * - * Only affects tasks that have {@link RunningTaskInfo#getActivityType()} set to - * {@link WindowConfiguration#ACTIVITY_TYPE_STANDARD}. - * - * @param displayId display id for tasks that will have bounds cleared - * @return {@link WindowContainerTransaction} with pending operations to clear bounds - */ - public WindowContainerTransaction prepareClearBoundsForStandardTasks(int displayId) { - ProtoLog.d(WM_SHELL_DESKTOP_MODE, "prepareClearBoundsForTasks: displayId=%d", displayId); - WindowContainerTransaction wct = new WindowContainerTransaction(); - for (int i = 0; i < mTasks.size(); i++) { - RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo(); - if ((taskInfo.displayId == displayId) && (taskInfo.getActivityType() - == WindowConfiguration.ACTIVITY_TYPE_STANDARD)) { - ProtoLog.d(WM_SHELL_DESKTOP_MODE, "clearing bounds for token=%s taskInfo=%s", - taskInfo.token, taskInfo); - wct.setBounds(taskInfo.token, null); - } - } - return wct; - } - - /** - * Create a {@link WindowContainerTransaction} to clear task level freeform setting. - * - * Only affects tasks that have {@link RunningTaskInfo#getActivityType()} set to - * {@link WindowConfiguration#ACTIVITY_TYPE_STANDARD}. - * - * @param displayId display id for tasks that will have windowing mode reset to {@link - * WindowConfiguration#WINDOWING_MODE_UNDEFINED} - * @return {@link WindowContainerTransaction} with pending operations to clear windowing mode - */ - public WindowContainerTransaction prepareClearFreeformForStandardTasks(int displayId) { - ProtoLog.d(WM_SHELL_DESKTOP_MODE, "prepareClearFreeformForTasks: displayId=%d", displayId); - WindowContainerTransaction wct = new WindowContainerTransaction(); - for (int i = 0; i < mTasks.size(); i++) { - RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo(); - if (taskInfo.displayId == displayId - && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM - && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD) { - ProtoLog.d(WM_SHELL_DESKTOP_MODE, - "clearing windowing mode for token=%s taskInfo=%s", taskInfo.token, - taskInfo); - wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED); - } - } - return wct; - } - private void logSizeCompatRestartButtonEventReported(@NonNull TaskAppearedInfo info, int event) { ActivityInfo topActivityInfo = info.getTaskInfo().topActivityInfo; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/back/OWNERS new file mode 100644 index 000000000000..1e0f9bc6322f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/OWNERS @@ -0,0 +1,5 @@ +# WM shell sub-module back navigation owners +# Bug component: 1152663 +shanh@google.com +arthurhung@google.com +wilsonshih@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java index 8bc16bcc9d9d..1474754fc7f7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java @@ -46,8 +46,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.policy.DividerSnapAlgorithm; +import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.animation.Interpolators; +import com.android.wm.shell.protolog.ShellProtoLogGroup; /** * Divider for multi window splits. @@ -364,8 +366,11 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mViewHost.relayout(lp); } - void setInteractive(boolean interactive) { + void setInteractive(boolean interactive, String from) { if (interactive == mInteractive) return; + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "Set divider bar %s from %s", interactive ? "interactive" : "non-interactive", + from); mInteractive = interactive; releaseTouching(); mHandle.setVisibility(mInteractive ? View.VISIBLE : View.INVISIBLE); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java index 74f8bf9ac863..5b7ed278e843 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java @@ -48,6 +48,7 @@ import androidx.annotation.NonNull; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; +import com.android.wm.shell.common.ScreenshotUtils; import com.android.wm.shell.common.SurfaceUtils; import java.util.function.Consumer; @@ -74,10 +75,14 @@ public class SplitDecorManager extends WindowlessWindowManager { private boolean mShown; private boolean mIsResizing; - private Rect mBounds = new Rect(); + private final Rect mBounds = new Rect(); + private final Rect mResizingBounds = new Rect(); + private final Rect mTempRect = new Rect(); private ValueAnimator mFadeAnimator; private int mIconSize; + private int mOffsetX; + private int mOffsetY; public SplitDecorManager(Configuration configuration, IconProvider iconProvider, SurfaceSession surfaceSession) { @@ -158,7 +163,7 @@ public class SplitDecorManager extends WindowlessWindowManager { /** Showing resizing hint. */ public void onResizing(ActivityManager.RunningTaskInfo resizingTask, Rect newBounds, - Rect sideBounds, SurfaceControl.Transaction t) { + Rect sideBounds, SurfaceControl.Transaction t, int offsetX, int offsetY) { if (mResizingIconView == null) { return; } @@ -167,6 +172,9 @@ public class SplitDecorManager extends WindowlessWindowManager { mIsResizing = true; mBounds.set(newBounds); } + mResizingBounds.set(newBounds); + mOffsetX = offsetX; + mOffsetY = offsetY; final boolean show = newBounds.width() > mBounds.width() || newBounds.height() > mBounds.height(); @@ -221,11 +229,37 @@ public class SplitDecorManager extends WindowlessWindowManager { /** Stops showing resizing hint. */ public void onResized(SurfaceControl.Transaction t) { + if (!mShown && mIsResizing) { + mTempRect.set(mResizingBounds); + mTempRect.offsetTo(-mOffsetX, -mOffsetY); + final SurfaceControl screenshot = ScreenshotUtils.takeScreenshot(t, + mHostLeash, mTempRect, Integer.MAX_VALUE - 1); + + final SurfaceControl.Transaction animT = new SurfaceControl.Transaction(); + final ValueAnimator va = ValueAnimator.ofFloat(1, 0); + va.addUpdateListener(valueAnimator -> { + final float progress = (float) valueAnimator.getAnimatedValue(); + animT.setAlpha(screenshot, progress); + animT.apply(); + }); + va.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(@androidx.annotation.NonNull Animator animation) { + animT.remove(screenshot); + animT.apply(); + animT.close(); + } + }); + va.start(); + } + if (mResizingIconView == null) { return; } mIsResizing = false; + mOffsetX = 0; + mOffsetY = 0; if (mFadeAnimator != null && mFadeAnimator.isRunning()) { if (!mShown) { // If fade-out animation is running, just add release callback to it. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index 295a2e3c4244..3de1045bfbda 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -448,7 +448,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange */ void updateDivideBounds(int position) { updateBounds(position); - mSplitLayoutHandler.onLayoutSizeChanging(this); + mSplitLayoutHandler.onLayoutSizeChanging(this, mSurfaceEffectPolicy.mParallaxOffset.x, + mSurfaceEffectPolicy.mParallaxOffset.y); } void setDividePosition(int position, boolean applyLayoutChange) { @@ -600,7 +601,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange animator.start(); } - /** Swich both surface position with animation. */ + /** Switch both surface position with animation. */ public void splitSwitching(SurfaceControl.Transaction t, SurfaceControl leash1, SurfaceControl leash2, Consumer<Rect> finishCallback) { final boolean isLandscape = isLandscape(); @@ -811,7 +812,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl, * SurfaceControl, SurfaceControl, boolean) */ - void onLayoutSizeChanging(SplitLayout layout); + void onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY); /** * Calls when finish resizing the split bounds. @@ -1092,7 +1093,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange // ImePositionProcessor#onImeVisibilityChanged directly in DividerView is not enough // because DividerView won't receive onImeVisibilityChanged callback after it being // re-inflated. - mSplitWindowManager.setInteractive(!mImeShown || !mHasImeFocus); + mSplitWindowManager.setInteractive(!mImeShown || !mHasImeFocus, + "onImeStartPositioning"); return needOffset ? IME_ANIMATION_NO_ALPHA : 0; } @@ -1118,7 +1120,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange // Restore the split layout when wm-shell is not controlling IME insets anymore. if (!controlling && mImeShown) { reset(); - mSplitWindowManager.setInteractive(true); + mSplitWindowManager.setInteractive(true, "onImeControlTargetChanged"); mSplitLayoutHandler.setLayoutOffsetTarget(0, 0, SplitLayout.this); mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java index 864b9a7528b0..060ac56cae96 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java @@ -166,9 +166,9 @@ public final class SplitWindowManager extends WindowlessWindowManager { } } - void setInteractive(boolean interactive) { + void setInteractive(boolean interactive, String from) { if (mDividerView == null) return; - mDividerView.setInteractive(interactive); + mDividerView.setInteractive(interactive, from); } View getDividerView() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java index 34ff6d814c8d..abc4024bc290 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java @@ -16,8 +16,11 @@ package com.android.wm.shell.desktopmode; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_FRONT; @@ -151,21 +154,18 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll int displayId = mContext.getDisplayId(); + ArrayList<RunningTaskInfo> runningTasks = mShellTaskOrganizer.getRunningTasks(displayId); + WindowContainerTransaction wct = new WindowContainerTransaction(); - // Reset freeform windowing mode that is set per task level (tasks should inherit - // container value) - wct.merge(mShellTaskOrganizer.prepareClearFreeformForStandardTasks(displayId), - true /* transfer */); - int targetWindowingMode; + // Reset freeform windowing mode that is set per task level so tasks inherit it + clearFreeformForStandardTasks(runningTasks, wct); if (active) { - targetWindowingMode = WINDOWING_MODE_FREEFORM; + moveHomeBehindVisibleTasks(runningTasks, wct); + setDisplayAreaWindowingMode(displayId, WINDOWING_MODE_FREEFORM, wct); } else { - targetWindowingMode = WINDOWING_MODE_FULLSCREEN; - // Clear any resized bounds - wct.merge(mShellTaskOrganizer.prepareClearBoundsForStandardTasks(displayId), - true /* transfer */); + clearBoundsForStandardTasks(runningTasks, wct); + setDisplayAreaWindowingMode(displayId, WINDOWING_MODE_FULLSCREEN, wct); } - prepareWindowingModeChange(wct, displayId, targetWindowingMode); if (Transitions.ENABLE_SHELL_TRANSITIONS) { mTransitions.startTransition(TRANSIT_CHANGE, wct, null); } else { @@ -173,17 +173,69 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll } } - private void prepareWindowingModeChange(WindowContainerTransaction wct, - int displayId, @WindowConfiguration.WindowingMode int windowingMode) { - DisplayAreaInfo displayAreaInfo = mRootTaskDisplayAreaOrganizer - .getDisplayAreaInfo(displayId); + private WindowContainerTransaction clearBoundsForStandardTasks( + ArrayList<RunningTaskInfo> runningTasks, WindowContainerTransaction wct) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "prepareClearBoundsForTasks"); + for (RunningTaskInfo taskInfo : runningTasks) { + if (taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "clearing bounds for token=%s taskInfo=%s", + taskInfo.token, taskInfo); + wct.setBounds(taskInfo.token, null); + } + } + return wct; + } + + private void clearFreeformForStandardTasks(ArrayList<RunningTaskInfo> runningTasks, + WindowContainerTransaction wct) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "prepareClearFreeformForTasks"); + for (RunningTaskInfo taskInfo : runningTasks) { + if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM + && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, + "clearing windowing mode for token=%s taskInfo=%s", taskInfo.token, + taskInfo); + wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED); + } + } + } + + private void moveHomeBehindVisibleTasks(ArrayList<RunningTaskInfo> runningTasks, + WindowContainerTransaction wct) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveHomeBehindVisibleTasks"); + RunningTaskInfo homeTask = null; + ArrayList<RunningTaskInfo> visibleTasks = new ArrayList<>(); + for (RunningTaskInfo taskInfo : runningTasks) { + if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME) { + homeTask = taskInfo; + } else if (taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD + && taskInfo.isVisible()) { + visibleTasks.add(taskInfo); + } + } + if (homeTask == null) { + ProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveHomeBehindVisibleTasks: home task not found"); + } else { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveHomeBehindVisibleTasks: visible tasks %d", + visibleTasks.size()); + wct.reorder(homeTask.getToken(), true /* onTop */); + for (RunningTaskInfo task : visibleTasks) { + wct.reorder(task.getToken(), true /* onTop */); + } + } + } + + private void setDisplayAreaWindowingMode(int displayId, + @WindowConfiguration.WindowingMode int windowingMode, WindowContainerTransaction wct) { + DisplayAreaInfo displayAreaInfo = mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo( + displayId); if (displayAreaInfo == null) { ProtoLog.e(WM_SHELL_DESKTOP_MODE, "unable to update windowing mode for display %d display not found", displayId); return; } - ProtoLog.d(WM_SHELL_DESKTOP_MODE, + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "setWindowingMode: displayId=%d current wmMode=%d new wmMode=%d", displayId, displayAreaInfo.configuration.windowConfiguration.getWindowingMode(), windowingMode); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java index f4888fbb2bb9..168f6d79a390 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java @@ -98,9 +98,11 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs switch (change.getMode()) { case WindowManager.TRANSIT_OPEN: - case WindowManager.TRANSIT_TO_FRONT: onOpenTransitionReady(change, startT, finishT); break; + case WindowManager.TRANSIT_TO_FRONT: + onToFrontTransitionReady(change, startT, finishT); + break; case WindowManager.TRANSIT_CLOSE: { taskInfoList.add(change.getTaskInfo()); onCloseTransitionReady(change, startT, finishT); @@ -138,6 +140,21 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs change.getTaskInfo(), startT, finishT); } + private void onToFrontTransitionReady( + TransitionInfo.Change change, + SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT) { + boolean exists = mWindowDecorViewModel.setupWindowDecorationForTransition( + change.getTaskInfo(), + startT, + finishT); + if (!exists) { + // Window caption does not exist, create it + mWindowDecorViewModel.createWindowDecoration( + change.getTaskInfo(), change.getLeash(), startT, finishT); + } + } + @Override public void onTransitionStarting(@NonNull IBinder transition) {} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java index c52ed249c2ca..75f9a4c33af9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java @@ -42,8 +42,8 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { Consts.TAG_WM_SHELL), WM_SHELL_PICTURE_IN_PICTURE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM_SHELL), - WM_SHELL_SPLIT_SCREEN(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, - Consts.TAG_WM_SHELL), + WM_SHELL_SPLIT_SCREEN(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, + Consts.TAG_WM_SPLIT_SCREEN), WM_SHELL_SYSUI_EVENTS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM_SHELL), WM_SHELL_DESKTOP_MODE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, @@ -110,6 +110,7 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { private static class Consts { private static final String TAG_WM_SHELL = "WindowManagerShell"; private static final String TAG_WM_STARTING_WINDOW = "ShellStartingWindow"; + private static final String TAG_WM_SPLIT_SCREEN = "ShellSplitScreen"; private static final boolean ENABLE_DEBUG = true; private static final boolean ENABLE_LOG_TO_PROTO_DEBUG = true; 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 eb08d0ecbd06..5533ad56d17c 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 @@ -86,8 +86,8 @@ interface ISplitScreen { /** * Starts a pair of intent and task in one transition. */ - oneway void startIntentAndTask(in PendingIntent pendingIntent, in Intent fillInIntent, - in Bundle options1, int taskId, in Bundle options2, int sidePosition, float splitRatio, + oneway void startIntentAndTask(in PendingIntent pendingIntent, in Bundle options1, int taskId, + in Bundle options2, int sidePosition, float splitRatio, in RemoteTransition remoteTransition, in InstanceId instanceId) = 16; /** @@ -108,9 +108,8 @@ interface ISplitScreen { * Starts a pair of intent and task using legacy transition system. */ oneway void startIntentAndTaskWithLegacyTransition(in PendingIntent pendingIntent, - in Intent fillInIntent, in Bundle options1, int taskId, in Bundle options2, - int splitPosition, float splitRatio, in RemoteAnimationAdapter adapter, - in InstanceId instanceId) = 12; + in Bundle options1, int taskId, in Bundle options2, int splitPosition, float splitRatio, + in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 12; /** * Starts a pair of shortcut and task using legacy transition system. 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 c6a2b8312ebd..cdc8cdd2c28d 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 @@ -18,6 +18,7 @@ package com.android.wm.shell.splitscreen; import static android.app.ActivityManager.START_SUCCESS; import static android.app.ActivityManager.START_TASK_TO_FRONT; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION; import static android.view.Display.DEFAULT_DISPLAY; @@ -32,6 +33,8 @@ import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN; import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.ActivityTaskManager; @@ -60,13 +63,12 @@ import android.window.WindowContainerTransaction; import androidx.annotation.BinderThread; import androidx.annotation.IntDef; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.InstanceId; import com.android.internal.protolog.common.ProtoLog; import com.android.launcher3.icons.IconProvider; +import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; @@ -166,8 +168,11 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, private final IconProvider mIconProvider; private final Optional<RecentTasksController> mRecentTasksOptional; private final SplitScreenShellCommandHandler mSplitScreenShellCommandHandler; + private final String[] mMultiInstancesComponents; + + @VisibleForTesting + StageCoordinator mStageCoordinator; - private StageCoordinator mStageCoordinator; // Only used for the legacy recents animation from splitscreen to allow the tasks to be animated // outside the bounds of the roots by being reparented into a higher level fullscreen container private SurfaceControl mGoingToRecentsTasksLayer; @@ -210,6 +215,51 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, if (ActivityTaskManager.supportsSplitScreenMultiWindow(context)) { shellInit.addInitCallback(this::onInit, this); } + + // TODO(255224696): Remove the config once having a way for client apps to opt-in + // multi-instances split. + mMultiInstancesComponents = mContext.getResources() + .getStringArray(R.array.config_componentsSupportMultiInstancesSplit); + } + + @VisibleForTesting + SplitScreenController(Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + ShellController shellController, + ShellTaskOrganizer shellTaskOrganizer, + SyncTransactionQueue syncQueue, + RootTaskDisplayAreaOrganizer rootTDAOrganizer, + DisplayController displayController, + DisplayImeController displayImeController, + DisplayInsetsController displayInsetsController, + DragAndDropController dragAndDropController, + Transitions transitions, + TransactionPool transactionPool, + IconProvider iconProvider, + RecentTasksController recentTasks, + ShellExecutor mainExecutor, + StageCoordinator stageCoordinator) { + mShellCommandHandler = shellCommandHandler; + mShellController = shellController; + mTaskOrganizer = shellTaskOrganizer; + mSyncQueue = syncQueue; + mContext = context; + mRootTDAOrganizer = rootTDAOrganizer; + mMainExecutor = mainExecutor; + mDisplayController = displayController; + mDisplayImeController = displayImeController; + mDisplayInsetsController = displayInsetsController; + mDragAndDropController = dragAndDropController; + mTransitions = transitions; + mTransactionPool = transactionPool; + mIconProvider = iconProvider; + mRecentTasksOptional = Optional.of(recentTasks); + mStageCoordinator = stageCoordinator; + mSplitScreenShellCommandHandler = new SplitScreenShellCommandHandler(this); + shellInit.addInitCallback(this::onInit, this); + mMultiInstancesComponents = mContext.getResources() + .getStringArray(R.array.config_componentsSupportMultiInstancesSplit); } public SplitScreen asSplitScreen() { @@ -471,72 +521,116 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, startIntent(intent, fillInIntent, position, options); } + private void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, + @Nullable Bundle options1, int taskId, @Nullable Bundle options2, + @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter, + InstanceId instanceId) { + Intent fillInIntent = null; + if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId) + && supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) { + fillInIntent = new Intent(); + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + } + mStageCoordinator.startIntentAndTaskWithLegacyTransition(pendingIntent, fillInIntent, + options1, taskId, options2, splitPosition, splitRatio, adapter, instanceId); + } + + private void startIntentAndTask(PendingIntent pendingIntent, @Nullable Bundle options1, + int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, + float splitRatio, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { + Intent fillInIntent = null; + if (launchSameComponentAdjacently(pendingIntent, splitPosition, taskId) + && supportMultiInstancesSplit(pendingIntent.getIntent().getComponent())) { + fillInIntent = new Intent(); + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + } + mStageCoordinator.startIntentAndTask(pendingIntent, fillInIntent, options1, taskId, + options2, splitPosition, splitRatio, remoteTransition, instanceId); + } + @Override public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent, @SplitPosition int position, @Nullable Bundle options) { - if (fillInIntent == null) { - fillInIntent = new Intent(); - } - // Flag this as a no-user-action launch to prevent sending user leaving event to the - // current top activity since it's going to be put into another side of the split. This - // prevents the current top activity from going into pip mode due to user leaving event. + // Flag this as a no-user-action launch to prevent sending user leaving event to the current + // top activity since it's going to be put into another side of the split. This prevents the + // current top activity from going into pip mode due to user leaving event. + if (fillInIntent == null) fillInIntent = new Intent(); fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION); - // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of the - // split and there is no reusable background task. - if (shouldAddMultipleTaskFlag(intent.getIntent(), position)) { - final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional.isPresent() - ? mRecentTasksOptional.get().findTaskInBackground( - intent.getIntent().getComponent()) - : null; - if (taskInfo != null) { - startTask(taskInfo.taskId, position, options); + if (launchSameComponentAdjacently(intent, position, INVALID_TASK_ID)) { + final ComponentName launching = intent.getIntent().getComponent(); + if (supportMultiInstancesSplit(launching)) { + // To prevent accumulating large number of instances in the background, reuse task + // in the background with priority. + final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional + .map(recentTasks -> recentTasks.findTaskInBackground(launching)) + .orElse(null); + if (taskInfo != null) { + startTask(taskInfo.taskId, position, options); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "Start task in background"); + return; + } + + // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of + // the split and there is no reusable background task. + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); + } else if (isSplitScreenVisible()) { + mStageCoordinator.switchSplitPosition("startIntent"); return; } - fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK"); } - if (!ENABLE_SHELL_TRANSITIONS) { - mStageCoordinator.startIntentLegacy(intent, fillInIntent, position, options); - return; - } mStageCoordinator.startIntent(intent, fillInIntent, position, options); } /** Returns {@code true} if it's launching the same component on both sides of the split. */ - @VisibleForTesting - boolean shouldAddMultipleTaskFlag(@Nullable Intent startIntent, @SplitPosition int position) { - if (startIntent == null) { - return false; - } + private boolean launchSameComponentAdjacently(@Nullable PendingIntent pendingIntent, + @SplitPosition int position, int taskId) { + if (pendingIntent == null || pendingIntent.getIntent() == null) return false; + + final ComponentName launchingActivity = pendingIntent.getIntent().getComponent(); + if (launchingActivity == null) return false; - final ComponentName launchingActivity = startIntent.getComponent(); - if (launchingActivity == null) { + if (taskId != INVALID_TASK_ID) { + final ActivityManager.RunningTaskInfo taskInfo = + mTaskOrganizer.getRunningTaskInfo(taskId); + if (taskInfo != null) { + return Objects.equals(taskInfo.baseIntent.getComponent(), launchingActivity); + } return false; } - if (isSplitScreenVisible()) { - // To prevent users from constantly dropping the same app to the same side resulting in - // a large number of instances in the background. - final ActivityManager.RunningTaskInfo targetTaskInfo = getTaskInfo(position); - final ComponentName targetActivity = targetTaskInfo != null - ? targetTaskInfo.baseIntent.getComponent() : null; - if (Objects.equals(launchingActivity, targetActivity)) { - return false; + if (!isSplitScreenVisible()) { + // Split screen is not yet activated, check if the current top running task is valid to + // split together. + final ActivityManager.RunningTaskInfo taskInfo = getFocusingTaskInfo(); + if (taskInfo != null && isValidToEnterSplitScreen(taskInfo)) { + return Objects.equals(taskInfo.baseIntent.getComponent(), launchingActivity); } - - // Allow users to start a new instance the same to adjacent side. - final ActivityManager.RunningTaskInfo pairedTaskInfo = - getTaskInfo(SplitLayout.reversePosition(position)); - final ComponentName pairedActivity = pairedTaskInfo != null - ? pairedTaskInfo.baseIntent.getComponent() : null; - return Objects.equals(launchingActivity, pairedActivity); + return false; } - final ActivityManager.RunningTaskInfo taskInfo = getFocusingTaskInfo(); - if (taskInfo != null && isValidToEnterSplitScreen(taskInfo)) { - return Objects.equals(taskInfo.baseIntent.getComponent(), launchingActivity); + // Compare to the adjacent side of the split to determine if this is launching the same + // component adjacently. + final ActivityManager.RunningTaskInfo pairedTaskInfo = + getTaskInfo(SplitLayout.reversePosition(position)); + final ComponentName pairedActivity = pairedTaskInfo != null + ? pairedTaskInfo.baseIntent.getComponent() : null; + return Objects.equals(launchingActivity, pairedActivity); + } + + @VisibleForTesting + /** Returns {@code true} if the component supports multi-instances split. */ + boolean supportMultiInstancesSplit(@Nullable ComponentName launching) { + if (launching == null) return false; + + final String componentName = launching.flattenToString(); + for (int i = 0; i < mMultiInstancesComponents.length; i++) { + if (mMultiInstancesComponents[i].equals(componentName)) { + return true; + } } return false; @@ -839,14 +933,13 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, @Override public void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, - Intent fillInIntent, Bundle options1, int taskId, Bundle options2, - int splitPosition, float splitRatio, RemoteAnimationAdapter adapter, - InstanceId instanceId) { + Bundle options1, int taskId, Bundle options2, int splitPosition, float splitRatio, + RemoteAnimationAdapter adapter, InstanceId instanceId) { executeRemoteCallWithTaskPermission(mController, "startIntentAndTaskWithLegacyTransition", (controller) -> - controller.mStageCoordinator.startIntentAndTaskWithLegacyTransition( - pendingIntent, fillInIntent, options1, taskId, options2, - splitPosition, splitRatio, adapter, instanceId)); + controller.startIntentAndTaskWithLegacyTransition(pendingIntent, + options1, taskId, options2, splitPosition, splitRatio, adapter, + instanceId)); } @Override @@ -872,14 +965,13 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override - public void startIntentAndTask(PendingIntent pendingIntent, Intent fillInIntent, - @Nullable Bundle options1, int taskId, @Nullable Bundle options2, - @SplitPosition int splitPosition, float splitRatio, - @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { + public void startIntentAndTask(PendingIntent pendingIntent, @Nullable Bundle options1, + int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, + float splitRatio, @Nullable RemoteTransition remoteTransition, + InstanceId instanceId) { executeRemoteCallWithTaskPermission(mController, "startIntentAndTask", - (controller) -> controller.mStageCoordinator.startIntentAndTask(pendingIntent, - fillInIntent, options1, taskId, options2, splitPosition, splitRatio, - remoteTransition, instanceId)); + (controller) -> controller.startIntentAndTask(pendingIntent, options1, taskId, + options2, splitPosition, splitRatio, remoteTransition, instanceId)); } @Override 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 15a11334307b..c2ab7ef7e7bf 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 @@ -24,7 +24,6 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.RemoteAnimationTarget.MODE_OPENING; @@ -428,6 +427,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, /** Launches an activity into split. */ void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position, @Nullable Bundle options) { + if (!ENABLE_SHELL_TRANSITIONS) { + startIntentLegacy(intent, fillInIntent, position, options); + return; + } + final WindowContainerTransaction wct = new WindowContainerTransaction(); final WindowContainerTransaction evictWct = new WindowContainerTransaction(); prepareEvictChildTasks(position, evictWct); @@ -441,13 +445,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, prepareEnterSplitScreen(wct, null /* taskInfo */, position); mSplitTransitions.startEnterTransition(transitType, wct, null, this, - aborted -> { - // Switch the split position if launching as MULTIPLE_TASK failed. - if (aborted && (fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) { - setSideStagePositionAnimated( - SplitLayout.reversePosition(mSideStagePosition)); - } - } /* consumedCallback */, + null /* consumedCallback */, (finishWct, finishT) -> { if (!evictWct.isEmpty()) { finishWct.merge(evictWct, true); @@ -457,7 +455,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, /** Launches an activity into split by legacy transition. */ void startIntentLegacy(PendingIntent intent, Intent fillInIntent, - @SplitPosition int position, @androidx.annotation.Nullable Bundle options) { + @SplitPosition int position, @Nullable Bundle options) { final WindowContainerTransaction evictWct = new WindowContainerTransaction(); prepareEvictChildTasks(position, evictWct); @@ -473,12 +471,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, exitSplitScreen(mMainStage.getChildCount() == 0 ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN)); mSplitUnsupportedToast.show(); - } else { - // Switch the split position if launching as MULTIPLE_TASK failed. - if ((fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) { - setSideStagePosition(SplitLayout.reversePosition( - getSideStagePosition()), null); - } } // Do nothing when the animation was cancelled. @@ -771,9 +763,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSideStage.evictInvisibleChildren(wct); } - Bundle resolveStartStage(@StageType int stage, - @SplitPosition int position, @androidx.annotation.Nullable Bundle options, - @androidx.annotation.Nullable WindowContainerTransaction wct) { + Bundle resolveStartStage(@StageType int stage, @SplitPosition int position, + @Nullable Bundle options, @Nullable WindowContainerTransaction wct) { switch (stage) { case STAGE_TYPE_UNDEFINED: { if (position != SPLIT_POSITION_UNDEFINED) { @@ -844,9 +835,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, : mMainStage.getTopVisibleChildTaskId(); } - void setSideStagePositionAnimated(@SplitPosition int sideStagePosition) { - if (mSideStagePosition == sideStagePosition) return; - SurfaceControl.Transaction t = mTransactionPool.acquire(); + void switchSplitPosition(String reason) { + final SurfaceControl.Transaction t = mTransactionPool.acquire(); mTempRect1.setEmpty(); final StageTaskListener topLeftStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; @@ -886,6 +876,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, va.start(); }); }); + + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Switch split position: %s", reason); + mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(), + getSideStagePosition(), mSideStage.getTopChildTaskUid(), + mSplitLayout.isLandscape()); } void setSideStagePosition(@SplitPosition int sideStagePosition, @@ -1097,7 +1092,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, activityTaskManagerService.setFocusedTask(getTaskId(stageToFocus)); } catch (RemoteException | NullPointerException e) { ProtoLog.e(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, - "%s: Unable to update focus on the chosen stage, %s", TAG, e); + "Unable to update focus on the chosen stage: %s", e.getMessage()); } } @@ -1434,14 +1429,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, - "%s: Request to %s divider bar from %s.", TAG, + "Request to %s divider bar from %s.", (visible ? "show" : "hide"), Debug.getCaller()); // Defer showing divider bar after keyguard dismissed, so it won't interfere with keyguard // dismissing animation. if (visible && mKeyguardShowing) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, - "%s: Defer showing divider bar due to keyguard showing.", TAG); + " Defer showing divider bar due to keyguard showing."); return; } @@ -1450,7 +1445,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (mIsDividerRemoteAnimating) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, - "%s: Skip animating divider bar due to it's remote animating.", TAG); + " Skip animating divider bar due to it's remote animating."); return; } @@ -1465,12 +1460,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); if (dividerLeash == null) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, - "%s: Skip animating divider bar due to divider leash not ready.", TAG); + " Skip animating divider bar due to divider leash not ready."); return; } if (mIsDividerRemoteAnimating) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, - "%s: Skip animating divider bar due to it's remote animating.", TAG); + " Skip animating divider bar due to it's remote animating."); return; } @@ -1617,10 +1612,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Override public void onDoubleTappedDivider() { - setSideStagePositionAnimated(SplitLayout.reversePosition(mSideStagePosition)); - mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(), - getSideStagePosition(), mSideStage.getTopChildTaskUid(), - mSplitLayout.isLandscape()); + switchSplitPosition("double tap"); } @Override @@ -1633,14 +1625,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } @Override - public void onLayoutSizeChanging(SplitLayout layout) { + public void onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY) { final SurfaceControl.Transaction t = mTransactionPool.acquire(); t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); updateSurfaceBounds(layout, t, true /* applyResizingOffset */); getMainStageBounds(mTempRect1); getSideStageBounds(mTempRect2); - mMainStage.onResizing(mTempRect1, mTempRect2, t); - mSideStage.onResizing(mTempRect2, mTempRect1, t); + mMainStage.onResizing(mTempRect1, mTempRect2, t, offsetX, offsetY); + mSideStage.onResizing(mTempRect2, mTempRect1, t, offsetX, offsetY); t.apply(); mTransactionPool.release(t); } 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 6b90eabe3bd2..acad5d93eab4 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 @@ -288,9 +288,11 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { } } - void onResizing(Rect newBounds, Rect sideBounds, SurfaceControl.Transaction t) { + void onResizing(Rect newBounds, Rect sideBounds, SurfaceControl.Transaction t, int offsetX, + int offsetY) { if (mSplitDecorManager != null && mRootTaskInfo != null) { - mSplitDecorManager.onResizing(mRootTaskInfo, newBounds, sideBounds, t); + mSplitDecorManager.onResizing(mRootTaskInfo, newBounds, sideBounds, t, offsetX, + offsetY); } } 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 2830fa967011..857decf65567 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 @@ -24,7 +24,6 @@ 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.fixScale; -import static android.window.TransitionInfo.FLAG_IS_INPUT_METHOD; import static android.window.TransitionInfo.FLAG_IS_OCCLUDED; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; @@ -333,9 +332,12 @@ public class Transitions implements RemoteCallable<Transitions> { boolean isOpening = isOpeningType(info.getType()); for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); - if ((change.getFlags() & TransitionInfo.FLAG_IS_SYSTEM_WINDOW) != 0) { + if (change.hasFlags(TransitionInfo.FLAGS_IS_NON_APP_WINDOW)) { // Currently system windows are controlled by WindowState, so don't change their - // surfaces. Otherwise their window tokens could be hidden unexpectedly. + // surfaces. Otherwise their surfaces could be hidden or cropped unexpectedly. + // This includes Wallpaper (always z-ordered at bottom) and IME (associated with + // app), because there may not be a transition associated with their visibility + // changes, and currently they don't need transition animation. continue; } final SurfaceControl leash = change.getLeash(); @@ -372,16 +374,7 @@ public class Transitions implements RemoteCallable<Transitions> { finishT.setAlpha(leash, 1.f); } } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) { - // Wallpaper/IME are anomalies: their visibility is tied to other WindowStates. - // As a result, we actually can't hide their WindowTokens because there may not be a - // transition associated with them becoming visible again. Fortunately, since - // wallpapers are always z-ordered to the back, we don't have to worry about it - // flickering to the front during reparenting. Similarly, the IME is reparented to - // the associated app, so its visibility is coupled. So, an explicit hide is not - // needed visually anyways. - if ((change.getFlags() & (FLAG_IS_WALLPAPER | FLAG_IS_INPUT_METHOD)) == 0) { - finishT.hide(leash); - } + finishT.hide(leash); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index 36dd8edaa8b7..ca15f0002fac 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -105,6 +105,11 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { if (!shouldShowWindowDecor(taskInfo)) return false; + CaptionWindowDecoration oldDecoration = mWindowDecorByTaskId.get(taskInfo.taskId); + if (oldDecoration != null) { + // close the old decoration if it exists to avoid two window decorations being added + oldDecoration.close(); + } final CaptionWindowDecoration windowDecoration = new CaptionWindowDecoration( mContext, mDisplayController, @@ -141,23 +146,25 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } @Override - public void setupWindowDecorationForTransition( + public boolean setupWindowDecorationForTransition( RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); - if (decoration == null) return; + if (decoration == null) return false; decoration.relayout(taskInfo, startT, finishT); + return true; } @Override - public void destroyWindowDecoration(RunningTaskInfo taskInfo) { + public boolean destroyWindowDecoration(RunningTaskInfo taskInfo) { final CaptionWindowDecoration decoration = mWindowDecorByTaskId.removeReturnOld(taskInfo.taskId); - if (decoration == null) return; + if (decoration == null) return false; decoration.close(); + return true; } private class CaptionTouchEventListener implements diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java index f0f2db7ded80..a49a300995e6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java @@ -40,6 +40,9 @@ class TaskPositioner implements DragResizeCallback { private final Rect mTaskBoundsAtDragStart = new Rect(); private final PointF mResizeStartPoint = new PointF(); private final Rect mResizeTaskBounds = new Rect(); + // Whether the |dragResizing| hint should be sent with the next bounds change WCT. + // Used to optimized fluid resizing of freeform tasks. + private boolean mPendingDragResizeHint = false; private int mCtrlType; private DragStartListener mDragStartListener; @@ -53,6 +56,12 @@ class TaskPositioner implements DragResizeCallback { @Override public void onDragResizeStart(int ctrlType, float x, float y) { + if (ctrlType != CTRL_TYPE_UNDEFINED) { + // The task is being resized, send the |dragResizing| hint to core with the first + // bounds-change wct. + mPendingDragResizeHint = true; + } + mDragStartListener.onDragStart(mWindowDecoration.mTaskInfo.taskId); mCtrlType = ctrlType; @@ -63,19 +72,31 @@ class TaskPositioner implements DragResizeCallback { @Override public void onDragResizeMove(float x, float y) { - changeBounds(x, y); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + if (changeBounds(wct, x, y)) { + if (mPendingDragResizeHint) { + // This is the first bounds change since drag resize operation started. + wct.setDragResizing(mWindowDecoration.mTaskInfo.token, true /* dragResizing */); + mPendingDragResizeHint = false; + } + mTaskOrganizer.applyTransaction(wct); + } } @Override public void onDragResizeEnd(float x, float y) { - changeBounds(x, y); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setDragResizing(mWindowDecoration.mTaskInfo.token, false /* dragResizing */); + changeBounds(wct, x, y); + mTaskOrganizer.applyTransaction(wct); mCtrlType = 0; mTaskBoundsAtDragStart.setEmpty(); mResizeStartPoint.set(0, 0); + mPendingDragResizeHint = false; } - private void changeBounds(float x, float y) { + private boolean changeBounds(WindowContainerTransaction wct, float x, float y) { float deltaX = x - mResizeStartPoint.x; mResizeTaskBounds.set(mTaskBoundsAtDragStart); if ((mCtrlType & CTRL_TYPE_LEFT) != 0) { @@ -96,10 +117,10 @@ class TaskPositioner implements DragResizeCallback { } if (!mResizeTaskBounds.isEmpty()) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.setBounds(mWindowDecoration.mTaskInfo.token, mResizeTaskBounds); - mTaskOrganizer.applyTransaction(wct); + return true; } + return false; } interface DragStartListener { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java index d7f71c8235f1..2ce4d04377a1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java @@ -44,7 +44,7 @@ public interface WindowDecorViewModel { * @param taskSurface the surface of the task * @param startT the start transaction to be applied before the transition * @param finishT the finish transaction to restore states after the transition - * @return the window decoration object + * @return {@code true} if window decoration was created, {@code false} otherwise */ boolean createWindowDecoration( ActivityManager.RunningTaskInfo taskInfo, @@ -66,8 +66,9 @@ public interface WindowDecorViewModel { * * @param startT the start transaction to be applied before the transition * @param finishT the finish transaction to restore states after the transition + * @return {@code true} if window decoration exists, {@code false} otherwise */ - void setupWindowDecorationForTransition( + boolean setupWindowDecorationForTransition( ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT); @@ -76,6 +77,7 @@ public interface WindowDecorViewModel { * Destroys the window decoration of the give task. * * @param taskInfo the info of the task + * @return {@code true} if window decoration was destroyed, {@code false} otherwise */ - void destroyWindowDecoration(ActivityManager.RunningTaskInfo taskInfo); + boolean destroyWindowDecoration(ActivityManager.RunningTaskInfo taskInfo); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java index 7cbace5af48f..081c8ae91bdb 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java @@ -16,13 +16,9 @@ package com.android.wm.shell; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; -import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; -import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -34,8 +30,6 @@ import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIO import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeFalse; @@ -44,11 +38,9 @@ import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import android.app.ActivityManager.RunningTaskInfo; import android.app.TaskInfo; -import android.app.WindowConfiguration; import android.content.LocusId; import android.content.pm.ParceledListSlice; import android.os.Binder; @@ -61,8 +53,6 @@ import android.window.ITaskOrganizer; import android.window.ITaskOrganizerController; import android.window.TaskAppearedInfo; import android.window.WindowContainerToken; -import android.window.WindowContainerTransaction; -import android.window.WindowContainerTransaction.Change; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -638,130 +628,10 @@ public class ShellTaskOrganizerTests extends ShellTestCase { verify(mTaskOrganizerController).restartTaskTopActivityProcessIfVisible(task1.token); } - @Test - public void testPrepareClearBoundsForStandardTasks() { - MockToken token1 = new MockToken(); - RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_UNDEFINED, token1); - mOrganizer.onTaskAppeared(task1, null); - - MockToken token2 = new MockToken(); - RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_UNDEFINED, token2); - mOrganizer.onTaskAppeared(task2, null); - - MockToken otherDisplayToken = new MockToken(); - RunningTaskInfo otherDisplayTask = createTaskInfo(3, WINDOWING_MODE_UNDEFINED, - otherDisplayToken); - otherDisplayTask.displayId = 2; - mOrganizer.onTaskAppeared(otherDisplayTask, null); - - WindowContainerTransaction wct = mOrganizer.prepareClearBoundsForStandardTasks(1); - - assertEquals(wct.getChanges().size(), 2); - Change boundsChange1 = wct.getChanges().get(token1.binder()); - assertNotNull(boundsChange1); - assertNotEquals( - (boundsChange1.getWindowSetMask() & WindowConfiguration.WINDOW_CONFIG_BOUNDS), 0); - assertTrue(boundsChange1.getConfiguration().windowConfiguration.getBounds().isEmpty()); - - Change boundsChange2 = wct.getChanges().get(token2.binder()); - assertNotNull(boundsChange2); - assertNotEquals( - (boundsChange2.getWindowSetMask() & WindowConfiguration.WINDOW_CONFIG_BOUNDS), 0); - assertTrue(boundsChange2.getConfiguration().windowConfiguration.getBounds().isEmpty()); - } - - @Test - public void testPrepareClearBoundsForStandardTasks_onlyClearActivityTypeStandard() { - MockToken token1 = new MockToken(); - RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_UNDEFINED, token1); - mOrganizer.onTaskAppeared(task1, null); - - MockToken token2 = new MockToken(); - RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_UNDEFINED, token2); - task2.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_HOME); - mOrganizer.onTaskAppeared(task2, null); - - WindowContainerTransaction wct = mOrganizer.prepareClearBoundsForStandardTasks(1); - - // Only clear bounds for task1 - assertEquals(1, wct.getChanges().size()); - assertNotNull(wct.getChanges().get(token1.binder())); - } - - @Test - public void testPrepareClearFreeformForStandardTasks() { - MockToken token1 = new MockToken(); - RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FREEFORM, token1); - mOrganizer.onTaskAppeared(task1, null); - - MockToken token2 = new MockToken(); - RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW, token2); - mOrganizer.onTaskAppeared(task2, null); - - MockToken otherDisplayToken = new MockToken(); - RunningTaskInfo otherDisplayTask = createTaskInfo(3, WINDOWING_MODE_FREEFORM, - otherDisplayToken); - otherDisplayTask.displayId = 2; - mOrganizer.onTaskAppeared(otherDisplayTask, null); - - WindowContainerTransaction wct = mOrganizer.prepareClearFreeformForStandardTasks(1); - - // Only task with freeform windowing mode and the right display should be updated - assertEquals(wct.getChanges().size(), 1); - Change wmModeChange1 = wct.getChanges().get(token1.binder()); - assertNotNull(wmModeChange1); - assertEquals(wmModeChange1.getWindowingMode(), WINDOWING_MODE_UNDEFINED); - } - - @Test - public void testPrepareClearFreeformForStandardTasks_onlyClearActivityTypeStandard() { - MockToken token1 = new MockToken(); - RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FREEFORM, token1); - mOrganizer.onTaskAppeared(task1, null); - - MockToken token2 = new MockToken(); - RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_FREEFORM, token2); - task2.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_HOME); - mOrganizer.onTaskAppeared(task2, null); - - WindowContainerTransaction wct = mOrganizer.prepareClearFreeformForStandardTasks(1); - - // Only clear freeform for task1 - assertEquals(1, wct.getChanges().size()); - assertNotNull(wct.getChanges().get(token1.binder())); - } - private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode) { RunningTaskInfo taskInfo = new RunningTaskInfo(); taskInfo.taskId = taskId; taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); return taskInfo; } - - private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode, MockToken token) { - RunningTaskInfo taskInfo = createTaskInfo(taskId, windowingMode); - taskInfo.displayId = 1; - taskInfo.token = token.token(); - taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD); - return taskInfo; - } - - private static class MockToken { - private final WindowContainerToken mToken; - private final IBinder mBinder; - - MockToken() { - mToken = mock(WindowContainerToken.class); - mBinder = mock(IBinder.class); - when(mToken.asBinder()).thenReturn(mBinder); - } - - WindowContainerToken token() { - return mToken; - } - - IBinder binder() { - return mBinder; - } - } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/OWNERS b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/OWNERS new file mode 100644 index 000000000000..1e0f9bc6322f --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/OWNERS @@ -0,0 +1,5 @@ +# WM shell sub-module back navigation owners +# Bug component: 1152663 +shanh@google.com +arthurhung@google.com +wilsonshih@google.com diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java index 5332476d5130..3d779481d361 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java @@ -105,7 +105,8 @@ public class SplitLayoutTests extends ShellTestCase { @Test public void testUpdateDivideBounds() { mSplitLayout.updateDivideBounds(anyInt()); - verify(mSplitLayoutHandler).onLayoutSizeChanging(any(SplitLayout.class)); + verify(mSplitLayoutHandler).onLayoutSizeChanging(any(SplitLayout.class), anyInt(), + anyInt()); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java index 79b520c734c8..89bafcb6b2f4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java @@ -16,10 +16,13 @@ package com.android.wm.shell.desktopmode; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS; +import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER; @@ -30,13 +33,14 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.app.ActivityManager; +import android.app.ActivityManager.RunningTaskInfo; import android.os.Binder; import android.os.Handler; import android.os.IBinder; @@ -68,6 +72,9 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Mockito; +import java.util.ArrayList; +import java.util.Arrays; + @SmallTest @RunWith(AndroidTestingRunner.class) public class DesktopModeControllerTest extends ShellTestCase { @@ -83,9 +90,7 @@ public class DesktopModeControllerTest extends ShellTestCase { @Mock private Handler mMockHandler; @Mock - private Transitions mMockTransitions; - private TestShellExecutor mExecutor; - + private Transitions mTransitions; private DesktopModeController mController; private DesktopModeTaskRepository mDesktopModeTaskRepository; private ShellInit mShellInit; @@ -97,20 +102,19 @@ public class DesktopModeControllerTest extends ShellTestCase { when(DesktopModeStatus.isActive(any())).thenReturn(true); mShellInit = Mockito.spy(new ShellInit(mTestExecutor)); - mExecutor = new TestShellExecutor(); mDesktopModeTaskRepository = new DesktopModeTaskRepository(); mController = new DesktopModeController(mContext, mShellInit, mShellController, - mShellTaskOrganizer, mRootTaskDisplayAreaOrganizer, mMockTransitions, - mDesktopModeTaskRepository, mMockHandler, mExecutor); + mShellTaskOrganizer, mRootTaskDisplayAreaOrganizer, mTransitions, + mDesktopModeTaskRepository, mMockHandler, new TestShellExecutor()); - when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks(anyInt())).thenReturn( - new WindowContainerTransaction()); + when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>()); mShellInit.init(); clearInvocations(mShellTaskOrganizer); clearInvocations(mRootTaskDisplayAreaOrganizer); + clearInvocations(mTransitions); } @After @@ -124,113 +128,133 @@ public class DesktopModeControllerTest extends ShellTestCase { } @Test - public void testDesktopModeEnabled_taskWmClearedDisplaySetToFreeform() { - // Create a fake WCT to simulate setting task windowing mode to undefined - WindowContainerTransaction taskWct = new WindowContainerTransaction(); - MockToken taskMockToken = new MockToken(); - taskWct.setWindowingMode(taskMockToken.token(), WINDOWING_MODE_UNDEFINED); - when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks( - mContext.getDisplayId())).thenReturn(taskWct); - - // Create a fake DisplayAreaInfo to check if windowing mode change is set correctly - MockToken displayMockToken = new MockToken(); - DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(displayMockToken.mToken, - mContext.getDisplayId(), 0); - when(mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(mContext.getDisplayId())) - .thenReturn(displayAreaInfo); + public void testDesktopModeEnabled_rootTdaSetToFreeform() { + DisplayAreaInfo displayAreaInfo = createMockDisplayArea(); - // The test mController.updateDesktopModeActive(true); + WindowContainerTransaction wct = getDesktopModeSwitchTransaction(); + + // 1 change: Root TDA windowing mode + assertThat(wct.getChanges().size()).isEqualTo(1); + // Verify WCT has a change for setting windowing mode to freeform + Change change = wct.getChanges().get(displayAreaInfo.token.asBinder()); + assertThat(change).isNotNull(); + assertThat(change.getWindowingMode()).isEqualTo(WINDOWING_MODE_FREEFORM); + } - ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass( - WindowContainerTransaction.class); - verify(mRootTaskDisplayAreaOrganizer).applyTransaction(arg.capture()); + @Test + public void testDesktopModeDisabled_rootTdaSetToFullscreen() { + DisplayAreaInfo displayAreaInfo = createMockDisplayArea(); - // WCT should have 2 changes - clear task wm mode and set display wm mode - WindowContainerTransaction wct = arg.getValue(); - assertThat(wct.getChanges()).hasSize(2); + mController.updateDesktopModeActive(false); + WindowContainerTransaction wct = getDesktopModeSwitchTransaction(); + + // 1 change: Root TDA windowing mode + assertThat(wct.getChanges().size()).isEqualTo(1); + // Verify WCT has a change for setting windowing mode to fullscreen + Change change = wct.getChanges().get(displayAreaInfo.token.asBinder()); + assertThat(change).isNotNull(); + assertThat(change.getWindowingMode()).isEqualTo(WINDOWING_MODE_FULLSCREEN); + } - // Verify executed WCT has a change for setting task windowing mode to undefined - Change taskWmModeChange = wct.getChanges().get(taskMockToken.binder()); - assertThat(taskWmModeChange).isNotNull(); - assertThat(taskWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED); + @Test + public void testDesktopModeEnabled_windowingModeCleared() { + createMockDisplayArea(); + RunningTaskInfo freeformTask = createFreeformTask(); + RunningTaskInfo fullscreenTask = createFullscreenTask(); + RunningTaskInfo homeTask = createHomeTask(); + when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>( + Arrays.asList(freeformTask, fullscreenTask, homeTask))); - // Verify executed WCT has a change for setting display windowing mode to freeform - Change displayWmModeChange = wct.getChanges().get(displayAreaInfo.token.asBinder()); - assertThat(displayWmModeChange).isNotNull(); - assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FREEFORM); + mController.updateDesktopModeActive(true); + WindowContainerTransaction wct = getDesktopModeSwitchTransaction(); + + // 2 changes: Root TDA windowing mode and 1 task + assertThat(wct.getChanges().size()).isEqualTo(2); + // No changes for tasks that are not standard or freeform + assertThat(wct.getChanges().get(fullscreenTask.token.asBinder())).isNull(); + assertThat(wct.getChanges().get(homeTask.token.asBinder())).isNull(); + // Standard freeform task has windowing mode cleared + Change change = wct.getChanges().get(freeformTask.token.asBinder()); + assertThat(change).isNotNull(); + assertThat(change.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED); } @Test - public void testDesktopModeDisabled_taskWmAndBoundsClearedDisplaySetToFullscreen() { - // Create a fake WCT to simulate setting task windowing mode to undefined - WindowContainerTransaction taskWmWct = new WindowContainerTransaction(); - MockToken taskWmMockToken = new MockToken(); - taskWmWct.setWindowingMode(taskWmMockToken.token(), WINDOWING_MODE_UNDEFINED); - when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks( - mContext.getDisplayId())).thenReturn(taskWmWct); - - // Create a fake WCT to simulate clearing task bounds - WindowContainerTransaction taskBoundsWct = new WindowContainerTransaction(); - MockToken taskBoundsMockToken = new MockToken(); - taskBoundsWct.setBounds(taskBoundsMockToken.token(), null); - when(mShellTaskOrganizer.prepareClearBoundsForStandardTasks( - mContext.getDisplayId())).thenReturn(taskBoundsWct); - - // Create a fake DisplayAreaInfo to check if windowing mode change is set correctly - MockToken displayMockToken = new MockToken(); - DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(displayMockToken.mToken, - mContext.getDisplayId(), 0); - when(mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(mContext.getDisplayId())) - .thenReturn(displayAreaInfo); + public void testDesktopModeDisabled_windowingModeAndBoundsCleared() { + createMockDisplayArea(); + RunningTaskInfo freeformTask = createFreeformTask(); + RunningTaskInfo fullscreenTask = createFullscreenTask(); + RunningTaskInfo homeTask = createHomeTask(); + when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>( + Arrays.asList(freeformTask, fullscreenTask, homeTask))); - // The test mController.updateDesktopModeActive(false); + WindowContainerTransaction wct = getDesktopModeSwitchTransaction(); + + // 3 changes: Root TDA windowing mode and 2 tasks + assertThat(wct.getChanges().size()).isEqualTo(3); + // No changes to home task + assertThat(wct.getChanges().get(homeTask.token.asBinder())).isNull(); + // Standard tasks have bounds cleared + assertThatBoundsCleared(wct.getChanges().get(freeformTask.token.asBinder())); + assertThatBoundsCleared(wct.getChanges().get(fullscreenTask.token.asBinder())); + // Freeform standard tasks have windowing mode cleared + assertThat(wct.getChanges().get( + freeformTask.token.asBinder()).getWindowingMode()).isEqualTo( + WINDOWING_MODE_UNDEFINED); + } - ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass( - WindowContainerTransaction.class); - verify(mRootTaskDisplayAreaOrganizer).applyTransaction(arg.capture()); - - // WCT should have 3 changes - clear task wm mode and bounds and set display wm mode - WindowContainerTransaction wct = arg.getValue(); - assertThat(wct.getChanges()).hasSize(3); - - // Verify executed WCT has a change for setting task windowing mode to undefined - Change taskWmMode = wct.getChanges().get(taskWmMockToken.binder()); - assertThat(taskWmMode).isNotNull(); - assertThat(taskWmMode.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED); - - // Verify executed WCT has a change for clearing task bounds - Change bounds = wct.getChanges().get(taskBoundsMockToken.binder()); - assertThat(bounds).isNotNull(); - assertThat(bounds.getWindowSetMask() & WINDOW_CONFIG_BOUNDS).isNotEqualTo(0); - assertThat(bounds.getConfiguration().windowConfiguration.getBounds().isEmpty()).isTrue(); - - // Verify executed WCT has a change for setting display windowing mode to fullscreen - Change displayWmModeChange = wct.getChanges().get(displayAreaInfo.token.asBinder()); - assertThat(displayWmModeChange).isNotNull(); - assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FULLSCREEN); + @Test + public void testDesktopModeEnabled_homeTaskBehindVisibleTask() { + createMockDisplayArea(); + RunningTaskInfo fullscreenTask1 = createFullscreenTask(); + fullscreenTask1.isVisible = true; + RunningTaskInfo fullscreenTask2 = createFullscreenTask(); + fullscreenTask2.isVisible = false; + RunningTaskInfo homeTask = createHomeTask(); + when(mShellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(new ArrayList<>( + Arrays.asList(fullscreenTask1, fullscreenTask2, homeTask))); + + mController.updateDesktopModeActive(true); + WindowContainerTransaction wct = getDesktopModeSwitchTransaction(); + + // Check that there are hierarchy changes for home task and visible task + assertThat(wct.getHierarchyOps()).hasSize(2); + // First show home task + WindowContainerTransaction.HierarchyOp op1 = wct.getHierarchyOps().get(0); + assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); + assertThat(op1.getContainer()).isEqualTo(homeTask.token.asBinder()); + + // Then visible task on top of it + WindowContainerTransaction.HierarchyOp op2 = wct.getHierarchyOps().get(1); + assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); + assertThat(op2.getContainer()).isEqualTo(fullscreenTask1.token.asBinder()); } @Test public void testShowDesktopApps() { // Set up two active tasks on desktop - mDesktopModeTaskRepository.addActiveTask(1); - mDesktopModeTaskRepository.addActiveTask(2); - MockToken token1 = new MockToken(); - MockToken token2 = new MockToken(); - ActivityManager.RunningTaskInfo taskInfo1 = new TestRunningTaskInfoBuilder().setToken( - token1.token()).setLastActiveTime(100).build(); - ActivityManager.RunningTaskInfo taskInfo2 = new TestRunningTaskInfoBuilder().setToken( - token2.token()).setLastActiveTime(200).build(); - when(mShellTaskOrganizer.getRunningTaskInfo(1)).thenReturn(taskInfo1); - when(mShellTaskOrganizer.getRunningTaskInfo(2)).thenReturn(taskInfo2); + RunningTaskInfo freeformTask1 = createFreeformTask(); + freeformTask1.lastActiveTime = 100; + RunningTaskInfo freeformTask2 = createFreeformTask(); + freeformTask2.lastActiveTime = 200; + mDesktopModeTaskRepository.addActiveTask(freeformTask1.taskId); + mDesktopModeTaskRepository.addActiveTask(freeformTask2.taskId); + when(mShellTaskOrganizer.getRunningTaskInfo(freeformTask1.taskId)).thenReturn( + freeformTask1); + when(mShellTaskOrganizer.getRunningTaskInfo(freeformTask2.taskId)).thenReturn( + freeformTask2); // Run show desktop apps logic mController.showDesktopApps(); ArgumentCaptor<WindowContainerTransaction> wctCaptor = ArgumentCaptor.forClass( WindowContainerTransaction.class); - verify(mShellTaskOrganizer).applyTransaction(wctCaptor.capture()); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + verify(mTransitions).startTransition(eq(TRANSIT_TO_FRONT), wctCaptor.capture(), any()); + } else { + verify(mShellTaskOrganizer).applyTransaction(wctCaptor.capture()); + } WindowContainerTransaction wct = wctCaptor.getValue(); // Check wct has reorder calls @@ -239,12 +263,12 @@ public class DesktopModeControllerTest extends ShellTestCase { // Task 2 has activity later, must be first WindowContainerTransaction.HierarchyOp op1 = wct.getHierarchyOps().get(0); assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); - assertThat(op1.getContainer()).isEqualTo(token2.binder()); + assertThat(op1.getContainer()).isEqualTo(freeformTask2.token.asBinder()); // Task 1 should be second - WindowContainerTransaction.HierarchyOp op2 = wct.getHierarchyOps().get(0); + WindowContainerTransaction.HierarchyOp op2 = wct.getHierarchyOps().get(1); assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER); - assertThat(op2.getContainer()).isEqualTo(token2.binder()); + assertThat(op2.getContainer()).isEqualTo(freeformTask1.token.asBinder()); } @Test @@ -266,7 +290,7 @@ public class DesktopModeControllerTest extends ShellTestCase { @Test public void testHandleTransitionRequest_notFreeform_returnsNull() { - ActivityManager.RunningTaskInfo trigger = new ActivityManager.RunningTaskInfo(); + RunningTaskInfo trigger = new RunningTaskInfo(); trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); WindowContainerTransaction wct = mController.handleRequest( new Binder(), @@ -276,7 +300,7 @@ public class DesktopModeControllerTest extends ShellTestCase { @Test public void testHandleTransitionRequest_returnsWct() { - ActivityManager.RunningTaskInfo trigger = new ActivityManager.RunningTaskInfo(); + RunningTaskInfo trigger = new RunningTaskInfo(); trigger.token = new MockToken().mToken; trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); WindowContainerTransaction wct = mController.handleRequest( @@ -285,6 +309,57 @@ public class DesktopModeControllerTest extends ShellTestCase { assertThat(wct).isNotNull(); } + private DisplayAreaInfo createMockDisplayArea() { + DisplayAreaInfo displayAreaInfo = new DisplayAreaInfo(new MockToken().mToken, + mContext.getDisplayId(), 0); + when(mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(mContext.getDisplayId())) + .thenReturn(displayAreaInfo); + return displayAreaInfo; + } + + private RunningTaskInfo createFreeformTask() { + return new TestRunningTaskInfoBuilder() + .setToken(new MockToken().token()) + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setWindowingMode(WINDOWING_MODE_FREEFORM) + .setLastActiveTime(100) + .build(); + } + + private RunningTaskInfo createFullscreenTask() { + return new TestRunningTaskInfoBuilder() + .setToken(new MockToken().token()) + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .setLastActiveTime(100) + .build(); + } + + private RunningTaskInfo createHomeTask() { + return new TestRunningTaskInfoBuilder() + .setToken(new MockToken().token()) + .setActivityType(ACTIVITY_TYPE_HOME) + .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .setLastActiveTime(100) + .build(); + } + + private WindowContainerTransaction getDesktopModeSwitchTransaction() { + ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass( + WindowContainerTransaction.class); + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + verify(mTransitions).startTransition(eq(TRANSIT_CHANGE), arg.capture(), any()); + } else { + verify(mRootTaskDisplayAreaOrganizer).applyTransaction(arg.capture()); + } + return arg.getValue(); + } + + private void assertThatBoundsCleared(Change change) { + assertThat((change.getWindowSetMask() & WINDOW_CONFIG_BOUNDS) != 0).isTrue(); + assertThat(change.getConfiguration().windowConfiguration.getBounds().isEmpty()).isTrue(); + } + private static class MockToken { private final WindowContainerToken mToken; private final IBinder mBinder; @@ -298,9 +373,5 @@ public class DesktopModeControllerTest extends ShellTestCase { WindowContainerToken token() { return mToken; } - - IBinder binder() { - return mBinder; - } } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java index d01f3d310fc3..38b75f81171f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java @@ -16,18 +16,24 @@ package com.android.wm.shell.splitscreen; +import static android.app.PendingIntent.FLAG_IMMUTABLE; 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.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; +import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -35,6 +41,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.PendingIntent; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; @@ -65,11 +73,11 @@ import com.android.wm.shell.transition.Transitions; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.util.Optional; - /** * Tests for {@link SplitScreenController} */ @@ -91,18 +99,21 @@ public class SplitScreenControllerTests extends ShellTestCase { @Mock Transitions mTransitions; @Mock TransactionPool mTransactionPool; @Mock IconProvider mIconProvider; - @Mock Optional<RecentTasksController> mRecentTasks; + @Mock StageCoordinator mStageCoordinator; + @Mock RecentTasksController mRecentTasks; + @Captor ArgumentCaptor<Intent> mIntentCaptor; private SplitScreenController mSplitScreenController; @Before public void setup() { + assumeTrue(ActivityTaskManager.supportsSplitScreenMultiWindow(mContext)); MockitoAnnotations.initMocks(this); mSplitScreenController = spy(new SplitScreenController(mContext, mShellInit, mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue, mRootTDAOrganizer, mDisplayController, mDisplayImeController, mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool, - mIconProvider, mRecentTasks, mMainExecutor)); + mIconProvider, mRecentTasks, mMainExecutor, mStageCoordinator)); } @Test @@ -148,58 +159,100 @@ public class SplitScreenControllerTests extends ShellTestCase { } @Test - public void testShouldAddMultipleTaskFlag_notInSplitScreen() { - doReturn(false).when(mSplitScreenController).isSplitScreenVisible(); - doReturn(true).when(mSplitScreenController).isValidToEnterSplitScreen(any()); + public void testStartIntent_appendsNoUserActionFlag() { + Intent startIntent = createStartIntent("startActivity"); + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); - // Verify launching the same activity returns true. + mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null); + + verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(), + eq(SPLIT_POSITION_TOP_OR_LEFT), isNull()); + assertEquals(FLAG_ACTIVITY_NO_USER_ACTION, + mIntentCaptor.getValue().getFlags() & FLAG_ACTIVITY_NO_USER_ACTION); + } + + @Test + public void startIntent_multiInstancesSupported_appendsMultipleTaskFag() { + doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any()); Intent startIntent = createStartIntent("startActivity"); + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); + // Put the same component into focus task ActivityManager.RunningTaskInfo focusTaskInfo = createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent); - doReturn(focusTaskInfo).when(mSplitScreenController).getFocusingTaskInfo(); - assertTrue(mSplitScreenController.shouldAddMultipleTaskFlag( - startIntent, SPLIT_POSITION_TOP_OR_LEFT)); - - // Verify launching different activity returns false. - Intent diffIntent = createStartIntent("diffActivity"); - focusTaskInfo = - createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, diffIntent); - doReturn(focusTaskInfo).when(mSplitScreenController).getFocusingTaskInfo(); - assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag( - startIntent, SPLIT_POSITION_TOP_OR_LEFT)); + doReturn(focusTaskInfo).when(mStageCoordinator).getFocusingTaskInfo(); + doReturn(true).when(mStageCoordinator).isValidToEnterSplitScreen(any()); + + mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null); + + verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(), + eq(SPLIT_POSITION_TOP_OR_LEFT), isNull()); + assertEquals(FLAG_ACTIVITY_MULTIPLE_TASK, + mIntentCaptor.getValue().getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK); } @Test - public void testShouldAddMultipleTaskFlag_inSplitScreen() { + public void startIntent_multiInstancesSupported_startTaskInBackgroundBeforeSplitActivated() { + doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any()); + doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any()); + Intent startIntent = createStartIntent("startActivity"); + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); + // Put the same component into focus task + ActivityManager.RunningTaskInfo focusTaskInfo = + createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, startIntent); + doReturn(focusTaskInfo).when(mStageCoordinator).getFocusingTaskInfo(); + doReturn(true).when(mStageCoordinator).isValidToEnterSplitScreen(any()); + // Put the same component into a task in the background + ActivityManager.RecentTaskInfo sameTaskInfo = new ActivityManager.RecentTaskInfo(); + doReturn(sameTaskInfo).when(mRecentTasks).findTaskInBackground(any()); + + mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null); + + verify(mSplitScreenController).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT), + isNull()); + } + + @Test + public void startIntent_multiInstancesSupported_startTaskInBackgroundAfterSplitActivated() { + doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any()); + doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any()); + Intent startIntent = createStartIntent("startActivity"); + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); + // Put the same component into another side of the split doReturn(true).when(mSplitScreenController).isSplitScreenVisible(); + ActivityManager.RunningTaskInfo sameTaskInfo = + createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, startIntent); + doReturn(sameTaskInfo).when(mSplitScreenController).getTaskInfo( + SPLIT_POSITION_BOTTOM_OR_RIGHT); + // Put the same component into a task in the background + doReturn(new ActivityManager.RecentTaskInfo()).when(mRecentTasks) + .findTaskInBackground(any()); + + mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null); + + verify(mSplitScreenController).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT), + isNull()); + } + + @Test + public void startIntent_multiInstancesNotSupported_switchesPositionAfterSplitActivated() { + doReturn(false).when(mSplitScreenController).supportMultiInstancesSplit(any()); Intent startIntent = createStartIntent("startActivity"); + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); + // Put the same component into another side of the split + doReturn(true).when(mSplitScreenController).isSplitScreenVisible(); ActivityManager.RunningTaskInfo sameTaskInfo = createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, startIntent); - Intent diffIntent = createStartIntent("diffActivity"); - ActivityManager.RunningTaskInfo differentTaskInfo = - createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, diffIntent); - - // Verify launching the same activity return false. - doReturn(sameTaskInfo).when(mSplitScreenController) - .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT); - assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag( - startIntent, SPLIT_POSITION_TOP_OR_LEFT)); - - // Verify launching the same activity as adjacent returns true. - doReturn(differentTaskInfo).when(mSplitScreenController) - .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT); - doReturn(sameTaskInfo).when(mSplitScreenController) - .getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT); - assertTrue(mSplitScreenController.shouldAddMultipleTaskFlag( - startIntent, SPLIT_POSITION_TOP_OR_LEFT)); - - // Verify launching different activity from adjacent returns false. - doReturn(differentTaskInfo).when(mSplitScreenController) - .getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT); - doReturn(differentTaskInfo).when(mSplitScreenController) - .getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT); - assertFalse(mSplitScreenController.shouldAddMultipleTaskFlag( - startIntent, SPLIT_POSITION_TOP_OR_LEFT)); + doReturn(sameTaskInfo).when(mSplitScreenController).getTaskInfo( + SPLIT_POSITION_BOTTOM_OR_RIGHT); + + mSplitScreenController.startIntent(pendingIntent, null, SPLIT_POSITION_TOP_OR_LEFT, null); + + verify(mStageCoordinator).switchSplitPosition(anyString()); } private Intent createStartIntent(String activityName) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt new file mode 100644 index 000000000000..ac10ddb0116a --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt @@ -0,0 +1,130 @@ +package com.android.wm.shell.windowdecor + +import android.app.ActivityManager +import android.graphics.Rect +import android.os.IBinder +import android.testing.AndroidTestingRunner +import android.window.WindowContainerToken +import android.window.WindowContainerTransaction.Change.CHANGE_DRAG_RESIZING +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_RIGHT +import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_UNDEFINED +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.argThat +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +/** + * Tests for [TaskPositioner]. + * + * Build/Install/Run: + * atest WMShellUnitTests:TaskPositionerTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class TaskPositionerTest : ShellTestCase() { + + @Mock + private lateinit var mockShellTaskOrganizer: ShellTaskOrganizer + @Mock + private lateinit var mockWindowDecoration: WindowDecoration<*> + @Mock + private lateinit var mockDragStartListener: TaskPositioner.DragStartListener + + @Mock + private lateinit var taskToken: WindowContainerToken + @Mock + private lateinit var taskBinder: IBinder + + private lateinit var taskPositioner: TaskPositioner + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + taskPositioner = TaskPositioner( + mockShellTaskOrganizer, + mockWindowDecoration, + mockDragStartListener + ) + `when`(taskToken.asBinder()).thenReturn(taskBinder) + mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply { + taskId = TASK_ID + token = taskToken + configuration.windowConfiguration.bounds = STARTING_BOUNDS + } + } + + @Test + fun testDragResize_move_skipsDragResizingFlag() { + taskPositioner.onDragResizeStart( + CTRL_TYPE_UNDEFINED, // Move + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + + // Move the task 10px to the right. + val newX = STARTING_BOUNDS.left.toFloat() + 10 + val newY = STARTING_BOUNDS.top.toFloat() + taskPositioner.onDragResizeMove( + newX, + newY + ) + + taskPositioner.onDragResizeEnd(newX, newY) + + verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.changeMask and CHANGE_DRAG_RESIZING) != 0) && + change.dragResizing + } + }) + } + + @Test + fun testDragResize_resize_setsDragResizingFlag() { + taskPositioner.onDragResizeStart( + CTRL_TYPE_RIGHT, // Resize right + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + + // Resize the task by 10px to the right. + val newX = STARTING_BOUNDS.right.toFloat() + 10 + val newY = STARTING_BOUNDS.top.toFloat() + taskPositioner.onDragResizeMove( + newX, + newY + ) + + taskPositioner.onDragResizeEnd(newX, newY) + + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.changeMask and CHANGE_DRAG_RESIZING) != 0) && + change.dragResizing + } + }) + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.changeMask and CHANGE_DRAG_RESIZING) != 0) && + !change.dragResizing + } + }) + } + + companion object { + private const val TASK_ID = 5 + private val STARTING_BOUNDS = Rect(0, 0, 100, 100) + } +} |