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
This commit is contained in:
mattsziklay
2023-04-07 15:59:20 -07:00
parent 1e84c915b9
commit 0c624e1073
16 changed files with 995 additions and 240 deletions

View File

@@ -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 {}
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<shape android:shape="rectangle"
xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/white" />
</shape>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/desktop_mode_resize_veil_background">
<ImageView
android:id="@+id/veil_application_icon"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_gravity="center"
android:contentDescription="@string/app_icon_text" />
</FrameLayout>

View File

@@ -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
*

View File

@@ -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);

View File

@@ -101,7 +101,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
private final SparseArray<DesktopModeWindowDecoration> 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<SurfaceControl.Transaction> 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();

View File

@@ -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<WindowDecorLin
private AdditionalWindow mHandleMenuWindowingPill;
private AdditionalWindow mHandleMenuMoreActionsPill;
private ResizeVeil mResizeVeil;
private Drawable mAppIcon;
private CharSequence mAppName;
@@ -323,6 +326,42 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
mDragResizeListener = null;
}
/**
* Create the resize veil for this task. Note the veil's visibility is View.GONE by default
* until a resize event calls showResizeVeil below.
*/
void createResizeVeil() {
mResizeVeil = new ResizeVeil(mContext, mAppIcon, mTaskInfo,
mSurfaceControlBuilderSupplier, mDisplay, mSurfaceControlTransactionSupplier);
}
/**
* Fade in the resize veil
*/
void showResizeVeil() {
mResizeVeil.showVeil(mTaskSurface);
}
/**
* Set new bounds for the resize veil
*/
void updateResizeVeil(Rect newBounds) {
mResizeVeil.relayout(newBounds);
}
/**
* Fade the resize veil out.
*/
void hideResizeVeil() {
mResizeVeil.hideVeil();
}
private void disposeResizeVeil() {
if (mResizeVeil == null) return;
mResizeVeil.dispose();
mResizeVeil = null;
}
/**
* Create and display handle menu window
*/
@@ -601,6 +640,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
closeDragResizeListener();
closeHandleMenu();
mCornersListener.onTaskCornersRemoved(mTaskInfo.taskId);
disposeResizeVeil();
super.close();
}

View File

@@ -16,19 +16,29 @@
package com.android.wm.shell.windowdecor;
import android.annotation.IntDef;
/**
* Callback called when receiving drag-resize or drag-move related input events.
*/
public interface DragPositioningCallback {
@IntDef({CTRL_TYPE_UNDEFINED, CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT, CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM})
@interface CtrlType {}
int CTRL_TYPE_UNDEFINED = 0;
int CTRL_TYPE_LEFT = 1;
int CTRL_TYPE_RIGHT = 2;
int CTRL_TYPE_TOP = 4;
int CTRL_TYPE_BOTTOM = 8;
/**
* Called when a drag-resize or drag-move starts.
*
* @param ctrlType {@link TaskPositioner.CtrlType} indicating the direction of resizing, use
* @param ctrlType {@link CtrlType} indicating the direction of resizing, use
* {@code 0} to indicate it's a move
* @param x x coordinate in window decoration coordinate system where the drag starts
* @param y y coordinate in window decoration coordinate system where the drag starts
*/
void onDragPositioningStart(@TaskPositioner.CtrlType int ctrlType, float x, float y);
void onDragPositioningStart(@CtrlType int ctrlType, float x, float y);
/**
* Called when the pointer moves during a drag-resize or drag-move.

View File

@@ -0,0 +1,169 @@
/*
* 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 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 static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED;
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;
/**
* Utility class that contains logic common to classes implementing {@link DragPositioningCallback}
* Specifically, this class contains logic for determining changed bounds from a drag input
* and applying that change to the task bounds when applicable.
*/
public class DragPositioningCallbackUtility {
/**
* Determine the delta between input's current point and the input start point.
* @param inputX current input x coordinate
* @param inputY current input y coordinate
* @param repositionStartPoint initial input coordinate
* @return delta between these two points
*/
static PointF calculateDelta(float inputX, float inputY, PointF repositionStartPoint) {
final float deltaX = inputX - repositionStartPoint.x;
final float deltaY = inputY - repositionStartPoint.y;
return new PointF(deltaX, deltaY);
}
/**
* Based on type of drag and delta provided, calculate the new bounds to display for this task.
* @param ctrlType type of drag being performed
* @param hasMoved whether the current drag has moved on a prior input event
* @param repositionTaskBounds the bounds the task is being repositioned to
* @param taskBoundsAtDragStart the bounds of the task on the first drag input event
* @param stableBounds bounds that represent the resize limit of this task
* @param delta difference between start input and current input in x/y coordinates
* @param displayController task's display controller
* @param windowDecoration window decoration of the task being dragged
* @return whether this method changed repositionTaskBounds
*/
static boolean changeBounds(int ctrlType, boolean hasMoved,
Rect repositionTaskBounds, Rect taskBoundsAtDragStart, Rect stableBounds,
PointF delta, DisplayController displayController, WindowDecoration windowDecoration) {
// |mRepositionTaskBounds| is the bounds last reported if |mHasMoved| is true. If it's not
// true, we can compare it against |mTaskBoundsAtDragStart|.
final int oldLeft = hasMoved ? repositionTaskBounds.left : taskBoundsAtDragStart.left;
final int oldTop = hasMoved ? repositionTaskBounds.top : taskBoundsAtDragStart.top;
final int oldRight = hasMoved ? repositionTaskBounds.right : taskBoundsAtDragStart.right;
final int oldBottom =
hasMoved ? repositionTaskBounds.bottom : taskBoundsAtDragStart.bottom;
repositionTaskBounds.set(taskBoundsAtDragStart);
// 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.
displayController.getDisplayLayout(windowDecoration.mDisplay.getDisplayId())
.getStableBounds(stableBounds);
if ((ctrlType & CTRL_TYPE_LEFT) != 0) {
final int candidateLeft = repositionTaskBounds.left + (int) delta.x;
repositionTaskBounds.left = (candidateLeft > 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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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<SurfaceControl.Builder> mSurfaceControlBuilderSupplier;
private final Supplier<SurfaceControl.Transaction> 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<SurfaceControl.Builder> surfaceControlBuilderSupplier, Display display,
Supplier<SurfaceControl.Transaction> 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();
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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)

View File

@@ -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
)
}
}