From 0c624e107306cd21e37ed16a200d20841f5bd1ad Mon Sep 17 00:00:00 2001 From: mattsziklay Date: Fri, 7 Apr 2023 15:59:20 -0700 Subject: [PATCH] Implement veiled resizing. Implements a veil that covers app contents during drag resizing. Video: http://recall/-/hJNEr4C0IUowK3TyPPf4wT/c8b2u7opjuEJh7NrNhT3hS Bug: 274773589 Test: Manual; drag resize tasks in desktop mode and confirm resize is veiled. Test: atest TaskPositionerTest Change-Id: Iba789762283a3359a78476d557075c1bc427a3d5 --- core/java/android/window/TaskConstants.java | 10 +- .../desktop_mode_resize_veil_background.xml | 20 ++ .../res/layout/desktop_mode_resize_veil.xml | 28 ++ .../shell/desktopmode/DesktopModeStatus.java | 13 + .../CaptionWindowDecorViewModel.java | 4 +- .../DesktopModeWindowDecorViewModel.java | 25 +- .../DesktopModeWindowDecoration.java | 40 +++ .../windowdecor/DragPositioningCallback.java | 14 +- .../DragPositioningCallbackUtility.java | 169 ++++++++++++ .../windowdecor/DragResizeInputListener.java | 45 +-- .../FluidResizeTaskPositioner.java | 106 +++++++ .../wm/shell/windowdecor/ResizeVeil.java | 176 ++++++++++++ .../wm/shell/windowdecor/TaskPositioner.java | 197 ------------- .../VeiledResizeTaskPositioner.java | 106 +++++++ ...st.kt => FluidResizeTaskPositionerTest.kt} | 23 +- .../VeiledResizeTaskPositionerTest.kt | 259 ++++++++++++++++++ 16 files changed, 995 insertions(+), 240 deletions(-) create mode 100644 libs/WindowManager/Shell/res/drawable/desktop_mode_resize_veil_background.xml create mode 100644 libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java delete mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java rename libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/{TaskPositionerTest.kt => FluidResizeTaskPositionerTest.kt} (96%) create mode 100644 libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt diff --git a/core/java/android/window/TaskConstants.java b/core/java/android/window/TaskConstants.java index e18fd50dc38e8..69d79b449615e 100644 --- a/core/java/android/window/TaskConstants.java +++ b/core/java/android/window/TaskConstants.java @@ -73,6 +73,13 @@ public class TaskConstants { */ public static final int TASK_CHILD_LAYER_TASK_OVERLAY = 4 * TASK_CHILD_LAYER_REGION_SIZE; + + /** + * Veil to cover task surface and other window decorations during resizes. + * @hide + */ + public static final int TASK_CHILD_LAYER_RESIZE_VEIL = 6 * TASK_CHILD_LAYER_REGION_SIZE; + /** * Z-orders of task child layers other than activities, task fragments and layers interleaved * with them, e.g. IME windows. [-10000, 10000) is reserved for these layers. @@ -84,7 +91,8 @@ public class TaskConstants { TASK_CHILD_LAYER_COMPAT_UI, TASK_CHILD_LAYER_WINDOW_DECORATIONS, TASK_CHILD_LAYER_RECENTS_ANIMATION_PIP_OVERLAY, - TASK_CHILD_LAYER_TASK_OVERLAY + TASK_CHILD_LAYER_TASK_OVERLAY, + TASK_CHILD_LAYER_RESIZE_VEIL }) public @interface TaskChildLayer {} } diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_resize_veil_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_resize_veil_background.xml new file mode 100644 index 0000000000000..1f3e3a4c5b224 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_resize_veil_background.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml new file mode 100644 index 0000000000000..a4bbd8998cc5d --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml @@ -0,0 +1,28 @@ + + + + + + \ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java index 055949fd8c89d..d1760ed755475 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java @@ -42,6 +42,12 @@ public class DesktopModeStatus { private static final boolean IS_PROTO2_ENABLED = SystemProperties.getBoolean( "persist.wm.debug.desktop_mode_2", false); + /** + * Flag to indicate whether task resizing is veiled. + */ + private static final boolean IS_VEILED_RESIZE_ENABLED = SystemProperties.getBoolean( + "persist.wm.debug.desktop_veiled_resizing", true); + /** * Return {@code true} if desktop mode support is enabled */ @@ -64,6 +70,13 @@ public class DesktopModeStatus { return isProto1Enabled() || isProto2Enabled(); } + /** + * Return {@code true} if veiled resizing is active. If false, fluid resizing is used. + */ + public static boolean isVeiledResizeEnabled() { + return IS_VEILED_RESIZE_ENABLED; + } + /** * Check if desktop mode is active * 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 e8a6a159cb199..212ad9ebceafd 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 @@ -185,8 +185,8 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mSyncQueue); mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration); - final TaskPositioner taskPositioner = - new TaskPositioner(mTaskOrganizer, windowDecoration, mDisplayController); + final FluidResizeTaskPositioner taskPositioner = + new FluidResizeTaskPositioner(mTaskOrganizer, windowDecoration, mDisplayController); final CaptionTouchEventListener touchEventListener = new CaptionTouchEventListener(taskInfo, taskPositioner); windowDecoration.setCaptionListeners(touchEventListener, touchEventListener); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 8fb56fc2561b5..4ef3350d06a46 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -101,7 +101,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private final SparseArray mWindowDecorByTaskId = new SparseArray<>(); - private final DragListenerImpl mDragStartListener = new DragListenerImpl(); + private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl(); private final InputMonitorFactory mInputMonitorFactory; private TaskOperations mTaskOperations; private final Supplier mTransactionFactory; @@ -777,21 +777,32 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mSyncQueue); mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration); - final TaskPositioner taskPositioner = - new TaskPositioner(mTaskOrganizer, windowDecoration, mDisplayController, - mDragStartListener); + final DragPositioningCallback dragPositioningCallback; + if (!DesktopModeStatus.isVeiledResizeEnabled()) { + dragPositioningCallback = + new FluidResizeTaskPositioner(mTaskOrganizer, windowDecoration, + mDisplayController, mDragStartListener); + } else { + windowDecoration.createResizeVeil(); + dragPositioningCallback = + new VeiledResizeTaskPositioner(mTaskOrganizer, windowDecoration, + mDisplayController, mDragStartListener); + } final DesktopModeTouchEventListener touchEventListener = - new DesktopModeTouchEventListener(taskInfo, taskPositioner); + new DesktopModeTouchEventListener(taskInfo, dragPositioningCallback); + windowDecoration.setCaptionListeners(touchEventListener, touchEventListener); windowDecoration.setCornersListener(mCornersListener); - windowDecoration.setDragPositioningCallback(taskPositioner); + windowDecoration.setDragPositioningCallback(dragPositioningCallback); windowDecoration.setDragDetector(touchEventListener.mDragDetector); windowDecoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */); incrementEventReceiverTasks(taskInfo.displayId); } - private class DragListenerImpl implements TaskPositioner.DragStartListener { + + private class DragStartListenerImpl + implements DragPositioningCallbackUtility.DragStartListener { @Override public void onDragStart(int taskId) { mWindowDecorByTaskId.get(taskId).closeHandleMenu(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 619e963aaea64..a2b9a3188d834 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -30,6 +30,7 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Point; import android.graphics.PointF; +import android.graphics.Rect; import android.graphics.Region; import android.graphics.drawable.Drawable; import android.os.Handler; @@ -91,6 +92,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration stableBounds.left) + ? candidateLeft : oldLeft; + } + if ((ctrlType & CTRL_TYPE_RIGHT) != 0) { + final int candidateRight = repositionTaskBounds.right + (int) delta.x; + repositionTaskBounds.right = (candidateRight < stableBounds.right) + ? candidateRight : oldRight; + } + if ((ctrlType & CTRL_TYPE_TOP) != 0) { + final int candidateTop = repositionTaskBounds.top + (int) delta.y; + repositionTaskBounds.top = (candidateTop > stableBounds.top) + ? candidateTop : oldTop; + } + if ((ctrlType & CTRL_TYPE_BOTTOM) != 0) { + final int candidateBottom = repositionTaskBounds.bottom + (int) delta.y; + repositionTaskBounds.bottom = (candidateBottom < stableBounds.bottom) + ? candidateBottom : oldBottom; + } + if (ctrlType == CTRL_TYPE_UNDEFINED) { + repositionTaskBounds.offset((int) delta.x, (int) delta.y); + } + + // If width or height are negative or less than the minimum width or height, revert the + // respective bounds to use previous bound dimensions. + if (repositionTaskBounds.width() < getMinWidth(displayController, windowDecoration)) { + repositionTaskBounds.right = oldRight; + repositionTaskBounds.left = oldLeft; + } + if (repositionTaskBounds.height() < getMinHeight(displayController, windowDecoration)) { + repositionTaskBounds.top = oldTop; + repositionTaskBounds.bottom = oldBottom; + } + // If there are no changes to the bounds after checking new bounds against minimum width + // and height, do not set bounds and return false + if (oldLeft == repositionTaskBounds.left && oldTop == repositionTaskBounds.top + && oldRight == repositionTaskBounds.right + && oldBottom == repositionTaskBounds.bottom) { + return false; + } + return true; + } + + /** + * Apply a bounds change to a task. + * @param wct provided {@link WindowContainerTransaction} that may contain other changes + * @param windowDecoration decor of task we are changing bounds for + * @param taskBounds new bounds of this task + * @param taskOrganizer applies the provided WindowContainerTransaction + */ + static void applyTaskBoundsChange(WindowContainerTransaction wct, + WindowDecoration windowDecoration, Rect taskBounds, ShellTaskOrganizer taskOrganizer) { + wct.setBounds(windowDecoration.mTaskInfo.token, taskBounds); + taskOrganizer.applyTransaction(wct); + } + + private static float getMinWidth(DisplayController displayController, + WindowDecoration windowDecoration) { + return windowDecoration.mTaskInfo.minWidth < 0 ? getDefaultMinSize(displayController, + windowDecoration) + : windowDecoration.mTaskInfo.minWidth; + } + + private static float getMinHeight(DisplayController displayController, + WindowDecoration windowDecoration) { + return windowDecoration.mTaskInfo.minHeight < 0 ? getDefaultMinSize(displayController, + windowDecoration) + : windowDecoration.mTaskInfo.minHeight; + } + + private static float getDefaultMinSize(DisplayController displayController, + WindowDecoration windowDecoration) { + float density = displayController.getDisplayLayout(windowDecoration.mTaskInfo.displayId) + .densityDpi() * DisplayMetrics.DENSITY_DEFAULT_SCALE; + return windowDecoration.mTaskInfo.defaultMinSize * density; + } + + interface DragStartListener { + /** + * Inform the implementing class that a drag resize has started + * @param taskId id of this positioner's {@link WindowDecoration} + */ + void onDragStart(int taskId); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java index 34c8c087cf957..287d861872885 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java @@ -21,6 +21,11 @@ import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; +import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM; +import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT; +import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT; +import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP; + import android.content.Context; import android.graphics.Rect; import android.graphics.Region; @@ -367,7 +372,7 @@ class DragResizeInputListener implements AutoCloseable { return calculateResizeHandlesCtrlType(x, y) != 0; } - @TaskPositioner.CtrlType + @DragPositioningCallback.CtrlType private int calculateCtrlType(boolean isTouch, float x, float y) { if (isTouch) { return calculateCornersCtrlType(x, y); @@ -375,62 +380,62 @@ class DragResizeInputListener implements AutoCloseable { return calculateResizeHandlesCtrlType(x, y); } - @TaskPositioner.CtrlType + @DragPositioningCallback.CtrlType private int calculateResizeHandlesCtrlType(float x, float y) { int ctrlType = 0; if (x < 0) { - ctrlType |= TaskPositioner.CTRL_TYPE_LEFT; + ctrlType |= CTRL_TYPE_LEFT; } if (x > mTaskWidth) { - ctrlType |= TaskPositioner.CTRL_TYPE_RIGHT; + ctrlType |= CTRL_TYPE_RIGHT; } if (y < 0) { - ctrlType |= TaskPositioner.CTRL_TYPE_TOP; + ctrlType |= CTRL_TYPE_TOP; } if (y > mTaskHeight) { - ctrlType |= TaskPositioner.CTRL_TYPE_BOTTOM; + ctrlType |= CTRL_TYPE_BOTTOM; } return ctrlType; } - @TaskPositioner.CtrlType + @DragPositioningCallback.CtrlType private int calculateCornersCtrlType(float x, float y) { int xi = (int) x; int yi = (int) y; if (mLeftTopCornerBounds.contains(xi, yi)) { - return TaskPositioner.CTRL_TYPE_LEFT | TaskPositioner.CTRL_TYPE_TOP; + return CTRL_TYPE_LEFT | CTRL_TYPE_TOP; } if (mLeftBottomCornerBounds.contains(xi, yi)) { - return TaskPositioner.CTRL_TYPE_LEFT | TaskPositioner.CTRL_TYPE_BOTTOM; + return CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM; } if (mRightTopCornerBounds.contains(xi, yi)) { - return TaskPositioner.CTRL_TYPE_RIGHT | TaskPositioner.CTRL_TYPE_TOP; + return CTRL_TYPE_RIGHT | CTRL_TYPE_TOP; } if (mRightBottomCornerBounds.contains(xi, yi)) { - return TaskPositioner.CTRL_TYPE_RIGHT | TaskPositioner.CTRL_TYPE_BOTTOM; + return CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM; } return 0; } private void updateCursorType(float x, float y) { - @TaskPositioner.CtrlType int ctrlType = calculateResizeHandlesCtrlType(x, y); + @DragPositioningCallback.CtrlType int ctrlType = calculateResizeHandlesCtrlType(x, y); int cursorType = PointerIcon.TYPE_DEFAULT; switch (ctrlType) { - case TaskPositioner.CTRL_TYPE_LEFT: - case TaskPositioner.CTRL_TYPE_RIGHT: + case CTRL_TYPE_LEFT: + case CTRL_TYPE_RIGHT: cursorType = PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW; break; - case TaskPositioner.CTRL_TYPE_TOP: - case TaskPositioner.CTRL_TYPE_BOTTOM: + case CTRL_TYPE_TOP: + case CTRL_TYPE_BOTTOM: cursorType = PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW; break; - case TaskPositioner.CTRL_TYPE_LEFT | TaskPositioner.CTRL_TYPE_TOP: - case TaskPositioner.CTRL_TYPE_RIGHT | TaskPositioner.CTRL_TYPE_BOTTOM: + case CTRL_TYPE_LEFT | CTRL_TYPE_TOP: + case CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM: cursorType = PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW; break; - case TaskPositioner.CTRL_TYPE_LEFT | TaskPositioner.CTRL_TYPE_BOTTOM: - case TaskPositioner.CTRL_TYPE_RIGHT | TaskPositioner.CTRL_TYPE_TOP: + case CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM: + case CTRL_TYPE_RIGHT | CTRL_TYPE_TOP: cursorType = PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW; break; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java new file mode 100644 index 0000000000000..b366c809537f7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor; + +import android.graphics.PointF; +import android.graphics.Rect; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayController; + +/** + * A task positioner that resizes/relocates task contents as it is dragged. + * Utilizes {@link DragPositioningCallbackUtility} to determine new task bounds. + */ +class FluidResizeTaskPositioner implements DragPositioningCallback { + private final ShellTaskOrganizer mTaskOrganizer; + private final WindowDecoration mWindowDecoration; + private DisplayController mDisplayController; + private DragPositioningCallbackUtility.DragStartListener mDragStartListener; + private final Rect mStableBounds = new Rect(); + private final Rect mTaskBoundsAtDragStart = new Rect(); + private final PointF mRepositionStartPoint = new PointF(); + private final Rect mRepositionTaskBounds = new Rect(); + private int mCtrlType; + private boolean mHasMoved; + + FluidResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration, + DisplayController displayController) { + this(taskOrganizer, windowDecoration, displayController, dragStartListener -> {}); + } + + FluidResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration, + DisplayController displayController, + DragPositioningCallbackUtility.DragStartListener dragStartListener) { + mTaskOrganizer = taskOrganizer; + mWindowDecoration = windowDecoration; + mDisplayController = displayController; + mDragStartListener = dragStartListener; + } + + @Override + public void onDragPositioningStart(int ctrlType, float x, float y) { + mCtrlType = ctrlType; + mTaskBoundsAtDragStart.set( + mWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds()); + mRepositionStartPoint.set(x, y); + mDragStartListener.onDragStart(mWindowDecoration.mTaskInfo.taskId); + } + + @Override + public void onDragPositioningMove(float x, float y) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + PointF delta = DragPositioningCallbackUtility.calculateDelta(x, y, mRepositionStartPoint); + if (DragPositioningCallbackUtility.changeBounds(mCtrlType, mHasMoved, + mRepositionTaskBounds, mTaskBoundsAtDragStart, mStableBounds, delta, + mDisplayController, mWindowDecoration)) { + // The task is being resized, send the |dragResizing| hint to core with the first + // bounds-change wct. + if (!mHasMoved && mCtrlType != CTRL_TYPE_UNDEFINED) { + // This is the first bounds change since drag resize operation started. + wct.setDragResizing(mWindowDecoration.mTaskInfo.token, true /* dragResizing */); + } + DragPositioningCallbackUtility.applyTaskBoundsChange(wct, mWindowDecoration, + mRepositionTaskBounds, mTaskOrganizer); + mHasMoved = true; + } + } + + @Override + public void onDragPositioningEnd(float x, float y) { + // |mHasMoved| being false means there is no real change to the task bounds in WM core, so + // we don't need a WCT to finish it. + if (mHasMoved) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setDragResizing(mWindowDecoration.mTaskInfo.token, false /* dragResizing */); + PointF delta = DragPositioningCallbackUtility.calculateDelta(x, y, + mRepositionStartPoint); + if (DragPositioningCallbackUtility.changeBounds(mCtrlType, mHasMoved, + mRepositionTaskBounds, mTaskBoundsAtDragStart, mStableBounds, delta, + mDisplayController, mWindowDecoration)) { + wct.setBounds(mWindowDecoration.mTaskInfo.token, mRepositionTaskBounds); + } + mTaskOrganizer.applyTransaction(wct); + } + + mTaskBoundsAtDragStart.setEmpty(); + mRepositionStartPoint.set(0, 0); + mCtrlType = CTRL_TYPE_UNDEFINED; + mHasMoved = false; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java new file mode 100644 index 0000000000000..fa2d7a87c3cb5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.app.ActivityManager.RunningTaskInfo; +import android.content.Context; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.view.Display; +import android.view.LayoutInflater; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.View; +import android.view.WindowManager; +import android.view.WindowlessWindowManager; +import android.widget.ImageView; +import android.window.TaskConstants; + +import com.android.wm.shell.R; + +import java.util.function.Supplier; + +/** + * Creates and updates a veil that covers task contents on resize. + */ +public class ResizeVeil { + private static final int RESIZE_ALPHA_DURATION = 200; + private final Context mContext; + private final Supplier mSurfaceControlBuilderSupplier; + private final Supplier mSurfaceControlTransactionSupplier; + private final Drawable mAppIcon; + private SurfaceControl mParentSurface; + private SurfaceControl mVeilSurface; + private final RunningTaskInfo mTaskInfo; + private SurfaceControlViewHost mViewHost; + private final Display mDisplay; + + public ResizeVeil(Context context, Drawable appIcon, RunningTaskInfo taskInfo, + Supplier surfaceControlBuilderSupplier, Display display, + Supplier surfaceControlTransactionSupplier) { + mContext = context; + mAppIcon = appIcon; + mSurfaceControlBuilderSupplier = surfaceControlBuilderSupplier; + mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier; + mTaskInfo = taskInfo; + mDisplay = display; + setupResizeVeil(); + } + + /** + * Create the veil in its default invisible state. + */ + private void setupResizeVeil() { + SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); + final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get(); + mVeilSurface = builder + .setName("Resize veil of Task= " + mTaskInfo.taskId) + .setContainerLayer() + .build(); + View v = LayoutInflater.from(mContext) + .inflate(R.layout.desktop_mode_resize_veil, null); + + t.setPosition(mVeilSurface, 0, 0) + .setLayer(mVeilSurface, TaskConstants.TASK_CHILD_LAYER_RESIZE_VEIL) + .apply(); + Rect taskBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); + final WindowManager.LayoutParams lp = + new WindowManager.LayoutParams(taskBounds.width(), + taskBounds.height(), + WindowManager.LayoutParams.TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSPARENT); + lp.setTitle("Resize veil of Task=" + mTaskInfo.taskId); + lp.setTrustedOverlay(); + WindowlessWindowManager windowManager = new WindowlessWindowManager(mTaskInfo.configuration, + mVeilSurface, null /* hostInputToken */); + mViewHost = new SurfaceControlViewHost(mContext, mDisplay, windowManager, "ResizeVeil"); + mViewHost.setView(v, lp); + + final ImageView appIcon = mViewHost.getView().findViewById(R.id.veil_application_icon); + appIcon.setImageDrawable(mAppIcon); + } + + /** + * Animate veil's alpha to 1, fading it in. + */ + public void showVeil(SurfaceControl parentSurface) { + // Parent surface can change, ensure it is up to date. + SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); + if (!parentSurface.equals(mParentSurface)) { + t.reparent(mVeilSurface, parentSurface); + mParentSurface = parentSurface; + } + t.show(mVeilSurface) + .apply(); + final ValueAnimator animator = new ValueAnimator(); + animator.setFloatValues(0f, 1f); + animator.setDuration(RESIZE_ALPHA_DURATION); + animator.addUpdateListener(animation -> { + t.setAlpha(mVeilSurface, animator.getAnimatedFraction()); + t.apply(); + }); + animator.start(); + } + + /** + * Update veil bounds to match bounds changes. + * @param newBounds bounds to update veil to. + */ + public void relayout(Rect newBounds) { + SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); + mViewHost.relayout(newBounds.width(), newBounds.height()); + t.setWindowCrop(mVeilSurface, newBounds.width(), newBounds.height()); + t.setPosition(mParentSurface, newBounds.left, newBounds.top); + t.setWindowCrop(mParentSurface, newBounds.width(), newBounds.height()); + mViewHost.getView().getViewRootImpl().applyTransactionOnDraw(t); + } + + /** + * Animate veil's alpha to 0, fading it out. + */ + public void hideVeil() { + final View resizeVeilView = mViewHost.getView(); + final ValueAnimator animator = new ValueAnimator(); + animator.setFloatValues(1, 0); + animator.setDuration(RESIZE_ALPHA_DURATION); + animator.addUpdateListener(animation -> { + SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); + t.setAlpha(mVeilSurface, 1 - animator.getAnimatedFraction()); + t.apply(); + }); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); + t.hide(mVeilSurface); + t.apply(); + } + }); + animator.start(); + } + + /** + * Dispose of veil when it is no longer needed, likely on close of its container decor. + */ + void dispose() { + if (mViewHost != null) { + mViewHost.release(); + mViewHost = null; + } + if (mVeilSurface != null) { + final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); + t.remove(mVeilSurface); + mVeilSurface = null; + t.apply(); + } + } +} 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 deleted file mode 100644 index 0bce3acecb3c8..0000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskPositioner.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.windowdecor; - -import android.annotation.IntDef; -import android.graphics.PointF; -import android.graphics.Rect; -import android.util.DisplayMetrics; -import android.window.WindowContainerTransaction; - -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.common.DisplayController; - -class TaskPositioner implements DragPositioningCallback { - - @IntDef({CTRL_TYPE_UNDEFINED, CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT, CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM}) - @interface CtrlType {} - - static final int CTRL_TYPE_UNDEFINED = 0; - static final int CTRL_TYPE_LEFT = 1; - static final int CTRL_TYPE_RIGHT = 2; - static final int CTRL_TYPE_TOP = 4; - static final int CTRL_TYPE_BOTTOM = 8; - - private final ShellTaskOrganizer mTaskOrganizer; - private final DisplayController mDisplayController; - private final WindowDecoration mWindowDecoration; - - private final Rect mTempBounds = new Rect(); - private final Rect mTaskBoundsAtDragStart = new Rect(); - private final PointF mRepositionStartPoint = new PointF(); - private final Rect mRepositionTaskBounds = new Rect(); - private boolean mHasMoved = false; - - private int mCtrlType; - private DragStartListener mDragStartListener; - - TaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration, - DisplayController displayController) { - this(taskOrganizer, windowDecoration, displayController, dragStartListener -> {}); - } - - TaskPositioner(ShellTaskOrganizer taskOrganizer, WindowDecoration windowDecoration, - DisplayController displayController, DragStartListener dragStartListener) { - mTaskOrganizer = taskOrganizer; - mWindowDecoration = windowDecoration; - mDisplayController = displayController; - mDragStartListener = dragStartListener; - } - - @Override - public void onDragPositioningStart(int ctrlType, float x, float y) { - mHasMoved = false; - - mDragStartListener.onDragStart(mWindowDecoration.mTaskInfo.taskId); - mCtrlType = ctrlType; - - mTaskBoundsAtDragStart.set( - mWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds()); - mRepositionStartPoint.set(x, y); - } - - @Override - public void onDragPositioningMove(float x, float y) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - if (changeBounds(wct, x, y)) { - // The task is being resized, send the |dragResizing| hint to core with the first - // bounds-change wct. - if (!mHasMoved && mCtrlType != CTRL_TYPE_UNDEFINED) { - // This is the first bounds change since drag resize operation started. - wct.setDragResizing(mWindowDecoration.mTaskInfo.token, true /* dragResizing */); - } - mTaskOrganizer.applyTransaction(wct); - mHasMoved = true; - } - } - - @Override - public void onDragPositioningEnd(float x, float y) { - // |mHasMoved| being false means there is no real change to the task bounds in WM core, so - // we don't need a WCT to finish it. - if (mHasMoved) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.setDragResizing(mWindowDecoration.mTaskInfo.token, false /* dragResizing */); - changeBounds(wct, x, y); - mTaskOrganizer.applyTransaction(wct); - } - - mCtrlType = CTRL_TYPE_UNDEFINED; - mTaskBoundsAtDragStart.setEmpty(); - mRepositionStartPoint.set(0, 0); - mHasMoved = false; - } - - private boolean changeBounds(WindowContainerTransaction wct, float x, float y) { - // |mRepositionTaskBounds| is the bounds last reported if |mHasMoved| is true. If it's not - // true, we can compare it against |mTaskBoundsAtDragStart|. - final int oldLeft = mHasMoved ? mRepositionTaskBounds.left : mTaskBoundsAtDragStart.left; - final int oldTop = mHasMoved ? mRepositionTaskBounds.top : mTaskBoundsAtDragStart.top; - final int oldRight = mHasMoved ? mRepositionTaskBounds.right : mTaskBoundsAtDragStart.right; - final int oldBottom = - mHasMoved ? mRepositionTaskBounds.bottom : mTaskBoundsAtDragStart.bottom; - - final float deltaX = x - mRepositionStartPoint.x; - final float deltaY = y - mRepositionStartPoint.y; - mRepositionTaskBounds.set(mTaskBoundsAtDragStart); - - final Rect stableBounds = mTempBounds; - // Make sure the new resizing destination in any direction falls within the stable bounds. - // If not, set the bounds back to the old location that was valid to avoid conflicts with - // some regions such as the gesture area. - mDisplayController.getDisplayLayout(mWindowDecoration.mDisplay.getDisplayId()) - .getStableBounds(stableBounds); - if ((mCtrlType & CTRL_TYPE_LEFT) != 0) { - final int candidateLeft = mRepositionTaskBounds.left + (int) deltaX; - mRepositionTaskBounds.left = (candidateLeft > stableBounds.left) - ? candidateLeft : oldLeft; - } - if ((mCtrlType & CTRL_TYPE_RIGHT) != 0) { - final int candidateRight = mRepositionTaskBounds.right + (int) deltaX; - mRepositionTaskBounds.right = (candidateRight < stableBounds.right) - ? candidateRight : oldRight; - } - if ((mCtrlType & CTRL_TYPE_TOP) != 0) { - final int candidateTop = mRepositionTaskBounds.top + (int) deltaY; - mRepositionTaskBounds.top = (candidateTop > stableBounds.top) - ? candidateTop : oldTop; - } - if ((mCtrlType & CTRL_TYPE_BOTTOM) != 0) { - final int candidateBottom = mRepositionTaskBounds.bottom + (int) deltaY; - mRepositionTaskBounds.bottom = (candidateBottom < stableBounds.bottom) - ? candidateBottom : oldBottom; - } - if (mCtrlType == CTRL_TYPE_UNDEFINED) { - mRepositionTaskBounds.offset((int) deltaX, (int) deltaY); - } - - // If width or height are negative or less than the minimum width or height, revert the - // respective bounds to use previous bound dimensions. - if (mRepositionTaskBounds.width() < getMinWidth()) { - mRepositionTaskBounds.right = oldRight; - mRepositionTaskBounds.left = oldLeft; - } - if (mRepositionTaskBounds.height() < getMinHeight()) { - mRepositionTaskBounds.top = oldTop; - mRepositionTaskBounds.bottom = oldBottom; - } - // If there are no changes to the bounds after checking new bounds against minimum width - // and height, do not set bounds and return false - if (oldLeft == mRepositionTaskBounds.left && oldTop == mRepositionTaskBounds.top - && oldRight == mRepositionTaskBounds.right - && oldBottom == mRepositionTaskBounds.bottom) { - return false; - } - - wct.setBounds(mWindowDecoration.mTaskInfo.token, mRepositionTaskBounds); - return true; - } - - private float getMinWidth() { - return mWindowDecoration.mTaskInfo.minWidth < 0 ? getDefaultMinSize() - : mWindowDecoration.mTaskInfo.minWidth; - } - - private float getMinHeight() { - return mWindowDecoration.mTaskInfo.minHeight < 0 ? getDefaultMinSize() - : mWindowDecoration.mTaskInfo.minHeight; - } - - private float getDefaultMinSize() { - float density = mDisplayController.getDisplayLayout(mWindowDecoration.mTaskInfo.displayId) - .densityDpi() * DisplayMetrics.DENSITY_DEFAULT_SCALE; - return mWindowDecoration.mTaskInfo.defaultMinSize * density; - } - - interface DragStartListener { - /** - * Inform the implementing class that a drag resize has started - * @param taskId id of this positioner's {@link WindowDecoration} - */ - void onDragStart(int taskId); - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java new file mode 100644 index 0000000000000..ef8332f5e3471 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor; + +import android.graphics.PointF; +import android.graphics.Rect; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayController; + +/** + * A task positioner that also takes into account resizing a + * {@link com.android.wm.shell.windowdecor.ResizeVeil}. + * If the drag is resizing the task, we resize the veil instead. + * If the drag is repositioning, we update in the typical manner. + */ +public class VeiledResizeTaskPositioner implements DragPositioningCallback { + + private DesktopModeWindowDecoration mDesktopWindowDecoration; + private ShellTaskOrganizer mTaskOrganizer; + private DisplayController mDisplayController; + private DragPositioningCallbackUtility.DragStartListener mDragStartListener; + private final Rect mStableBounds = new Rect(); + private final Rect mTaskBoundsAtDragStart = new Rect(); + private final PointF mRepositionStartPoint = new PointF(); + private final Rect mRepositionTaskBounds = new Rect(); + private int mCtrlType; + private boolean mHasMoved; + + + public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, + DesktopModeWindowDecoration windowDecoration, DisplayController displayController, + DragPositioningCallbackUtility.DragStartListener dragStartListener) { + mTaskOrganizer = taskOrganizer; + mDesktopWindowDecoration = windowDecoration; + mDisplayController = displayController; + mDragStartListener = dragStartListener; + } + + @Override + public void onDragPositioningStart(int ctrlType, float x, float y) { + mCtrlType = ctrlType; + mTaskBoundsAtDragStart.set( + mDesktopWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds()); + mRepositionStartPoint.set(x, y); + if (mCtrlType != CTRL_TYPE_UNDEFINED) { + mDesktopWindowDecoration.showResizeVeil(); + } + mHasMoved = false; + mDragStartListener.onDragStart(mDesktopWindowDecoration.mTaskInfo.taskId); + } + + @Override + public void onDragPositioningMove(float x, float y) { + PointF delta = DragPositioningCallbackUtility.calculateDelta(x, y, mRepositionStartPoint); + if (DragPositioningCallbackUtility.changeBounds(mCtrlType, mHasMoved, + mRepositionTaskBounds, mTaskBoundsAtDragStart, mStableBounds, delta, + mDisplayController, mDesktopWindowDecoration)) { + if (mCtrlType != CTRL_TYPE_UNDEFINED) { + mDesktopWindowDecoration.updateResizeVeil(mRepositionTaskBounds); + } else { + DragPositioningCallbackUtility.applyTaskBoundsChange( + new WindowContainerTransaction(), mDesktopWindowDecoration, + mRepositionTaskBounds, mTaskOrganizer); + } + mHasMoved = true; + } + } + + @Override + public void onDragPositioningEnd(float x, float y) { + PointF delta = DragPositioningCallbackUtility.calculateDelta(x, y, + mRepositionStartPoint); + if (mHasMoved && DragPositioningCallbackUtility.changeBounds(mCtrlType, mHasMoved, + mRepositionTaskBounds, mTaskBoundsAtDragStart, mStableBounds, delta, + mDisplayController, mDesktopWindowDecoration)) { + DragPositioningCallbackUtility.applyTaskBoundsChange( + new WindowContainerTransaction(), mDesktopWindowDecoration, + mRepositionTaskBounds, mTaskOrganizer); + } + // TODO: (b/279062291) Synchronize the start of hide to the end of the draw triggered above. + if (mCtrlType != CTRL_TYPE_UNDEFINED) { + mDesktopWindowDecoration.updateResizeVeil(mRepositionTaskBounds); + mDesktopWindowDecoration.hideResizeVeil(); + } + mCtrlType = CTRL_TYPE_UNDEFINED; + mTaskBoundsAtDragStart.setEmpty(); + mRepositionStartPoint.set(0, 0); + mHasMoved = false; + } +} 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/FluidResizeTaskPositionerTest.kt similarity index 96% rename from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/TaskPositionerTest.kt rename to libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt index 94c064bda7631..84ccddeb6a767 100644 --- 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/FluidResizeTaskPositionerTest.kt @@ -14,10 +14,10 @@ import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase -import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_BOTTOM -import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_RIGHT -import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_TOP -import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_UNDEFINED +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -30,21 +30,21 @@ import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations /** - * Tests for [TaskPositioner]. + * Tests for [FluidResizeTaskPositioner]. * * Build/Install/Run: - * atest WMShellUnitTests:TaskPositionerTest + * atest WMShellUnitTests:FluidResizeTaskPositionerTest */ @SmallTest @RunWith(AndroidTestingRunner::class) -class TaskPositionerTest : ShellTestCase() { +class FluidResizeTaskPositionerTest : ShellTestCase() { @Mock private lateinit var mockShellTaskOrganizer: ShellTaskOrganizer @Mock private lateinit var mockWindowDecoration: WindowDecoration<*> @Mock - private lateinit var mockDragStartListener: TaskPositioner.DragStartListener + private lateinit var mockDragStartListener: DragPositioningCallbackUtility.DragStartListener @Mock private lateinit var taskToken: WindowContainerToken @@ -58,18 +58,19 @@ class TaskPositionerTest : ShellTestCase() { @Mock private lateinit var mockDisplay: Display - private lateinit var taskPositioner: TaskPositioner + private lateinit var taskPositioner: FluidResizeTaskPositioner @Before fun setUp() { MockitoAnnotations.initMocks(this) - taskPositioner = TaskPositioner( + taskPositioner = + FluidResizeTaskPositioner( mockShellTaskOrganizer, mockWindowDecoration, mockDisplayController, mockDragStartListener - ) + ) `when`(taskToken.asBinder()).thenReturn(taskBinder) `when`(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt new file mode 100644 index 0000000000000..bf365ca782ee9 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.windowdecor + +import android.app.ActivityManager +import android.app.WindowConfiguration +import android.graphics.Rect +import android.os.IBinder +import android.testing.AndroidTestingRunner +import android.view.Display +import android.window.WindowContainerToken +import androidx.test.filters.SmallTest +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.Mockito.any +import org.mockito.Mockito.argThat +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +/** + * Tests for [VeiledResizeTaskPositioner]. + * + * Build/Install/Run: + * atest WMShellUnitTests:VeiledResizeTaskPositionerTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class VeiledResizeTaskPositionerTest : ShellTestCase() { + + @Mock + private lateinit var mockShellTaskOrganizer: ShellTaskOrganizer + @Mock + private lateinit var mockDesktopWindowDecoration: DesktopModeWindowDecoration + @Mock + private lateinit var mockDragStartListener: DragPositioningCallbackUtility.DragStartListener + + @Mock + private lateinit var taskToken: WindowContainerToken + @Mock + private lateinit var taskBinder: IBinder + + @Mock + private lateinit var mockDisplayController: DisplayController + @Mock + private lateinit var mockDisplayLayout: DisplayLayout + @Mock + private lateinit var mockDisplay: Display + + private lateinit var taskPositioner: VeiledResizeTaskPositioner + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + taskPositioner = + VeiledResizeTaskPositioner( + mockShellTaskOrganizer, + mockDesktopWindowDecoration, + mockDisplayController, + mockDragStartListener + ) + + `when`(taskToken.asBinder()).thenReturn(taskBinder) + `when`(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout) + `when`(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI) + `when`(mockDisplayLayout.getStableBounds(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(STABLE_BOUNDS) + } + + mockDesktopWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply { + taskId = TASK_ID + token = taskToken + minWidth = MIN_WIDTH + minHeight = MIN_HEIGHT + defaultMinSize = DEFAULT_MIN + displayId = DISPLAY_ID + configuration.windowConfiguration.bounds = STARTING_BOUNDS + } + mockDesktopWindowDecoration.mDisplay = mockDisplay + `when`(mockDisplay.displayId).thenAnswer { DISPLAY_ID } + } + + @Test + fun testDragResize_noMove_showsResizeVeil() { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + verify(mockDesktopWindowDecoration).showResizeVeil() + + taskPositioner.onDragPositioningEnd( + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + verify(mockDesktopWindowDecoration).hideResizeVeil() + } + + @Test + fun testDragResize_movesTask_doesNotShowResizeVeil() { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_UNDEFINED, + STARTING_BOUNDS.left.toFloat() + 50, + STARTING_BOUNDS.top.toFloat() + ) + + taskPositioner.onDragPositioningMove( + STARTING_BOUNDS.left.toFloat() + 60, + STARTING_BOUNDS.top.toFloat() + 10 + ) + val rectAfterMove = Rect(STARTING_BOUNDS) + rectAfterMove.left += 10 + rectAfterMove.right += 10 + rectAfterMove.top += 10 + rectAfterMove.bottom += 10 + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 && + change.configuration.windowConfiguration.bounds == rectAfterMove + } + }) + + taskPositioner.onDragPositioningEnd( + STARTING_BOUNDS.left.toFloat() + 70, + STARTING_BOUNDS.top.toFloat() + 20 + ) + val rectAfterEnd = Rect(rectAfterMove) + rectAfterEnd.left += 10 + rectAfterEnd.top += 10 + rectAfterEnd.right += 10 + rectAfterEnd.bottom += 10 + + verify(mockDesktopWindowDecoration, never()).createResizeVeil() + verify(mockDesktopWindowDecoration, never()).hideResizeVeil() + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 && + change.configuration.windowConfiguration.bounds == rectAfterEnd + } + }) + } + + @Test + fun testDragResize_resize_boundsUpdateOnEnd() { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + verify(mockDesktopWindowDecoration).showResizeVeil() + + taskPositioner.onDragPositioningMove( + STARTING_BOUNDS.right.toFloat() + 10, + STARTING_BOUNDS.top.toFloat() + 10 + ) + + val rectAfterMove = Rect(STARTING_BOUNDS) + rectAfterMove.right += 10 + rectAfterMove.top += 10 + verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 && + change.configuration.windowConfiguration.bounds == rectAfterMove + } + }) + + taskPositioner.onDragPositioningEnd( + STARTING_BOUNDS.right.toFloat() + 20, + STARTING_BOUNDS.top.toFloat() + 20 + ) + val rectAfterEnd = Rect(rectAfterMove) + rectAfterEnd.right += 10 + rectAfterEnd.top += 10 + verify(mockDesktopWindowDecoration, times(2)).updateResizeVeil(any()) + verify(mockDesktopWindowDecoration).hideResizeVeil() + + verify(mockShellTaskOrganizer).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 && + change.configuration.windowConfiguration.bounds == rectAfterEnd + } + }) + } + + @Test + fun testDragResize_noEffectiveMove_skipsTransactionOnEnd() { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + verify(mockDesktopWindowDecoration).showResizeVeil() + + taskPositioner.onDragPositioningMove( + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat() + ) + + taskPositioner.onDragPositioningEnd( + STARTING_BOUNDS.left.toFloat() + 10, + STARTING_BOUNDS.top.toFloat() + 10 + ) + verify(mockDesktopWindowDecoration).hideResizeVeil() + + verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) + } + }) + } + + companion object { + private const val TASK_ID = 5 + private const val MIN_WIDTH = 10 + private const val MIN_HEIGHT = 10 + private const val DENSITY_DPI = 20 + private const val DEFAULT_MIN = 40 + private const val DISPLAY_ID = 1 + private const val NAVBAR_HEIGHT = 50 + private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600) + private val STARTING_BOUNDS = Rect(0, 0, 100, 100) + private val STABLE_BOUNDS = Rect( + DISPLAY_BOUNDS.left, + DISPLAY_BOUNDS.top, + DISPLAY_BOUNDS.right, + DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT + ) + } +}