Adding orientation preserving resizing

Adding freeform resizing to activities which require
a certain orientation. This is needed for e.g. ARC++.

Bug: 33267688
Test: runtest frameworks-services -c com.android.server.wm.TaskPositionerTests
Test: Visually on ARC++
Change-Id: If708c1602cb2ff464174389af4648ad767b0b079
diff --git a/services/core/java/com/android/server/wm/TaskPositioner.java b/services/core/java/com/android/server/wm/TaskPositioner.java
index 6887312..267566b 100644
--- a/services/core/java/com/android/server/wm/TaskPositioner.java
+++ b/services/core/java/com/android/server/wm/TaskPositioner.java
@@ -50,9 +50,9 @@
 import android.view.InputDevice;
 import android.view.InputEvent;
 import android.view.MotionEvent;
-import android.view.SurfaceControl;
 import android.view.WindowManager;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.input.InputApplicationHandle;
 import com.android.server.input.InputWindowHandle;
 import com.android.server.wm.WindowManagerService.H;
@@ -61,6 +61,7 @@
 import java.lang.annotation.RetentionPolicy;
 
 class TaskPositioner implements DimLayer.DimLayerUser {
+    private static final boolean DEBUG_ORIENTATION_VIOLATIONS = false;
     private static final String TAG_LOCAL = "TaskPositioner";
     private static final String TAG = TAG_WITH_CLASS_NAME ? TAG_LOCAL : TAG_WM;
 
@@ -89,6 +90,12 @@
 
     public static final int RESIZING_HINT_DURATION_MS = 0;
 
+    // The minimal aspect ratio which needs to be met to count as landscape (or 1/.. for portrait).
+    // Note: We do not use the 1.33 from the CDD here since the user is allowed to use what ever
+    // aspect he desires.
+    @VisibleForTesting
+    static final float MIN_ASPECT = 1.2f;
+
     private final WindowManagerService mService;
     private WindowPositionerEventReceiver mInputEventReceiver;
     private Display mDisplay;
@@ -103,8 +110,11 @@
 
     private Task mTask;
     private boolean mResizing;
+    private boolean mPreserveOrientation;
+    private boolean mStartOrientationWasLandscape;
     private final Rect mWindowOriginalBounds = new Rect();
     private final Rect mWindowDragBounds = new Rect();
+    private final Point mMaxVisibleSize = new Point();
     private float mStartDragX;
     private float mStartDragY;
     @CtrlType
@@ -226,6 +236,11 @@
         mService = service;
     }
 
+    @VisibleForTesting
+    Rect getWindowDragBounds() {
+        return mWindowDragBounds;
+    }
+
     /**
      * @param display The Display that the window being dragged is on.
      */
@@ -294,6 +309,7 @@
         mSideMargin = dipToPixel(SIDE_MARGIN_DIP, mDisplayMetrics);
         mMinVisibleWidth = dipToPixel(MINIMUM_VISIBLE_WIDTH_IN_DP, mDisplayMetrics);
         mMinVisibleHeight = dipToPixel(MINIMUM_VISIBLE_HEIGHT_IN_DP, mDisplayMetrics);
+        mDisplay.getRealSize(mMaxVisibleSize);
 
         mDragEnded = false;
     }
@@ -335,44 +351,57 @@
         mService.resumeRotationLocked();
     }
 
