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/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index dde948f..c7aa5f2 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -2322,6 +2322,11 @@
             return true;
         }
 
+        if (!task.canResizeToBounds(bounds)) {
+            throw new IllegalArgumentException("resizeTaskLocked: Can not resize task=" + task
+                    + " to bounds=" + bounds + " resizeMode=" + task.mResizeMode);
+        }
+
         // Do not move the task to another stack here.
         // This method assumes that the task is already placed in the right stack.
         // we do not mess with that decision and we only do the resize!
diff --git a/services/core/java/com/android/server/am/TaskRecord.java b/services/core/java/com/android/server/am/TaskRecord.java
index 5c352e1..383f106 100644
--- a/services/core/java/com/android/server/am/TaskRecord.java
+++ b/services/core/java/com/android/server/am/TaskRecord.java
@@ -73,6 +73,9 @@
 import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_DEFAULT;
 import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_IF_WHITELISTED;
 import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_NEVER;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_LANDSCAPE_ONLY;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_PORTRAIT_ONLY;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_PRESERVE_ORIENTATION;
 import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZEABLE;
 import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
 import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION;
@@ -1077,12 +1080,32 @@
                 && !mTemporarilyUnresizable;
     }
 
+    /**
+     * Check that a given bounds matches the application requested orientation.
+     *
+     * @param bounds The bounds to be tested.
+     * @return True if the requested bounds are okay for a resizing request.
+     */
+    boolean canResizeToBounds(Rect bounds) {
+        if (bounds == null || getStackId() != FREEFORM_WORKSPACE_STACK_ID) {
+            // Note: If not on the freeform workspace, we ignore the bounds.
+            return true;
+        }
+        final boolean landscape = bounds.width() > bounds.height();
+        if (mResizeMode == RESIZE_MODE_FORCE_RESIZABLE_PRESERVE_ORIENTATION) {
+            return mBounds == null || landscape == (mBounds.width() > mBounds.height());
+        }
+        return (mResizeMode != RESIZE_MODE_FORCE_RESIZABLE_PORTRAIT_ONLY || !landscape)
+                && (mResizeMode != RESIZE_MODE_FORCE_RESIZABLE_LANDSCAPE_ONLY || landscape);
+    }
+
     boolean isOnTopLauncher() {
         return isHomeTask() && mIsOnTopLauncher;
     }
 
     boolean canGoInDockedStack() {
-        return isResizeable();
+        return isResizeable() &&
+                !ActivityInfo.isPreserveOrientationMode(mResizeMode);
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 29afc8d..612af75 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -19,7 +19,9 @@
 import static android.app.ActivityManager.RESIZE_MODE_SYSTEM_SCREEN_ROTATION;
 import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
 import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
-
+import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_LANDSCAPE_ONLY;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_PRESERVE_ORIENTATION;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_PORTRAIT_ONLY;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_STACK;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
@@ -280,6 +282,17 @@
         return ActivityInfo.isResizeableMode(mResizeMode) || mService.mForceResizableTasks;
     }
 
+    /**
+     * Tests if the orientation should be preserved upon user interactive resizig operations.
+
+     * @return true if orientation should not get changed upon resizing operation.
+     */
+    boolean preserveOrientationOnResize() {
+        return mResizeMode == RESIZE_MODE_FORCE_RESIZABLE_PORTRAIT_ONLY
+                || mResizeMode == RESIZE_MODE_FORCE_RESIZABLE_LANDSCAPE_ONLY
+                || mResizeMode == RESIZE_MODE_FORCE_RESIZABLE_PRESERVE_ORIENTATION;
+    }
+
     boolean isOnTopLauncher() {
         return mIsOnTopLauncher;
     }
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);
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 04cf2e3..b3d1b13 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -5726,7 +5726,8 @@
             win = windowForClientLocked(null, window, false);
             // win shouldn't be null here, pass it down to startPositioningLocked
             // to get warning if it's null.
-            if (!startPositioningLocked(win, false /*resize*/, startX, startY)) {
+            if (!startPositioningLocked(
+                        win, false /*resize*/, false /*preserveOrientation*/, startX, startY)) {
                 return false;
             }
         }
