diff options
8 files changed, 925 insertions, 271 deletions
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 4ee2c1a1da60..c2c90c84278e 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -515,8 +515,20 @@ <!-- The size of the icon shown in the resize veil. --> <dimen name="desktop_mode_resize_veil_icon_size">96dp</dimen> + <!-- The with of the border around the app task for edge resizing, when + enable_windowing_edge_drag_resize is enabled. --> + <dimen name="desktop_mode_edge_handle">12dp</dimen> + + <!-- The original width of the border around the app task for edge resizing, when + enable_windowing_edge_drag_resize is disabled. --> <dimen name="freeform_resize_handle">15dp</dimen> + <!-- The size of the corner region for drag resizing with touch, when a larger touch region is + appropriate. Applied when enable_windowing_edge_drag_resize is enabled. --> + <dimen name="desktop_mode_corner_resize_large">48dp</dimen> + + <!-- The original size of the corner region for darg resizing, when + enable_windowing_edge_drag_resize is disabled. --> <dimen name="freeform_resize_corner">44dp</dimen> <!-- The width of the area at the sides of the screen where a freeform task will transition to diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index beead6a19355..43fd32ba1750 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -16,17 +16,23 @@ package com.android.wm.shell.windowdecor; +import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize; +import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize; +import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize; + import android.annotation.NonNull; import android.app.ActivityManager.RunningTaskInfo; import android.app.WindowConfiguration; import android.app.WindowConfiguration.WindowingMode; import android.content.Context; import android.content.res.ColorStateList; +import android.content.res.Resources; import android.graphics.Color; import android.graphics.Rect; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.VectorDrawable; import android.os.Handler; +import android.util.Size; import android.view.Choreographer; import android.view.SurfaceControl; import android.view.View; @@ -222,7 +228,6 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL mHandler, mChoreographer, mDisplay.getDisplayId(), - 0 /* taskCornerRadius */, mDecorationContainerSurface, mDragPositioningCallback, mSurfaceControlBuilderSupplier, @@ -234,12 +239,10 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL .getScaledTouchSlop(); mDragDetector.setTouchSlop(touchSlop); - final int resize_handle = mResult.mRootView.getResources() - .getDimensionPixelSize(R.dimen.freeform_resize_handle); - final int resize_corner = mResult.mRootView.getResources() - .getDimensionPixelSize(R.dimen.freeform_resize_corner); - mDragResizeListener.setGeometry( - mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop); + final Resources res = mResult.mRootView.getResources(); + mDragResizeListener.setGeometry(new DragResizeWindowGeometry(0 /* taskCornerRadius */, + new Size(mResult.mWidth, mResult.mHeight), getResizeEdgeHandleSize(res), + getFineResizeCornerSize(res), getLargeResizeCornerSize(res)), touchSlop); } /** 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 963b1303c379..2bbe530fbaf6 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 @@ -24,6 +24,9 @@ import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_UP; import static com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT; +import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize; +import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize; +import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize; import android.annotation.NonNull; import android.app.ActivityManager; @@ -42,6 +45,7 @@ import android.graphics.Region; import android.graphics.drawable.Drawable; import android.os.Handler; import android.util.Log; +import android.util.Size; import android.view.Choreographer; import android.view.MotionEvent; import android.view.SurfaceControl; @@ -276,7 +280,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mHandler, mChoreographer, mDisplay.getDisplayId(), - mRelayoutParams.mCornerRadius, mDecorationContainerSurface, mDragPositioningCallback, mSurfaceControlBuilderSupplier, @@ -288,15 +291,13 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin .getScaledTouchSlop(); mDragDetector.setTouchSlop(touchSlop); - final int resize_handle = mResult.mRootView.getResources() - .getDimensionPixelSize(R.dimen.freeform_resize_handle); - final int resize_corner = mResult.mRootView.getResources() - .getDimensionPixelSize(R.dimen.freeform_resize_corner); - // If either task geometry or position have changed, update this task's // exclusion region listener + final Resources res = mResult.mRootView.getResources(); if (mDragResizeListener.setGeometry( - mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop) + new DragResizeWindowGeometry(mRelayoutParams.mCornerRadius, + new Size(mResult.mWidth, mResult.mHeight), getResizeEdgeHandleSize(res), + getFineResizeCornerSize(res), getLargeResizeCornerSize(res)), touchSlop) || !mTaskInfo.positionInParent.equals(mPositionInParent)) { updateExclusionRegion(); } @@ -427,7 +428,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return mHandleMenu != null; } - boolean shouldResizeListenerHandleEvent(MotionEvent e, Point offset) { + boolean shouldResizeListenerHandleEvent(@NonNull MotionEvent e, @NonNull Point offset) { return mDragResizeListener != null && mDragResizeListener.shouldHandleEvent(e, offset); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java index 8ce2d6d6d092..421ffd929fb2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java @@ -23,6 +23,9 @@ import android.graphics.Rect; * Callback called when receiving drag-resize or drag-move related input events. */ public interface DragPositioningCallback { + /** + * Indicates the direction of resizing. May be combined together to indicate a diagonal drag. + */ @IntDef(flag = true, value = { CTRL_TYPE_UNDEFINED, CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT, CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM }) 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 97eb4a425238..9624d46678bf 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 @@ -30,6 +30,7 @@ import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT; import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP; +import android.annotation.NonNull; import android.content.Context; import android.graphics.Point; import android.graphics.Rect; @@ -39,6 +40,7 @@ import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; +import android.util.Size; import android.view.Choreographer; import android.view.IWindowSession; import android.view.InputChannel; @@ -55,6 +57,7 @@ import android.window.InputTransferToken; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; +import java.util.function.Consumer; import java.util.function.Supplier; /** @@ -66,40 +69,20 @@ import java.util.function.Supplier; class DragResizeInputListener implements AutoCloseable { private static final String TAG = "DragResizeInputListener"; private final IWindowSession mWindowSession = WindowManagerGlobal.getWindowSession(); - private final Context mContext; - private final Handler mHandler; - private final Choreographer mChoreographer; - private final InputManager mInputManager; private final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier; private final int mDisplayId; private final IBinder mClientToken; - private final InputTransferToken mInputTransferToken; private final SurfaceControl mDecorationSurface; private final InputChannel mInputChannel; private final TaskResizeInputEventReceiver mInputEventReceiver; - private final DragPositioningCallback mCallback; private final SurfaceControl mInputSinkSurface; private final IBinder mSinkClientToken; private final InputChannel mSinkInputChannel; private final DisplayController mDisplayController; - - private int mTaskWidth; - private int mTaskHeight; - private int mResizeHandleThickness; - private int mCornerSize; - private int mTaskCornerRadius; - - private Rect mLeftTopCornerBounds; - private Rect mRightTopCornerBounds; - private Rect mLeftBottomCornerBounds; - private Rect mRightBottomCornerBounds; - - private int mDragPointerId = -1; - private DragDetector mDragDetector; private final Region mTouchRegion = new Region(); DragResizeInputListener( @@ -107,23 +90,17 @@ class DragResizeInputListener implements AutoCloseable { Handler handler, Choreographer choreographer, int displayId, - int taskCornerRadius, SurfaceControl decorationSurface, DragPositioningCallback callback, Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, DisplayController displayController) { - mInputManager = context.getSystemService(InputManager.class); - mContext = context; - mHandler = handler; - mChoreographer = choreographer; mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier; mDisplayId = displayId; - mTaskCornerRadius = taskCornerRadius; mDecorationSurface = decorationSurface; mDisplayController = displayController; mClientToken = new Binder(); - mInputTransferToken = new InputTransferToken(); + final InputTransferToken inputTransferToken = new InputTransferToken(); mInputChannel = new InputChannel(); try { mWindowSession.grantInputChannel( @@ -136,18 +113,19 @@ class DragResizeInputListener implements AutoCloseable { INPUT_FEATURE_SPY, TYPE_APPLICATION, null /* windowToken */, - mInputTransferToken, + inputTransferToken, TAG + " of " + decorationSurface.toString(), mInputChannel); } catch (RemoteException e) { e.rethrowFromSystemServer(); } - mInputEventReceiver = new TaskResizeInputEventReceiver( - mInputChannel, mHandler, mChoreographer); - mCallback = callback; - mDragDetector = new DragDetector(mInputEventReceiver); - mDragDetector.setTouchSlop(ViewConfiguration.get(context).getScaledTouchSlop()); + mInputEventReceiver = new TaskResizeInputEventReceiver(context, mInputChannel, callback, + handler, choreographer, () -> { + final DisplayLayout layout = mDisplayController.getDisplayLayout(mDisplayId); + return new Size(layout.width(), layout.height()); + }, this::updateSinkInputChannel); + mInputEventReceiver.setTouchSlop(ViewConfiguration.get(context).getScaledTouchSlop()); mInputSinkSurface = surfaceControlBuilderSupplier.get() .setName("TaskInputSink of " + decorationSurface) @@ -171,7 +149,7 @@ class DragResizeInputListener implements AutoCloseable { INPUT_FEATURE_NO_INPUT_CHANNEL, TYPE_INPUT_CONSUMER, null /* windowToken */, - mInputTransferToken, + inputTransferToken, "TaskInputSink of " + decorationSurface, mSinkInputChannel); } catch (RemoteException e) { @@ -182,86 +160,26 @@ class DragResizeInputListener implements AutoCloseable { /** * Updates the geometry (the touch region) of this drag resize handler. * - * @param taskWidth The width of the task. - * @param taskHeight The height of the task. - * @param resizeHandleThickness The thickness of the resize handle in pixels. - * @param cornerSize The size of the resize handle centered in each corner. - * @param touchSlop The distance in pixels user has to drag with touch for it to register as - * a resize action. + * @param incomingGeometry The geometry update to apply for this task's drag resize regions. + * @param touchSlop The distance in pixels user has to drag with touch for it to register + * as a resize action. * @return whether the geometry has changed or not */ - boolean setGeometry(int taskWidth, int taskHeight, int resizeHandleThickness, int cornerSize, - int touchSlop) { - if (mTaskWidth == taskWidth && mTaskHeight == taskHeight - && mResizeHandleThickness == resizeHandleThickness - && mCornerSize == cornerSize) { + boolean setGeometry(@NonNull DragResizeWindowGeometry incomingGeometry, int touchSlop) { + DragResizeWindowGeometry geometry = mInputEventReceiver.getGeometry(); + if (incomingGeometry.equals(geometry)) { + // Geometry hasn't changed size so skip all updates. return false; + } else { + geometry = incomingGeometry; } - - mTaskWidth = taskWidth; - mTaskHeight = taskHeight; - mResizeHandleThickness = resizeHandleThickness; - mCornerSize = cornerSize; - mDragDetector.setTouchSlop(touchSlop); + mInputEventReceiver.setTouchSlop(touchSlop); mTouchRegion.setEmpty(); - final Rect topInputBounds = new Rect( - -mResizeHandleThickness, - -mResizeHandleThickness, - mTaskWidth + mResizeHandleThickness, - 0); - mTouchRegion.union(topInputBounds); - - final Rect leftInputBounds = new Rect( - -mResizeHandleThickness, - 0, - 0, - mTaskHeight); - mTouchRegion.union(leftInputBounds); - - final Rect rightInputBounds = new Rect( - mTaskWidth, - 0, - mTaskWidth + mResizeHandleThickness, - mTaskHeight); - mTouchRegion.union(rightInputBounds); - - final Rect bottomInputBounds = new Rect( - -mResizeHandleThickness, - mTaskHeight, - mTaskWidth + mResizeHandleThickness, - mTaskHeight + mResizeHandleThickness); - mTouchRegion.union(bottomInputBounds); - - // Set up touch areas in each corner. - int cornerRadius = mCornerSize / 2; - mLeftTopCornerBounds = new Rect( - -cornerRadius, - -cornerRadius, - cornerRadius, - cornerRadius); - mTouchRegion.union(mLeftTopCornerBounds); - - mRightTopCornerBounds = new Rect( - mTaskWidth - cornerRadius, - -cornerRadius, - mTaskWidth + cornerRadius, - cornerRadius); - mTouchRegion.union(mRightTopCornerBounds); - - mLeftBottomCornerBounds = new Rect( - -cornerRadius, - mTaskHeight - cornerRadius, - cornerRadius, - mTaskHeight + cornerRadius); - mTouchRegion.union(mLeftBottomCornerBounds); - - mRightBottomCornerBounds = new Rect( - mTaskWidth - cornerRadius, - mTaskHeight - cornerRadius, - mTaskWidth + cornerRadius, - mTaskHeight + cornerRadius); - mTouchRegion.union(mRightBottomCornerBounds); + // Apply the geometry to the touch region. + geometry.union(mTouchRegion); + mInputEventReceiver.setGeometry(geometry); + mInputEventReceiver.setTouchRegion(mTouchRegion); try { mWindowSession.updateInputChannel( @@ -276,8 +194,9 @@ class DragResizeInputListener implements AutoCloseable { e.rethrowFromSystemServer(); } + final Size taskSize = geometry.getTaskSize(); mSurfaceControlTransactionSupplier.get() - .setWindowCrop(mInputSinkSurface, mTaskWidth, mTaskHeight) + .setWindowCrop(mInputSinkSurface, taskSize.getWidth(), taskSize.getHeight()) .apply(); // The touch region of the TaskInputSink should be the touch region of this // DragResizeInputHandler minus the task bounds. Pilfering events isn't enough to prevent @@ -290,21 +209,16 @@ class DragResizeInputListener implements AutoCloseable { // issue. However, were there touchscreen-only a region out of the task bounds, mouse // gestures will become no-op in that region, even though the mouse gestures may appear to // be performed on the input window behind the resize handle. - mTouchRegion.op(0, 0, mTaskWidth, mTaskHeight, Region.Op.DIFFERENCE); + mTouchRegion.op(0, 0, taskSize.getWidth(), taskSize.getHeight(), Region.Op.DIFFERENCE); updateSinkInputChannel(mTouchRegion); return true; } /** - * Generate a Region that encapsulates all 4 corner handles + * Generate a Region that encapsulates all 4 corner handles and window edges. */ - Region getCornersRegion() { - Region region = new Region(); - region.union(mLeftTopCornerBounds); - region.union(mLeftBottomCornerBounds); - region.union(mRightTopCornerBounds); - region.union(mRightBottomCornerBounds); - return region; + @NonNull Region getCornersRegion() { + return mInputEventReceiver.getCornersRegion(); } private void updateSinkInputChannel(Region region) { @@ -322,7 +236,7 @@ class DragResizeInputListener implements AutoCloseable { } } - boolean shouldHandleEvent(MotionEvent e, Point offset) { + boolean shouldHandleEvent(@NonNull MotionEvent e, @NonNull Point offset) { return mInputEventReceiver.shouldHandleEvent(e, offset); } @@ -351,19 +265,37 @@ class DragResizeInputListener implements AutoCloseable { .apply(); } - private class TaskResizeInputEventReceiver extends InputEventReceiver - implements DragDetector.MotionEventHandler { - private final Choreographer mChoreographer; - private final Runnable mConsumeBatchEventRunnable; + private static class TaskResizeInputEventReceiver extends InputEventReceiver implements + DragDetector.MotionEventHandler { + @NonNull private final Context mContext; + private final InputManager mInputManager; + @NonNull private final InputChannel mInputChannel; + @NonNull private final DragPositioningCallback mCallback; + @NonNull private final Choreographer mChoreographer; + @NonNull private final Runnable mConsumeBatchEventRunnable; + @NonNull private final DragDetector mDragDetector; + @NonNull private final Supplier<Size> mDisplayLayoutSizeSupplier; + @NonNull private final Consumer<Region> mTouchRegionConsumer; + private final Rect mTmpRect = new Rect(); private boolean mConsumeBatchEventScheduled; + private DragResizeWindowGeometry mDragResizeWindowGeometry; + private Region mTouchRegion; private boolean mShouldHandleEvents; private int mLastCursorType = PointerIcon.TYPE_DEFAULT; private Rect mDragStartTaskBounds; - private final Rect mTmpRect = new Rect(); - - private TaskResizeInputEventReceiver( - InputChannel inputChannel, Handler handler, Choreographer choreographer) { + private int mDragPointerId = -1; + + private TaskResizeInputEventReceiver(@NonNull Context context, + @NonNull InputChannel inputChannel, + @NonNull DragPositioningCallback callback, @NonNull Handler handler, + @NonNull Choreographer choreographer, + @NonNull Supplier<Size> displayLayoutSizeSupplier, + @NonNull Consumer<Region> touchRegionConsumer) { super(inputChannel, handler.getLooper()); + mContext = context; + mInputManager = context.getSystemService(InputManager.class); + mInputChannel = inputChannel; + mCallback = callback; mChoreographer = choreographer; mConsumeBatchEventRunnable = () -> { @@ -376,6 +308,48 @@ class DragResizeInputListener implements AutoCloseable { scheduleConsumeBatchEvent(); } }; + + mDragDetector = new DragDetector(this); + mDisplayLayoutSizeSupplier = displayLayoutSizeSupplier; + mTouchRegionConsumer = touchRegionConsumer; + } + + /** + * Returns the geometry of the areas to drag resize. + */ + DragResizeWindowGeometry getGeometry() { + return mDragResizeWindowGeometry; + } + + /** + * Updates the geometry of the areas to drag resize. + */ + void setGeometry(@NonNull DragResizeWindowGeometry dragResizeWindowGeometry) { + mDragResizeWindowGeometry = dragResizeWindowGeometry; + } + + /** + * Sets how much slop to allow for touches. + */ + void setTouchSlop(int touchSlop) { + mDragDetector.setTouchSlop(touchSlop); + } + + /** + * Updates the region accepting input for drag resizing the task. + */ + void setTouchRegion(@NonNull Region touchRegion) { + mTouchRegion = touchRegion; + } + + /** + * Returns the union of all regions that can be touched for drag resizing; the corners and + * window edges. + */ + @NonNull Region getCornersRegion() { + Region region = new Region(); + mDragResizeWindowGeometry.union(region); + return region; } @Override @@ -416,14 +390,15 @@ class DragResizeInputListener implements AutoCloseable { boolean isTouch = isTouchEvent(e); switch (e.getActionMasked()) { case MotionEvent.ACTION_DOWN: { - mShouldHandleEvents = shouldHandleEvent(e, isTouch, new Point() /* offset */); + mShouldHandleEvents = mDragResizeWindowGeometry.shouldHandleEvent(e, isTouch, + new Point() /* offset */); if (mShouldHandleEvents) { mDragPointerId = e.getPointerId(0); float x = e.getX(0); float y = e.getY(0); float rawX = e.getRawX(0); float rawY = e.getRawY(0); - int ctrlType = calculateCtrlType(isTouch, x, y); + int ctrlType = mDragResizeWindowGeometry.calculateCtrlType(isTouch, x, y); mDragStartTaskBounds = mCallback.onDragPositioningStart(ctrlType, rawX, rawY); // Increase the input sink region to cover the whole screen; this is to @@ -455,7 +430,7 @@ class DragResizeInputListener implements AutoCloseable { // If taskBounds has changed, setGeometry will be called and update the // sink region. Otherwise, we should revert it here. if (taskBounds.equals(mDragStartTaskBounds)) { - updateSinkInputChannel(mTouchRegion); + mTouchRegionConsumer.accept(mTouchRegion); } } mShouldHandleEvents = false; @@ -480,125 +455,20 @@ class DragResizeInputListener implements AutoCloseable { private void updateInputSinkRegionForDrag(Rect taskBounds) { mTmpRect.set(taskBounds); - final DisplayLayout layout = mDisplayController.getDisplayLayout(mDisplayId); - final Region dragTouchRegion = new Region(-taskBounds.left, - -taskBounds.top, - -taskBounds.left + layout.width(), - -taskBounds.top + layout.height()); + final Size displayLayoutSize = mDisplayLayoutSizeSupplier.get(); + final Region dragTouchRegion = new Region(-taskBounds.left, -taskBounds.top, + -taskBounds.left + displayLayoutSize.getWidth(), + -taskBounds.top + displayLayoutSize.getHeight()); // Remove the localized task bounds from the touch region. mTmpRect.offsetTo(0, 0); dragTouchRegion.op(mTmpRect, Region.Op.DIFFERENCE); - updateSinkInputChannel(dragTouchRegion); - } - - private boolean isInCornerBounds(float xf, float yf) { - return calculateCornersCtrlType(xf, yf) != 0; - } - - private boolean isInResizeHandleBounds(float x, float y) { - return calculateResizeHandlesCtrlType(x, y) != 0; - } - - @DragPositioningCallback.CtrlType - private int calculateCtrlType(boolean isTouch, float x, float y) { - if (isTouch) { - return calculateCornersCtrlType(x, y); - } - return calculateResizeHandlesCtrlType(x, y); - } - - @DragPositioningCallback.CtrlType - private int calculateResizeHandlesCtrlType(float x, float y) { - int ctrlType = 0; - // mTaskCornerRadius is only used in comparing with corner regions. Comparisons with - // sides will use the bounds specified in setGeometry and not go into task bounds. - if (x < mTaskCornerRadius) { - ctrlType |= CTRL_TYPE_LEFT; - } - if (x > mTaskWidth - mTaskCornerRadius) { - ctrlType |= CTRL_TYPE_RIGHT; - } - if (y < mTaskCornerRadius) { - ctrlType |= CTRL_TYPE_TOP; - } - if (y > mTaskHeight - mTaskCornerRadius) { - ctrlType |= CTRL_TYPE_BOTTOM; - } - // Check distances from the center if it's in one of four corners. - if ((ctrlType & (CTRL_TYPE_LEFT | CTRL_TYPE_RIGHT)) != 0 - && (ctrlType & (CTRL_TYPE_TOP | CTRL_TYPE_BOTTOM)) != 0) { - return checkDistanceFromCenter(ctrlType, x, y); - } - // Otherwise, we should make sure we don't resize tasks inside task bounds. - return (x < 0 || y < 0 || x >= mTaskWidth || y >= mTaskHeight) ? ctrlType : 0; - } - - // If corner input is not within appropriate distance of corner radius, do not use it. - // If input is not on a corner or is within valid distance, return ctrlType. - @DragPositioningCallback.CtrlType - private int checkDistanceFromCenter(@DragPositioningCallback.CtrlType int ctrlType, - float x, float y) { - int centerX; - int centerY; - - // Determine center of rounded corner circle; this is simply the corner if radius is 0. - switch (ctrlType) { - case CTRL_TYPE_LEFT | CTRL_TYPE_TOP: { - centerX = mTaskCornerRadius; - centerY = mTaskCornerRadius; - break; - } - case CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM: { - centerX = mTaskCornerRadius; - centerY = mTaskHeight - mTaskCornerRadius; - break; - } - case CTRL_TYPE_RIGHT | CTRL_TYPE_TOP: { - centerX = mTaskWidth - mTaskCornerRadius; - centerY = mTaskCornerRadius; - break; - } - case CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM: { - centerX = mTaskWidth - mTaskCornerRadius; - centerY = mTaskHeight - mTaskCornerRadius; - break; - } - default: { - throw new IllegalArgumentException("ctrlType should be complex, but it's 0x" - + Integer.toHexString(ctrlType)); - } - } - double distanceFromCenter = Math.hypot(x - centerX, y - centerY); - - if (distanceFromCenter < mTaskCornerRadius + mResizeHandleThickness - && distanceFromCenter >= mTaskCornerRadius) { - return ctrlType; - } - return 0; - } - - @DragPositioningCallback.CtrlType - private int calculateCornersCtrlType(float x, float y) { - int xi = (int) x; - int yi = (int) y; - if (mLeftTopCornerBounds.contains(xi, yi)) { - return CTRL_TYPE_LEFT | CTRL_TYPE_TOP; - } - if (mLeftBottomCornerBounds.contains(xi, yi)) { - return CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM; - } - if (mRightTopCornerBounds.contains(xi, yi)) { - return CTRL_TYPE_RIGHT | CTRL_TYPE_TOP; - } - if (mRightBottomCornerBounds.contains(xi, yi)) { - return CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM; - } - return 0; + mTouchRegionConsumer.accept(dragTouchRegion); } private void updateCursorType(int displayId, int deviceId, int pointerId, float x, float y) { - @DragPositioningCallback.CtrlType int ctrlType = calculateResizeHandlesCtrlType(x, y); + @DragPositioningCallback.CtrlType int ctrlType = + mDragResizeWindowGeometry.calculateCtrlType(/* isTouch= */ false, x, y); int cursorType = PointerIcon.TYPE_DEFAULT; switch (ctrlType) { @@ -640,19 +510,7 @@ class DragResizeInputListener implements AutoCloseable { } private boolean shouldHandleEvent(MotionEvent e, Point offset) { - return shouldHandleEvent(e, isTouchEvent(e), offset); - } - - private boolean shouldHandleEvent(MotionEvent e, boolean isTouch, Point offset) { - boolean result; - final float x = e.getX(0) + offset.x; - final float y = e.getY(0) + offset.y; - if (isTouch) { - result = isInCornerBounds(x, y); - } else { - result = isInResizeHandleBounds(x, y); - } - return result; + return mDragResizeWindowGeometry.shouldHandleEvent(e, offset); } private boolean isTouchEvent(MotionEvent e) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java new file mode 100644 index 000000000000..eafb56995db7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java @@ -0,0 +1,434 @@ +/* + * Copyright (C) 2024 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 android.view.InputDevice.SOURCE_TOUCHSCREEN; + +import static com.android.window.flags.Flags.enableWindowingEdgeDragResize; +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.annotation.NonNull; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.Region; +import android.util.Size; +import android.view.MotionEvent; + +import com.android.wm.shell.R; + +import java.util.Objects; + +/** + * Geometry for a drag resize region for a particular window. + */ +final class DragResizeWindowGeometry { + private final int mTaskCornerRadius; + private final Size mTaskSize; + // The size of the handle applied to the edges of the window, for the user to drag resize. + private final int mResizeHandleThickness; + // The task corners to permit drag resizing with a course input, such as touch. + + private final @NonNull TaskCorners mLargeTaskCorners; + // The task corners to permit drag resizing with a fine input, such as stylus or cursor. + private final @NonNull TaskCorners mFineTaskCorners; + // The bounds for each edge drag region, which can resize the task in one direction. + private final @NonNull Rect mTopEdgeBounds; + private final @NonNull Rect mLeftEdgeBounds; + private final @NonNull Rect mRightEdgeBounds; + private final @NonNull Rect mBottomEdgeBounds; + + DragResizeWindowGeometry(int taskCornerRadius, @NonNull Size taskSize, + int resizeHandleThickness, int fineCornerSize, int largeCornerSize) { + mTaskCornerRadius = taskCornerRadius; + mTaskSize = taskSize; + mResizeHandleThickness = resizeHandleThickness; + + mLargeTaskCorners = new TaskCorners(mTaskSize, largeCornerSize); + mFineTaskCorners = new TaskCorners(mTaskSize, fineCornerSize); + + // Save touch areas for each edge. + mTopEdgeBounds = new Rect( + -mResizeHandleThickness, + -mResizeHandleThickness, + mTaskSize.getWidth() + mResizeHandleThickness, + 0); + mLeftEdgeBounds = new Rect( + -mResizeHandleThickness, + 0, + 0, + mTaskSize.getHeight()); + mRightEdgeBounds = new Rect( + mTaskSize.getWidth(), + 0, + mTaskSize.getWidth() + mResizeHandleThickness, + mTaskSize.getHeight()); + mBottomEdgeBounds = new Rect( + -mResizeHandleThickness, + mTaskSize.getHeight(), + mTaskSize.getWidth() + mResizeHandleThickness, + mTaskSize.getHeight() + mResizeHandleThickness); + } + + /** + * Returns the resource value to use for the resize handle on the edge of the window. + */ + static int getResizeEdgeHandleSize(@NonNull Resources res) { + return enableWindowingEdgeDragResize() + ? res.getDimensionPixelSize(R.dimen.desktop_mode_edge_handle) + : res.getDimensionPixelSize(R.dimen.freeform_resize_handle); + } + + /** + * Returns the resource value to use for course input, such as touch, that benefits from a large + * square on each of the window's corners. + */ + static int getLargeResizeCornerSize(@NonNull Resources res) { + return res.getDimensionPixelSize(R.dimen.desktop_mode_corner_resize_large); + } + + /** + * Returns the resource value to use for fine input, such as stylus, that can use a smaller + * square on each of the window's corners. + */ + static int getFineResizeCornerSize(@NonNull Resources res) { + return res.getDimensionPixelSize(R.dimen.freeform_resize_corner); + } + + /** + * Returns the size of the task this geometry is calculated for. + */ + @NonNull Size getTaskSize() { + // Safe to return directly since size is immutable. + return mTaskSize; + } + + /** + * Returns the union of all regions that can be touched for drag resizing; the corners window + * and window edges. + */ + void union(@NonNull Region region) { + // Apply the edge resize regions. + region.union(mTopEdgeBounds); + region.union(mLeftEdgeBounds); + region.union(mRightEdgeBounds); + region.union(mBottomEdgeBounds); + + if (enableWindowingEdgeDragResize()) { + // Apply the corners as well for the larger corners, to ensure we capture all possible + // touches. + mLargeTaskCorners.union(region); + } else { + // Only apply fine corners for the legacy approach. + mFineTaskCorners.union(region); + } + } + + /** + * Returns if this MotionEvent should be handled, based on its source and position. + */ + boolean shouldHandleEvent(@NonNull MotionEvent e, @NonNull Point offset) { + return shouldHandleEvent(e, isTouchEvent(e), offset); + } + + /** + * Returns if this MotionEvent should be handled, based on its source and position. + */ + boolean shouldHandleEvent(@NonNull MotionEvent e, boolean isTouch, @NonNull Point offset) { + final float x = e.getX(0) + offset.x; + final float y = e.getY(0) + offset.y; + + if (enableWindowingEdgeDragResize()) { + // First check if touch falls within a corner. + // Large corner bounds are used for course input like touch, otherwise fine bounds. + boolean result = isTouch + ? isInCornerBounds(mLargeTaskCorners, x, y) + : isInCornerBounds(mFineTaskCorners, x, y); + // Check if touch falls within the edge resize handle, since edge resizing can apply + // for any input source. + if (!result) { + result = isInEdgeResizeBounds(x, y); + } + return result; + } else { + // Legacy uses only fine corners for touch, and edges only for non-touch input. + return isTouch + ? isInCornerBounds(mFineTaskCorners, x, y) + : isInEdgeResizeBounds(x, y); + } + } + + private boolean isTouchEvent(@NonNull MotionEvent e) { + return (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN; + } + + private boolean isInCornerBounds(TaskCorners corners, float xf, float yf) { + return corners.calculateCornersCtrlType(xf, yf) != 0; + } + + private boolean isInEdgeResizeBounds(float x, float y) { + return calculateEdgeResizeCtrlType(x, y) != 0; + } + + /** + * Returns the control type for the drag-resize, based on the touch regions and this + * MotionEvent's coordinates. + */ + @DragPositioningCallback.CtrlType + int calculateCtrlType(boolean isTouch, float x, float y) { + if (enableWindowingEdgeDragResize()) { + // First check if touch falls within a corner. + // Large corner bounds are used for course input like touch, otherwise fine bounds. + int ctrlType = isTouch + ? mLargeTaskCorners.calculateCornersCtrlType(x, y) + : mFineTaskCorners.calculateCornersCtrlType(x, y); + // Check if touch falls within the edge resize handle, since edge resizing can apply + // for any input source. + if (ctrlType == CTRL_TYPE_UNDEFINED) { + ctrlType = calculateEdgeResizeCtrlType(x, y); + } + return ctrlType; + } else { + // Legacy uses only fine corners for touch, and edges only for non-touch input. + return isTouch + ? mFineTaskCorners.calculateCornersCtrlType(x, y) + : calculateEdgeResizeCtrlType(x, y); + } + } + + @DragPositioningCallback.CtrlType + private int calculateEdgeResizeCtrlType(float x, float y) { + int ctrlType = CTRL_TYPE_UNDEFINED; + // mTaskCornerRadius is only used in comparing with corner regions. Comparisons with + // sides will use the bounds specified in setGeometry and not go into task bounds. + if (x < mTaskCornerRadius) { + ctrlType |= CTRL_TYPE_LEFT; + } + if (x > mTaskSize.getWidth() - mTaskCornerRadius) { + ctrlType |= CTRL_TYPE_RIGHT; + } + if (y < mTaskCornerRadius) { + ctrlType |= CTRL_TYPE_TOP; + } + if (y > mTaskSize.getHeight() - mTaskCornerRadius) { + ctrlType |= CTRL_TYPE_BOTTOM; + } + // If the touch is within one of the four corners, check if it is within the bounds of the + // // handle. + if ((ctrlType & (CTRL_TYPE_LEFT | CTRL_TYPE_RIGHT)) != 0 + && (ctrlType & (CTRL_TYPE_TOP | CTRL_TYPE_BOTTOM)) != 0) { + return checkDistanceFromCenter(ctrlType, x, y); + } + // Otherwise, we should make sure we don't resize tasks inside task bounds. + return (x < 0 || y < 0 || x >= mTaskSize.getWidth() || y >= mTaskSize.getHeight()) + ? ctrlType : CTRL_TYPE_UNDEFINED; + } + + /** + * Return {@code ctrlType} if the corner input is outside the (potentially rounded) corner of + * the task, and within the thickness of the resize handle. Otherwise, return 0. + */ + @DragPositioningCallback.CtrlType + private int checkDistanceFromCenter(@DragPositioningCallback.CtrlType int ctrlType, float x, + float y) { + final Point cornerRadiusCenter = calculateCenterForCornerRadius(ctrlType); + double distanceFromCenter = Math.hypot(x - cornerRadiusCenter.x, y - cornerRadiusCenter.y); + + if (distanceFromCenter < mTaskCornerRadius + mResizeHandleThickness + && distanceFromCenter >= mTaskCornerRadius) { + return ctrlType; + } + return CTRL_TYPE_UNDEFINED; + } + + /** + * Returns center of rounded corner circle; this is simply the corner if radius is 0. + */ + private Point calculateCenterForCornerRadius(@DragPositioningCallback.CtrlType int ctrlType) { + int centerX; + int centerY; + + switch (ctrlType) { + case CTRL_TYPE_LEFT | CTRL_TYPE_TOP: { + centerX = mTaskCornerRadius; + centerY = mTaskCornerRadius; + break; + } + case CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM: { + centerX = mTaskCornerRadius; + centerY = mTaskSize.getHeight() - mTaskCornerRadius; + break; + } + case CTRL_TYPE_RIGHT | CTRL_TYPE_TOP: { + centerX = mTaskSize.getWidth() - mTaskCornerRadius; + centerY = mTaskCornerRadius; + break; + } + case CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM: { + centerX = mTaskSize.getWidth() - mTaskCornerRadius; + centerY = mTaskSize.getHeight() - mTaskCornerRadius; + break; + } + default: { + throw new IllegalArgumentException( + "ctrlType should be complex, but it's 0x" + Integer.toHexString(ctrlType)); + } + } + return new Point(centerX, centerY); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof DragResizeWindowGeometry other)) return false; + + return this.mTaskCornerRadius == other.mTaskCornerRadius + && this.mTaskSize.equals(other.mTaskSize) + && this.mResizeHandleThickness == other.mResizeHandleThickness + && this.mFineTaskCorners.equals(other.mFineTaskCorners) + && this.mLargeTaskCorners.equals(other.mLargeTaskCorners) + && this.mTopEdgeBounds.equals(other.mTopEdgeBounds) + && this.mLeftEdgeBounds.equals(other.mLeftEdgeBounds) + && this.mRightEdgeBounds.equals(other.mRightEdgeBounds) + && this.mBottomEdgeBounds.equals(other.mBottomEdgeBounds); + } + + @Override + public int hashCode() { + return Objects.hash( + mTaskCornerRadius, + mTaskSize, + mResizeHandleThickness, + mFineTaskCorners, + mLargeTaskCorners, + mTopEdgeBounds, + mLeftEdgeBounds, + mRightEdgeBounds, + mBottomEdgeBounds); + } + + /** + * Representation of the drag resize regions at the corner of the window. + */ + private static class TaskCorners { + // The size of the square applied to the corners of the window, for the user to drag + // resize. + private final int mCornerSize; + // The square for each corner. + private final @NonNull Rect mLeftTopCornerBounds; + private final @NonNull Rect mRightTopCornerBounds; + private final @NonNull Rect mLeftBottomCornerBounds; + private final @NonNull Rect mRightBottomCornerBounds; + + TaskCorners(@NonNull Size taskSize, int cornerSize) { + mCornerSize = cornerSize; + final int cornerRadius = cornerSize / 2; + mLeftTopCornerBounds = new Rect( + -cornerRadius, + -cornerRadius, + cornerRadius, + cornerRadius); + + mRightTopCornerBounds = new Rect( + taskSize.getWidth() - cornerRadius, + -cornerRadius, + taskSize.getWidth() + cornerRadius, + cornerRadius); + + mLeftBottomCornerBounds = new Rect( + -cornerRadius, + taskSize.getHeight() - cornerRadius, + cornerRadius, + taskSize.getHeight() + cornerRadius); + + mRightBottomCornerBounds = new Rect( + taskSize.getWidth() - cornerRadius, + taskSize.getHeight() - cornerRadius, + taskSize.getWidth() + cornerRadius, + taskSize.getHeight() + cornerRadius); + } + + /** + * Updates the region to include all four corners. + */ + void union(Region region) { + region.union(mLeftTopCornerBounds); + region.union(mRightTopCornerBounds); + region.union(mLeftBottomCornerBounds); + region.union(mRightBottomCornerBounds); + } + + /** + * Returns the control type based on the position of the {@code MotionEvent}'s coordinates. + */ + @DragPositioningCallback.CtrlType + int calculateCornersCtrlType(float x, float y) { + int xi = (int) x; + int yi = (int) y; + if (mLeftTopCornerBounds.contains(xi, yi)) { + return CTRL_TYPE_LEFT | CTRL_TYPE_TOP; + } + if (mLeftBottomCornerBounds.contains(xi, yi)) { + return CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM; + } + if (mRightTopCornerBounds.contains(xi, yi)) { + return CTRL_TYPE_RIGHT | CTRL_TYPE_TOP; + } + if (mRightBottomCornerBounds.contains(xi, yi)) { + return CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM; + } + return 0; + } + + @Override + public String toString() { + return "TaskCorners of size " + mCornerSize + " for the" + + " top left " + mLeftTopCornerBounds + + " top right " + mRightTopCornerBounds + + " bottom left " + mLeftBottomCornerBounds + + " bottom right " + mRightBottomCornerBounds; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof TaskCorners other)) return false; + + return this.mCornerSize == other.mCornerSize + && this.mLeftTopCornerBounds.equals(other.mLeftTopCornerBounds) + && this.mRightTopCornerBounds.equals(other.mRightTopCornerBounds) + && this.mLeftBottomCornerBounds.equals(other.mLeftBottomCornerBounds) + && this.mRightBottomCornerBounds.equals(other.mRightBottomCornerBounds); + } + + @Override + public int hashCode() { + return Objects.hash( + mCornerSize, + mLeftTopCornerBounds, + mRightTopCornerBounds, + mLeftBottomCornerBounds, + mRightBottomCornerBounds); + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp index 32c070305e05..13f95ccea640 100644 --- a/libs/WindowManager/Shell/tests/unittest/Android.bp +++ b/libs/WindowManager/Shell/tests/unittest/Android.bp @@ -39,7 +39,7 @@ android_test { static_libs: [ "WindowManager-Shell", "junit", - "flag-junit-base", + "flag-junit", "androidx.test.runner", "androidx.test.rules", "androidx.test.ext.junit", @@ -55,6 +55,9 @@ android_test { "platform-test-annotations", "servicestests-utils", "com_android_wm_shell_flags_lib", + "guava-android-testlib", + "com.android.window.flags.window-aconfig-java", + "platform-test-annotations", ], libs: [ diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java new file mode 100644 index 000000000000..82e5a1cd25ce --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2024 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 static com.google.common.truth.Truth.assertThat; + +import android.annotation.NonNull; +import android.graphics.Point; +import android.graphics.Region; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.testing.AndroidTestingRunner; +import android.util.Size; + +import androidx.test.filters.SmallTest; + +import com.android.window.flags.Flags; + +import com.google.common.testing.EqualsTester; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for {@link DragResizeWindowGeometry}. + * + * Build/Install/Run: + * atest WMShellUnitTests:DragResizeWindowGeometryTests + */ +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class DragResizeWindowGeometryTests { + private static final Size TASK_SIZE = new Size(500, 1000); + private static final int TASK_CORNER_RADIUS = 10; + private static final int EDGE_RESIZE_THICKNESS = 15; + private static final int FINE_CORNER_SIZE = EDGE_RESIZE_THICKNESS * 2 + 10; + private static final int LARGE_CORNER_SIZE = FINE_CORNER_SIZE + 10; + private static final DragResizeWindowGeometry GEOMETRY = new DragResizeWindowGeometry( + TASK_CORNER_RADIUS, TASK_SIZE, EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE, + LARGE_CORNER_SIZE); + // Points in the edge resize handle. Note that coordinates start from the top left. + private static final Point TOP_EDGE_POINT = new Point(TASK_SIZE.getWidth() / 2, + -EDGE_RESIZE_THICKNESS / 2); + private static final Point LEFT_EDGE_POINT = new Point(-EDGE_RESIZE_THICKNESS / 2, + TASK_SIZE.getHeight() / 2); + private static final Point RIGHT_EDGE_POINT = new Point( + TASK_SIZE.getWidth() + EDGE_RESIZE_THICKNESS / 2, TASK_SIZE.getHeight() / 2); + private static final Point BOTTOM_EDGE_POINT = new Point(TASK_SIZE.getWidth() / 2, + TASK_SIZE.getHeight() + EDGE_RESIZE_THICKNESS / 2); + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + /** + * Check that both groups of objects satisfy equals/hashcode within each group, and that each + * group is distinct from the next. + */ + @Test + public void testEqualsAndHash() { + new EqualsTester() + .addEqualityGroup( + GEOMETRY, + new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE, + EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE, LARGE_CORNER_SIZE)) + .addEqualityGroup( + new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE, + EDGE_RESIZE_THICKNESS + 10, FINE_CORNER_SIZE, LARGE_CORNER_SIZE), + new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE, + EDGE_RESIZE_THICKNESS + 10, FINE_CORNER_SIZE, LARGE_CORNER_SIZE)) + .addEqualityGroup( + new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE, + EDGE_RESIZE_THICKNESS + 10, FINE_CORNER_SIZE, + LARGE_CORNER_SIZE + 5), + new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE, + EDGE_RESIZE_THICKNESS + 10, FINE_CORNER_SIZE, + LARGE_CORNER_SIZE + 5)) + .testEquals(); + } + + @Test + public void testGetTaskSize() { + assertThat(GEOMETRY.getTaskSize()).isEqualTo(TASK_SIZE); + } + + @Test + public void testRegionUnionContainsEdges() { + Region region = new Region(); + GEOMETRY.union(region); + assertThat(region.isComplex()).isTrue(); + // Region excludes task area. Note that coordinates start from top left. + assertThat(region.contains(TASK_SIZE.getWidth() / 2, TASK_SIZE.getHeight() / 2)).isFalse(); + // Region includes edges outside the task window. + verifyVerticalEdge(region, LEFT_EDGE_POINT); + verifyHorizontalEdge(region, TOP_EDGE_POINT); + verifyVerticalEdge(region, RIGHT_EDGE_POINT); + verifyHorizontalEdge(region, BOTTOM_EDGE_POINT); + } + + private static void verifyHorizontalEdge(@NonNull Region region, @NonNull Point point) { + assertThat(region.contains(point.x, point.y)).isTrue(); + // Horizontally along the edge is still contained. + assertThat(region.contains(point.x + EDGE_RESIZE_THICKNESS, point.y)).isTrue(); + assertThat(region.contains(point.x - EDGE_RESIZE_THICKNESS, point.y)).isTrue(); + // Vertically along the edge is not contained. + assertThat(region.contains(point.x, point.y - EDGE_RESIZE_THICKNESS)).isFalse(); + assertThat(region.contains(point.x, point.y + EDGE_RESIZE_THICKNESS)).isFalse(); + } + + private static void verifyVerticalEdge(@NonNull Region region, @NonNull Point point) { + assertThat(region.contains(point.x, point.y)).isTrue(); + // Horizontally along the edge is not contained. + assertThat(region.contains(point.x + EDGE_RESIZE_THICKNESS, point.y)).isFalse(); + assertThat(region.contains(point.x - EDGE_RESIZE_THICKNESS, point.y)).isFalse(); + // Vertically along the edge is contained. + assertThat(region.contains(point.x, point.y - EDGE_RESIZE_THICKNESS)).isTrue(); + assertThat(region.contains(point.x, point.y + EDGE_RESIZE_THICKNESS)).isTrue(); + } + + /** + * Validate that with the flag enabled, the corner resize regions are the largest size, to + * capture all eligible input regardless of source (touch or cursor). + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + public void testRegionUnion_edgeDragResizeEnabled_containsLargeCorners() { + Region region = new Region(); + GEOMETRY.union(region); + final int cornerRadius = LARGE_CORNER_SIZE / 2; + + new TestPoints(TASK_SIZE, cornerRadius).validateRegion(region); + } + + /** + * Validate that with the flag disabled, the corner resize regions are the original smaller + * size. + */ + @Test + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + public void testRegionUnion_edgeDragResizeDisabled_containsFineCorners() { + Region region = new Region(); + GEOMETRY.union(region); + final int cornerRadius = FINE_CORNER_SIZE / 2; + + new TestPoints(TASK_SIZE, cornerRadius).validateRegion(region); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + public void testCalculateControlType_edgeDragResizeEnabled_edges() { + // The input source (touch or cursor) shouldn't impact the edge resize size. + validateCtrlTypeForEdges(/* isTouch= */ false); + validateCtrlTypeForEdges(/* isTouch= */ true); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + public void testCalculateControlType_edgeDragResizeDisabled_edges() { + // Edge resizing is not supported when the flag is disabled. + validateCtrlTypeForEdges(/* isTouch= */ false); + validateCtrlTypeForEdges(/* isTouch= */ false); + } + + private void validateCtrlTypeForEdges(boolean isTouch) { + assertThat(GEOMETRY.calculateCtrlType(isTouch, LEFT_EDGE_POINT.x, + LEFT_EDGE_POINT.y)).isEqualTo(CTRL_TYPE_LEFT); + assertThat(GEOMETRY.calculateCtrlType(isTouch, TOP_EDGE_POINT.x, + TOP_EDGE_POINT.y)).isEqualTo(CTRL_TYPE_TOP); + assertThat(GEOMETRY.calculateCtrlType(isTouch, RIGHT_EDGE_POINT.x, + RIGHT_EDGE_POINT.y)).isEqualTo(CTRL_TYPE_RIGHT); + assertThat(GEOMETRY.calculateCtrlType(isTouch, BOTTOM_EDGE_POINT.x, + BOTTOM_EDGE_POINT.y)).isEqualTo(CTRL_TYPE_BOTTOM); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + public void testCalculateControlType_edgeDragResizeEnabled_corners() { + final TestPoints fineTestPoints = new TestPoints(TASK_SIZE, FINE_CORNER_SIZE / 2); + final TestPoints largeCornerTestPoints = new TestPoints(TASK_SIZE, LARGE_CORNER_SIZE / 2); + + // When the flag is enabled, points within fine corners should pass regardless of touch or + // not. Points outside fine corners should not pass when using a course input (non-touch). + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ true, true); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ true, true); + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ false, true); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ false, false); + + // When the flag is enabled, points near the large corners should only pass when the point + // is within the corner for large touch inputs. + largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ true, true); + largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ true, + false); + largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ false, false); + largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ false, + false); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE) + public void testCalculateControlType_edgeDragResizeDisabled_corners() { + final TestPoints fineTestPoints = new TestPoints(TASK_SIZE, FINE_CORNER_SIZE / 2); + final TestPoints largeCornerTestPoints = new TestPoints(TASK_SIZE, LARGE_CORNER_SIZE / 2); + + // When the flag is disabled, points within fine corners should pass only when touch. + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ true, true); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ true, false); + fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ false, false); + fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ false, false); + + // When the flag is disabled, points near the large corners should never pass. + largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ true, false); + largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ true, + false); + largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ false, false); + largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ false, + false); + } + + /** + * Class for creating points for testing the drag resize corners. + * + * <p>Creates points that are both just within the bounds of each corner, and just outside. + */ + private static final class TestPoints { + private final Point mTopLeftPoint; + private final Point mTopLeftPointOutside; + private final Point mTopRightPoint; + private final Point mTopRightPointOutside; + private final Point mBottomLeftPoint; + private final Point mBottomLeftPointOutside; + private final Point mBottomRightPoint; + private final Point mBottomRightPointOutside; + + TestPoints(@NonNull Size taskSize, int cornerRadius) { + // Point just inside corner square is included. + mTopLeftPoint = new Point(-cornerRadius + 1, -cornerRadius + 1); + // Point just outside corner square is excluded. + mTopLeftPointOutside = new Point(mTopLeftPoint.x - 5, mTopLeftPoint.y - 5); + + mTopRightPoint = new Point(taskSize.getWidth() + cornerRadius - 1, -cornerRadius + 1); + mTopRightPointOutside = new Point(mTopRightPoint.x + 5, mTopRightPoint.y - 5); + + mBottomLeftPoint = new Point(-cornerRadius + 1, + taskSize.getHeight() + cornerRadius - 1); + mBottomLeftPointOutside = new Point(mBottomLeftPoint.x - 5, mBottomLeftPoint.y + 5); + + mBottomRightPoint = new Point(taskSize.getWidth() + cornerRadius - 1, + taskSize.getHeight() + cornerRadius - 1); + mBottomRightPointOutside = new Point(mBottomRightPoint.x + 5, mBottomRightPoint.y + 5); + } + + /** + * Validates that all test points are either within or without the given region. + */ + public void validateRegion(@NonNull Region region) { + // Point just inside corner square is included. + assertThat(region.contains(mTopLeftPoint.x, mTopLeftPoint.y)).isTrue(); + // Point just outside corner square is excluded. + assertThat(region.contains(mTopLeftPointOutside.x, mTopLeftPointOutside.y)).isFalse(); + + assertThat(region.contains(mTopRightPoint.x, mTopRightPoint.y)).isTrue(); + assertThat( + region.contains(mTopRightPointOutside.x, mTopRightPointOutside.y)).isFalse(); + + assertThat(region.contains(mBottomLeftPoint.x, mBottomLeftPoint.y)).isTrue(); + assertThat(region.contains(mBottomLeftPointOutside.x, + mBottomLeftPointOutside.y)).isFalse(); + + assertThat(region.contains(mBottomRightPoint.x, mBottomRightPoint.y)).isTrue(); + assertThat(region.contains(mBottomRightPointOutside.x, + mBottomRightPointOutside.y)).isFalse(); + } + + /** + * Validates that all test points within this drag corner size give the correct + * {@code @DragPositioningCallback.CtrlType}. + */ + public void validateCtrlTypeForInnerPoints(@NonNull DragResizeWindowGeometry geometry, + boolean isTouch, boolean expectedWithinGeometry) { + assertThat(geometry.calculateCtrlType(isTouch, mTopLeftPoint.x, + mTopLeftPoint.y)).isEqualTo( + expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED); + assertThat(geometry.calculateCtrlType(isTouch, mTopRightPoint.x, + mTopRightPoint.y)).isEqualTo( + expectedWithinGeometry ? CTRL_TYPE_RIGHT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED); + assertThat(geometry.calculateCtrlType(isTouch, mBottomLeftPoint.x, + mBottomLeftPoint.y)).isEqualTo( + expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM + : CTRL_TYPE_UNDEFINED); + assertThat(geometry.calculateCtrlType(isTouch, mBottomRightPoint.x, + mBottomRightPoint.y)).isEqualTo( + expectedWithinGeometry ? CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM + : CTRL_TYPE_UNDEFINED); + } + + /** + * Validates that all test points outside this drag corner size give the correct + * {@code @DragPositioningCallback.CtrlType}. + */ + public void validateCtrlTypeForOutsidePoints(@NonNull DragResizeWindowGeometry geometry, + boolean isTouch, boolean expectedWithinGeometry) { + assertThat(geometry.calculateCtrlType(isTouch, mTopLeftPointOutside.x, + mTopLeftPointOutside.y)).isEqualTo( + expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED); + assertThat(geometry.calculateCtrlType(isTouch, mTopRightPointOutside.x, + mTopRightPointOutside.y)).isEqualTo( + expectedWithinGeometry ? CTRL_TYPE_RIGHT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED); + assertThat(geometry.calculateCtrlType(isTouch, mBottomLeftPointOutside.x, + mBottomLeftPointOutside.y)).isEqualTo( + expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM + : CTRL_TYPE_UNDEFINED); + assertThat(geometry.calculateCtrlType(isTouch, mBottomRightPointOutside.x, + mBottomRightPointOutside.y)).isEqualTo( + expectedWithinGeometry ? CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM + : CTRL_TYPE_UNDEFINED); + } + } +} |