-    void startDragLocked(WindowState win, boolean resize, float startX, float startY) {
+    void startDrag(WindowState win, boolean resize, boolean preserveOrientation, float startX,
+                   float startY) {
         if (DEBUG_TASK_POSITIONING) {
-            Slog.d(TAG, "startDragLocked: win=" + win + ", resize=" + resize
-                + ", {" + startX + ", " + startY + "}");
+            Slog.d(TAG, "startDrag: win=" + win + ", resize=" + resize
+                    + ", preserveOrientation=" + preserveOrientation + ", {" + startX + ", "
+                    + startY + "}");
         }
-        mCtrlType = CTRL_NONE;
         mTask = win.getTask();
-        mStartDragX = startX;
-        mStartDragY = startY;
-
         // Use the dim bounds, not the original task bounds. The cursor
         // movement should be calculated relative to the visible bounds.
         // Also, use the dim bounds of the task which accounts for
         // multiple app windows. Don't use any bounds from win itself as it
         // may not be the same size as the task.
         mTask.getDimBounds(mTmpRect);
+        startDrag(resize, preserveOrientation, startX, startY, mTmpRect);
+    }
+
+    @VisibleForTesting
+    void startDrag(boolean resize, boolean preserveOrientation,
+                   float startX, float startY, Rect startBounds) {
+        mCtrlType = CTRL_NONE;
+        mStartDragX = startX;
+        mStartDragY = startY;
+        mPreserveOrientation = preserveOrientation;
 
         if (resize) {
-            if (startX < mTmpRect.left) {
+            if (startX < startBounds.left) {
                 mCtrlType |= CTRL_LEFT;
             }
-            if (startX > mTmpRect.right) {
+            if (startX > startBounds.right) {
                 mCtrlType |= CTRL_RIGHT;
             }
-            if (startY < mTmpRect.top) {
+            if (startY < startBounds.top) {
                 mCtrlType |= CTRL_TOP;
             }
-            if (startY > mTmpRect.bottom) {
+            if (startY > startBounds.bottom) {
                 mCtrlType |= CTRL_BOTTOM;
             }
-            mResizing = true;
+            mResizing = mCtrlType != CTRL_NONE;
         }
 
-        mWindowOriginalBounds.set(mTmpRect);
+        // In case of !isDockedInEffect we are using the union of all task bounds. These might be
+        // made up out of multiple windows which are only partially overlapping. When that happens,
+        // the orientation from the window of interest to the entire stack might diverge. However
+        // for now we treat them as the same.
+        mStartOrientationWasLandscape = startBounds.width() >= startBounds.height();
+        mWindowOriginalBounds.set(startBounds);
 
         // Make sure we always have valid drag bounds even if the drag ends before any move events
         // have been handled.
-        mWindowDragBounds.set(mTmpRect);
+        mWindowDragBounds.set(startBounds);
     }
 
     private void endDragLocked() {
@@ -387,26 +416,7 @@
         }
 
         if (mCtrlType != CTRL_NONE) {
-            // This is a resizing operation.
-            final int deltaX = Math.round(x - mStartDragX);
-            final int deltaY = Math.round(y - mStartDragY);
-            int left = mWindowOriginalBounds.left;
-            int top = mWindowOriginalBounds.top;
-            int right = mWindowOriginalBounds.right;
-            int bottom = mWindowOriginalBounds.bottom;
-            if ((mCtrlType & CTRL_LEFT) != 0) {
-                left = Math.min(left + deltaX, right - mMinVisibleWidth);
-            }
-            if ((mCtrlType & CTRL_TOP) != 0) {
-                top = Math.min(top + deltaY, bottom - mMinVisibleHeight);
-            }
-            if ((mCtrlType & CTRL_RIGHT) != 0) {
-                right = Math.max(left + mMinVisibleWidth, right + deltaX);
-            }
-            if ((mCtrlType & CTRL_BOTTOM) != 0) {
-                bottom = Math.max(top + mMinVisibleHeight, bottom + deltaY);
-            }
-            mWindowDragBounds.set(left, top, right, bottom);
+            resizeDrag(x, y);
             mTask.setDragResizing(true, DRAG_RESIZE_MODE_FREEFORM);
             return false;
         }
@@ -428,6 +438,168 @@
         return false;
     }
 
+    /**
+     * The user is drag - resizing the window.
+     *
+     * @param x The x coordinate of the current drag coordinate.
+     * @param y the y coordinate of the current drag coordinate.
+     */
+    @VisibleForTesting
+    void resizeDrag(float x, float y) {
+        // This is a resizing operation.
+        // We need to keep various constraints:
+        // 1. mMinVisible[Width/Height] <= [width/height] <= mMaxVisibleSize.[x/y]
+        // 2. The orientation is kept - if required.
+        final int deltaX = Math.round(x - mStartDragX);
+        final int deltaY = Math.round(y - mStartDragY);
+        int left = mWindowOriginalBounds.left;
+        int top = mWindowOriginalBounds.top;
+        int right = mWindowOriginalBounds.right;
+        int bottom = mWindowOriginalBounds.bottom;
+
+        // The aspect which we have to respect. Note that if the orientation does not need to be
+        // preserved the aspect will be calculated as 1.0 which neutralizes the following
+        // computations.
+        final float minAspect = !mPreserveOrientation
+                ? 1.0f
+                : (mStartOrientationWasLandscape ? MIN_ASPECT : (1.0f / MIN_ASPECT));
+        // Calculate the resulting width and height of the drag operation.
+        int width = right - left;
+        int height = bottom - top;
+        if ((mCtrlType & CTRL_LEFT) != 0) {
+            width = Math.max(mMinVisibleWidth, width - deltaX);
+        } else if ((mCtrlType & CTRL_RIGHT) != 0) {
+            width = Math.max(mMinVisibleWidth, width + deltaX);
+        }
+        if ((mCtrlType & CTRL_TOP) != 0) {
+            height = Math.max(mMinVisibleHeight, height - deltaY);
+        } else if ((mCtrlType & CTRL_BOTTOM) != 0) {
+            height = Math.max(mMinVisibleHeight, height + deltaY);
+        }
+
+        // If we have to preserve the orientation - check that we are doing so.
+        final float aspect = (float) width / (float) height;
+        if (mPreserveOrientation && ((mStartOrientationWasLandscape && aspect < MIN_ASPECT)
+                || (!mStartOrientationWasLandscape && aspect > (1.0 / MIN_ASPECT)))) {
+            // Calculate 2 rectangles fulfilling all requirements for either X or Y being the major
+            // drag axis. What ever is producing the bigger rectangle will be chosen.
+            int width1;
+            int width2;
+            int height1;
+            int height2;
+            if (mStartOrientationWasLandscape) {
+                // Assuming that the width is our target we calculate the height.
+                width1 = Math.max(mMinVisibleWidth, Math.min(mMaxVisibleSize.x, width));
+                height1 = Math.min(height, Math.round((float)width1 / MIN_ASPECT));
+                if (height1 < mMinVisibleHeight) {
+                    // If the resulting height is too small we adjust to the minimal size.
+                    height1 = mMinVisibleHeight;
+                    width1 = Math.max(mMinVisibleWidth,
+                            Math.min(mMaxVisibleSize.x, Math.round((float)height1 * MIN_ASPECT)));
+                }
+                // Assuming that the height is our target we calculate the width.
+                height2 = Math.max(mMinVisibleHeight, Math.min(mMaxVisibleSize.y, height));
+                width2 = Math.max(width, Math.round((float)height2 * MIN_ASPECT));
+                if (width2 < mMinVisibleWidth) {
+                    // If the resulting width is too small we adjust to the minimal size.
+                    width2 = mMinVisibleWidth;
+                    height2 = Math.max(mMinVisibleHeight,
+                            Math.min(mMaxVisibleSize.y, Math.round((float)width2 / MIN_ASPECT)));
+                }
+            } else {
+                // Assuming that the width is our target we calculate the height.
+                width1 = Math.max(mMinVisibleWidth, Math.min(mMaxVisibleSize.x, width));
+                height1 = Math.max(height, Math.round((float)width1 * MIN_ASPECT));
+                if (height1 < mMinVisibleHeight) {
+                    // If the resulting height is too small we adjust to the minimal size.
+                    height1 = mMinVisibleHeight;
+                    width1 = Math.max(mMinVisibleWidth,
+                            Math.min(mMaxVisibleSize.x, Math.round((float)height1 / MIN_ASPECT)));
+                }
+                // Assuming that the height is our target we calculate the width.
+                height2 = Math.max(mMinVisibleHeight, Math.min(mMaxVisibleSize.y, height));
+                width2 = Math.min(width, Math.round((float)height2 / MIN_ASPECT));
+                if (width2 < mMinVisibleWidth) {
+                    // If the resulting width is too small we adjust to the minimal size.
+                    width2 = mMinVisibleWidth;
+                    height2 = Math.max(mMinVisibleHeight,
+                            Math.min(mMaxVisibleSize.y, Math.round((float)width2 * MIN_ASPECT)));
+                }
+            }
+
+            // Use the bigger of the two rectangles if the major change was positive, otherwise
+            // do the opposite.
+            final boolean grows = width > (right - left) || height > (bottom - top);
+            if (grows == (width1 * height1 > width2 * height2)) {
+                width = width1;
+                height = height1;
+            } else {
+                width = width2;
+                height = height2;
+            }
+        }
+
+        // Update mWindowDragBounds to the new drag size.
+        updateDraggedBounds(left, top, right, bottom, width, height);
+    }
+
+    /**
+     * Given the old coordinates and the new width and height, update the mWindowDragBounds.
+     *
+     * @param left      The original left bound before the user started dragging.
+     * @param top       The original top bound before the user started dragging.
+     * @param right     The original right bound before the user started dragging.
+     * @param bottom    The original bottom bound before the user started dragging.
+     * @param newWidth  The new dragged width.
+     * @param newHeight The new dragged height.
+     */
+    void updateDraggedBounds(int left, int top, int right, int bottom, int newWidth,
+                             int newHeight) {
+        // Generate the final bounds by keeping the opposite drag edge constant.
+        if ((mCtrlType & CTRL_LEFT) != 0) {
+            left = right - newWidth;
+        } else { // Note: The right might have changed - if we pulled at the right or not.
+            right = left + newWidth;
+        }
+        if ((mCtrlType & CTRL_TOP) != 0) {
+            top = bottom - newHeight;
+        } else { // Note: The height might have changed - if we pulled at the bottom or not.
+            bottom = top + newHeight;
+        }
+
+        mWindowDragBounds.set(left, top, right, bottom);
+
+        checkBoundsForOrientationViolations(mWindowDragBounds);
+    }
+
+    /**
+     * Validate bounds against orientation violations (if DEBUG_ORIENTATION_VIOLATIONS is set).
+     *
+     * @param bounds The bounds to be checked.
+     */
+    private void checkBoundsForOrientationViolations(Rect bounds) {
+        // When using debug check that we are not violating the given constraints.
+        if (DEBUG_ORIENTATION_VIOLATIONS) {
+            if (mStartOrientationWasLandscape != (bounds.width() >= bounds.height())) {
+                Slog.e(TAG, "Orientation violation detected! should be "
+                        + (mStartOrientationWasLandscape ? "landscape" : "portrait")
+                        + " but is the other");
+            } else {
+                Slog.v(TAG, "new bounds size: " + bounds.width() + " x " + bounds.height());
+            }
+            if (mMinVisibleWidth > bounds.width() || mMinVisibleHeight > bounds.height()) {
+                Slog.v(TAG, "Minimum requirement violated: Width(min, is)=(" + mMinVisibleWidth
+                        + ", " + bounds.width() + ") Height(min,is)=("
+                        + mMinVisibleHeight + ", " + bounds.height() + ")");
+            }
+            if (mMaxVisibleSize.x < bounds.width() || mMaxVisibleSize.y < bounds.height()) {
+                Slog.v(TAG, "Maximum requirement violated: Width(min, is)=(" + mMaxVisibleSize.x
+                        + ", " + bounds.width() + ") Height(min,is)=("
+                        + mMaxVisibleSize.y + ", " + bounds.height() + ")");
+            }
+        }
+    }
+
     private void updateWindowDragBounds(int x, int y, Rect stackBounds) {
         final int offsetX = Math.round(x - mStartDragX);
         final int offsetY = Math.round(y - mStartDragY);