@@ -5741,8 +5742,8 @@
         synchronized (mWindowMap) {
             final Task task = displayContent.findTaskForResizePoint(x, y);
             if (task != null) {
-                if (!startPositioningLocked(
-                        task.getTopVisibleAppMainWindow(), true /*resize*/, x, y)) {
+                if (!startPositioningLocked(task.getTopVisibleAppMainWindow(), true /*resize*/,
+                            task.preserveOrientationOnResize(), x, y)) {
                     return;
                 }
                 taskId = task.mTaskId;
@@ -5757,10 +5758,12 @@
         }
     }
 
-    private boolean startPositioningLocked(
-            WindowState win, boolean resize, float startX, float startY) {
-        if (DEBUG_TASK_POSITIONING) Slog.d(TAG_WM, "startPositioningLocked: "
-            + "win=" + win + ", resize=" + resize + ", {" + startX + ", " + startY + "}");
+    private boolean startPositioningLocked(WindowState win, boolean resize,
+            boolean preserveOrientation, float startX, float startY) {
+        if (DEBUG_TASK_POSITIONING)
+            Slog.d(TAG_WM, "startPositioningLocked: "
+                            + "win=" + win + ", resize=" + resize + ", preserveOrientation="
+                            + preserveOrientation + ", {" + startX + ", " + startY + "}");
 
         if (win == null || win.getAppToken() == null) {
             Slog.w(TAG_WM, "startPositioningLocked: Bad window " + win);
@@ -5801,7 +5804,7 @@
             return false;
         }
 
-        mTaskPositioner.startDragLocked(win, resize, startX, startY);
+        mTaskPositioner.startDrag(win, resize, preserveOrientation, startX, startY);
         return true;
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/wm/TaskPositionerTests.java b/services/tests/servicestests/src/com/android/server/wm/TaskPositionerTests.java
new file mode 100644
index 0000000..aec6dec
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/TaskPositionerTests.java
@@ -0,0 +1,456 @@
+/*
+ * Copyright (C) 2016 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.server.wm;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.graphics.Rect;
+import android.os.Binder;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.Display;
+
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
+import static com.android.server.wm.TaskPositioner.MIN_ASPECT;
+import static com.android.server.wm.WindowManagerService.dipToPixel;
+import static com.android.server.wm.WindowState.MINIMUM_VISIBLE_HEIGHT_IN_DP;
+import static com.android.server.wm.WindowState.MINIMUM_VISIBLE_WIDTH_IN_DP;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for the {@link TaskPositioner} class.
+ *
+ * runtest frameworks-services -c com.android.server.wm.TaskPositionerTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class TaskPositionerTests extends WindowTestsBase {
+
+    private final boolean DEBUGGING = false;
+    private final String TAG = "TaskPositionerTest";
+
+    private final static int MOUSE_DELTA_X = 5;
+    private final static int MOUSE_DELTA_Y = 5;
+
+    private int mMinVisibleWidth;
+    private int mMinVisibleHeight;
+    private TaskPositioner mPositioner;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        final Display display = sDisplayContent.getDisplay();
+        final DisplayMetrics dm = new DisplayMetrics();
+        display.getMetrics(dm);
+
+        // This should be the same calculation as the TaskPositioner uses.
+        mMinVisibleWidth = dipToPixel(MINIMUM_VISIBLE_WIDTH_IN_DP, dm);
+        mMinVisibleHeight = dipToPixel(MINIMUM_VISIBLE_HEIGHT_IN_DP, dm);
+
+        mPositioner = new TaskPositioner(sWm);
+        mPositioner.register(display);
+    }
+
+    /**
+     * This tests that free resizing will allow to change the orientation as well
+     * as does some basic tests (e.g. dragging in Y only will keep X stable).
+     */
+    @Test
+    public void testBasicFreeWindowResizing() throws Exception {
+        final Rect r = new Rect(100, 220, 700, 520);
+        final int midY = (r.top + r.bottom) / 2;
+
+        // Start a drag resize starting upper left.
+        mPositioner.startDrag(true /*resizing*/,
+                false /*preserveOrientation*/, r.left - MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y, r);
+        assertBoundsEquals(r, mPositioner.getWindowDragBounds());
+
+        // Drag to a good landscape size.
+        mPositioner.resizeDrag(0.0f, 0.0f);
+        assertBoundsEquals(new Rect(MOUSE_DELTA_X, MOUSE_DELTA_Y, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+
+        // Drag to a good portrait size.
+        mPositioner.resizeDrag(400.0f, 0.0f);
+        assertBoundsEquals(new Rect(400 + MOUSE_DELTA_X, MOUSE_DELTA_Y, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+
+        // Drag to a too small size for the width.
+        mPositioner.resizeDrag(2000.0f, r.top);
+        assertBoundsEquals(
+                new Rect(r.right - mMinVisibleWidth, r.top + MOUSE_DELTA_Y, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+
+        // Drag to a too small size for the height.
+        mPositioner.resizeDrag(r.left, 2000.0f);
+        assertBoundsEquals(
+                new Rect(r.left + MOUSE_DELTA_X, r.bottom - mMinVisibleHeight, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+
+        // Start a drag resize left and see that only the left coord changes..
+        mPositioner.startDrag(true /*resizing*/,
+                false /*preserveOrientation*/, r.left - MOUSE_DELTA_X, midY, r);
+
+        // Drag to the left.
+        mPositioner.resizeDrag(0.0f, midY);
+        assertBoundsEquals(new Rect(MOUSE_DELTA_X, r.top, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+
+        // Drag to the right.
+        mPositioner.resizeDrag(200.0f, midY);
+        assertBoundsEquals(new Rect(200 + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+
+        // Drag to the top
+        mPositioner.resizeDrag(r.left, 0.0f);
+        assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+
+        // Drag to the bottom
+        mPositioner.resizeDrag(r.left, 1000.0f);
+        assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+    }
+
+    /**
+     * This tests that by dragging any edge, the fixed / opposite edge(s) remains anchored.
+     */
+    @Test
+    public void testFreeWindowResizingTestAllEdges() throws Exception {
+        final Rect r = new Rect(100, 220, 700, 520);
+        final int midX = (r.left + r.right) / 2;
+        final int midY = (r.top + r.bottom) / 2;
+
+        // Drag upper left.
+        mPositioner.startDrag(true /*resizing*/,
+                false /*preserveOrientation*/, r.left - MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y, r);
+        mPositioner.resizeDrag(0.0f, 0.0f);
+        assertTrue(r.left != mPositioner.getWindowDragBounds().left);
+        assertEquals(r.right, mPositioner.getWindowDragBounds().right);
+        assertTrue(r.top != mPositioner.getWindowDragBounds().top);
+        assertEquals(r.bottom, mPositioner.getWindowDragBounds().bottom);
+
+        // Drag upper.
+        mPositioner.startDrag(true /*resizing*/,
+                false /*preserveOrientation*/, midX, r.top - MOUSE_DELTA_Y, r);
+        mPositioner.resizeDrag(0.0f, 0.0f);
+        assertEquals(r.left, mPositioner.getWindowDragBounds().left);
+        assertEquals(r.right, mPositioner.getWindowDragBounds().right);
+        assertTrue(r.top != mPositioner.getWindowDragBounds().top);
+        assertEquals(r.bottom, mPositioner.getWindowDragBounds().bottom);
+
+        // Drag upper right.
+        mPositioner.startDrag(true /*resizing*/,
+                false /*preserveOrientation*/, r.right + MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y, r);
+        mPositioner.resizeDrag(r.right + 100, 0.0f);
+        assertEquals(r.left, mPositioner.getWindowDragBounds().left);
+        assertTrue(r.right != mPositioner.getWindowDragBounds().right);
+        assertTrue(r.top != mPositioner.getWindowDragBounds().top);
+        assertEquals(r.bottom, mPositioner.getWindowDragBounds().bottom);
+
+        // Drag right.
+        mPositioner.startDrag(true /*resizing*/,
+                false /*preserveOrientation*/, r.right + MOUSE_DELTA_X, midY, r);
+        mPositioner.resizeDrag(r.right + 100, 0.0f);
+        assertEquals(r.left, mPositioner.getWindowDragBounds().left);
+        assertTrue(r.right != mPositioner.getWindowDragBounds().right);
+        assertEquals(r.top, mPositioner.getWindowDragBounds().top);
+        assertEquals(r.bottom, mPositioner.getWindowDragBounds().bottom);
+
+        // Drag bottom right.
+        mPositioner.startDrag(true /*resizing*/,
+                false /*preserveOrientation*/,
+                r.right + MOUSE_DELTA_X, r.bottom + MOUSE_DELTA_Y, r);
+        mPositioner.resizeDrag(r.right + 100, r.bottom + 100);
+        assertEquals(r.left, mPositioner.getWindowDragBounds().left);
+        assertTrue(r.right != mPositioner.getWindowDragBounds().right);
+        assertEquals(r.top, mPositioner.getWindowDragBounds().top);
+        assertTrue(r.bottom != mPositioner.getWindowDragBounds().bottom);
+
+        // Drag bottom.
+        mPositioner.startDrag(true /*resizing*/,
+                false /*preserveOrientation*/, midX, r.bottom + MOUSE_DELTA_Y, r);
+        mPositioner.resizeDrag(r.right + 100, r.bottom + 100);
+        assertEquals(r.left, mPositioner.getWindowDragBounds().left);
+        assertEquals(r.right, mPositioner.getWindowDragBounds().right);
+        assertEquals(r.top, mPositioner.getWindowDragBounds().top);
+        assertTrue(r.bottom != mPositioner.getWindowDragBounds().bottom);
+
+        // Drag bottom left.
+        mPositioner.startDrag(true /*resizing*/,
+                false /*preserveOrientation*/, r.left - MOUSE_DELTA_X, r.bottom + MOUSE_DELTA_Y, r);
+        mPositioner.resizeDrag(0.0f, r.bottom + 100);
+        assertTrue(r.left != mPositioner.getWindowDragBounds().left);
+        assertEquals(r.right, mPositioner.getWindowDragBounds().right);
+        assertEquals(r.top, mPositioner.getWindowDragBounds().top);
+        assertTrue(r.bottom != mPositioner.getWindowDragBounds().bottom);
+
+        // Drag left.
+        mPositioner.startDrag(true /*resizing*/,
+                false /*preserveOrientation*/, r.left - MOUSE_DELTA_X, midX, r);
+        mPositioner.resizeDrag(0.0f, r.bottom + 100);
+        assertTrue(r.left != mPositioner.getWindowDragBounds().left);
+        assertEquals(r.right, mPositioner.getWindowDragBounds().right);
+        assertEquals(r.top, mPositioner.getWindowDragBounds().top);
+        assertEquals(r.bottom, mPositioner.getWindowDragBounds().bottom);
+    }
+
+    /**
+     * This tests that a constrained landscape window will keep the aspect and do the
+     * right things upon resizing when dragged from the top left corner.
+     */
+    @Test
+    public void testLandscapePreservedWindowResizingDragTopLeft() throws Exception {
+        final Rect r = new Rect(100, 220, 700, 520);
+
+        mPositioner.startDrag(true /*resizing*/,
+                true /*preserveOrientation*/, r.left - MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y, r);
+        assertBoundsEquals(r, mPositioner.getWindowDragBounds());
+
+        // Drag to a good landscape size.
+        mPositioner.resizeDrag(0.0f, 0.0f);
+        assertBoundsEquals(new Rect(MOUSE_DELTA_X, MOUSE_DELTA_Y, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+
+        // Drag to a good portrait size.
+        mPositioner.resizeDrag(400.0f, 0.0f);
+        int width = Math.round((float) (r.bottom - MOUSE_DELTA_Y) * MIN_ASPECT);
+        assertBoundsEquals(new Rect(r.right - width, MOUSE_DELTA_Y, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+
+        // Drag to a too small size for the width.
+        mPositioner.resizeDrag(2000.0f, r.top);
+        final int w = mMinVisibleWidth;
+        final int h = Math.round(w / MIN_ASPECT);
+        assertBoundsEquals(new Rect(r.right - w, r.bottom - h, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+
+        // Drag to a too small size for the height.
+        mPositioner.resizeDrag(r.left, 2000.0f);
+        assertBoundsEquals(
+                new Rect(r.left + MOUSE_DELTA_X, r.bottom - mMinVisibleHeight, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+    }
+
+    /**
+     * This tests that a constrained landscape window will keep the aspect and do the
+     * right things upon resizing when dragged from the left corner.
+     */
+    @Test
+    public void testLandscapePreservedWindowResizingDragLeft() throws Exception {
+        final Rect r = new Rect(100, 220, 700, 520);
+        final int midY = (r.top + r.bottom) / 2;
+
+        mPositioner.startDrag(true /*resizing*/,
+                true /*preserveOrientation*/, r.left - MOUSE_DELTA_X, midY, r);
+
+        // Drag to the left.
+        mPositioner.resizeDrag(0.0f, midY);
+        assertBoundsEquals(new Rect(MOUSE_DELTA_X, r.top, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+
+        // Drag to the right.
+        mPositioner.resizeDrag(200.0f, midY);
+        assertBoundsEquals(new Rect(200 + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+
+        // Drag all the way to the right and see the height also shrinking.
+        mPositioner.resizeDrag(2000.0f, midY);
+        final int w = mMinVisibleWidth;
+        final int h = Math.round((float)w / MIN_ASPECT);
+        assertBoundsEquals(new Rect(r.right - w, r.top, r.right, r.top + h),
+                mPositioner.getWindowDragBounds());
+
+        // Drag to the top.
+        mPositioner.resizeDrag(r.left, 0.0f);
+        assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+
+        // Drag to the bottom.
+        mPositioner.resizeDrag(r.left, 1000.0f);
+        assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+    }
+
+    /**
+     * This tests that a constrained landscape window will keep the aspect and do the
+     * right things upon resizing when dragged from the top corner.
+     */
+    @Test
+    public void testLandscapePreservedWindowResizingDragTop() throws Exception {
+        final Rect r = new Rect(100, 220, 700, 520);
+        final int midX = (r.left + r.right) / 2;
+
+        mPositioner.startDrag(true /*resizing*/,
+                true /*preserveOrientation*/, midX, r.top - MOUSE_DELTA_Y, r);
+
+        // Drag to the left (no change).
+        mPositioner.resizeDrag(0.0f, r.top);
+        assertBoundsEquals(new Rect(r.left, r.top + MOUSE_DELTA_Y, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+
+        // Drag to the right (no change).
+        mPositioner.resizeDrag(2000.0f, r.top);
+        assertBoundsEquals(new Rect(r.left , r.top + MOUSE_DELTA_Y, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+
+        // Drag to the top.
+        mPositioner.resizeDrag(300.0f, 0.0f);
+        int h = r.bottom - MOUSE_DELTA_Y;
+        int w = Math.max(r.right - r.left, Math.round(h * MIN_ASPECT));
+        assertBoundsEquals(new Rect(r.left, MOUSE_DELTA_Y, r.left + w, r.bottom),
+                mPositioner.getWindowDragBounds());
+
+        // Drag to the bottom.
+        mPositioner.resizeDrag(r.left, 1000.0f);
+        h = mMinVisibleHeight;
+        assertBoundsEquals(new Rect(r.left, r.bottom - h, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+    }
+
+    /**
+     * This tests that a constrained portrait window will keep the aspect and do the
+     * right things upon resizing when dragged from the top left corner.
+     */
+    @Test
+    public void testPortraitPreservedWindowResizingDragTopLeft() throws Exception {
+        final Rect r = new Rect(330, 100, 630, 600);
+
+        mPositioner.startDrag(true /*resizing*/,
+                true /*preserveOrientation*/, r.left - MOUSE_DELTA_X, r.top - MOUSE_DELTA_Y, r);
+        assertBoundsEquals(r, mPositioner.getWindowDragBounds());
+
+        // Drag to a good landscape size.
+        mPositioner.resizeDrag(0.0f, 0.0f);
+        int height = Math.round((float) (r.right - MOUSE_DELTA_X) * MIN_ASPECT);
+        assertBoundsEquals(new Rect(MOUSE_DELTA_X, r.bottom - height, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+
+        // Drag to a good portrait size.
+        mPositioner.resizeDrag(500.0f, 0.0f);
+        assertBoundsEquals(new Rect(500 + MOUSE_DELTA_X, MOUSE_DELTA_Y, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+
+        // Drag to a too small size for the height and the the width shrinking.
+        mPositioner.resizeDrag(r.left + MOUSE_DELTA_X, 2000.0f);
+        final int w = Math.max(mMinVisibleWidth, Math.round(mMinVisibleHeight / MIN_ASPECT));
+        final int h = Math.max(mMinVisibleHeight, Math.round(w * MIN_ASPECT));
+        assertBoundsEquals(
+                new Rect(r.right - w, r.bottom - h, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+    }
+
+    /**
+     * This tests that a constrained portrait window will keep the aspect and do the
+     * right things upon resizing when dragged from the left corner.
+     */
+    @Test
+    public void testPortraitPreservedWindowResizingDragLeft() throws Exception {
+        final Rect r = new Rect(330, 100, 630, 600);
+        final int midY = (r.top + r.bottom) / 2;
+
+        mPositioner.startDrag(true /*resizing*/,
+                true /*preserveOrientation*/, r.left - MOUSE_DELTA_X, midY, r);
+
+        // Drag to the left.
+        mPositioner.resizeDrag(0.0f, midY);
+        int w = r.right - MOUSE_DELTA_X;
+        int h = Math.round(w * MIN_ASPECT);
+        assertBoundsEquals(new Rect(MOUSE_DELTA_X, r.top, r.right, r.top + h),
+                mPositioner.getWindowDragBounds());
+
+        // Drag to the right.
+        mPositioner.resizeDrag(450.0f, midY);
+        assertBoundsEquals(new Rect(450 + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+
+        // Drag all the way to the right.
+        mPositioner.resizeDrag(2000.0f, midY);
+        w = mMinVisibleWidth;
+        h = Math.max(Math.round((float)w * MIN_ASPECT), r.height());
+        assertBoundsEquals(new Rect(r.right - w, r.top, r.right, r.top + h),
+                mPositioner.getWindowDragBounds());
+
+        // Drag to the top.
+        mPositioner.resizeDrag(r.left, 0.0f);
+        assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+
+        // Drag to the bottom.
+        mPositioner.resizeDrag(r.left, 1000.0f);
+        assertBoundsEquals(new Rect(r.left + MOUSE_DELTA_X, r.top, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+    }
+
+    /**
+     * This tests that a constrained portrait window will keep the aspect and do the
+     * right things upon resizing when dragged from the top corner.
+     */
+    @Test
+    public void testPortraitPreservedWindowResizingDragTop() throws Exception {
+        final Rect r = new Rect(330, 100, 630, 600);
+        final int midX = (r.left + r.right) / 2;
+
+        mPositioner.startDrag(true /*resizing*/,
+                true /*preserveOrientation*/, midX, r.top - MOUSE_DELTA_Y, r);
+
+        // Drag to the left (no change).
+        mPositioner.resizeDrag(0.0f, r.top);
+        assertBoundsEquals(new Rect(r.left, r.top + MOUSE_DELTA_Y, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+
+        // Drag to the right (no change).
+        mPositioner.resizeDrag(2000.0f, r.top);
+        assertBoundsEquals(new Rect(r.left , r.top + MOUSE_DELTA_Y, r.right, r.bottom),
+                mPositioner.getWindowDragBounds());
+
+        // Drag to the top.
+        mPositioner.resizeDrag(300.0f, 0.0f);
+        int h = r.bottom - MOUSE_DELTA_Y;
+        int w = Math.min(r.width(), Math.round(h / MIN_ASPECT));
+        assertBoundsEquals(new Rect(r.left, MOUSE_DELTA_Y, r.left + w, r.bottom),
+                mPositioner.getWindowDragBounds());
+
+        // Drag to the bottom.
+        mPositioner.resizeDrag(r.left, 1000.0f);
+        h = Math.max(mMinVisibleHeight, Math.round(mMinVisibleWidth * MIN_ASPECT));
+        w = Math.round(h / MIN_ASPECT);
+        assertBoundsEquals(new Rect(r.left, r.bottom - h, r.left + w, r.bottom),
+                mPositioner.getWindowDragBounds());
+    }
+
+    private void assertBoundsEquals(Rect expected, Rect actual) {
+        if (DEBUGGING) {
+            if (!expected.equals(actual)) {
+                Log.e(TAG, "rect(" + actual.toString() + ") != isRect(" + actual.toString()
+                        + ") " + Log.getStackTraceString(new Throwable()));
+            }
+        }
+        assertEquals(expected.left, actual.left);
+        assertEquals(expected.right, actual.right);
+        assertEquals(expected.top, actual.top);
+        assertEquals(expected.bottom, actual.bottom);
+    }
+}