Adding move window functionality for free form desktop

This patch adds the capability to move / drag a floating window on
the desktop when it has a non client decor capition.

It also adds the framework necessary adjustments to keep the window
in a visible area without relayout'ing the window upon move.

Bug: 21738328
Bug: 23176762
Change-Id: I0927e98902d8172f58d21c19c249936a81181678
diff --git a/core/java/com/android/internal/widget/NonClientDecorView.java b/core/java/com/android/internal/widget/NonClientDecorView.java
index 05711b5..92812f8 100644
--- a/core/java/com/android/internal/widget/NonClientDecorView.java
+++ b/core/java/com/android/internal/widget/NonClientDecorView.java
@@ -20,6 +20,7 @@
 import android.graphics.Rect;
 import android.os.RemoteException;
 import android.util.AttributeSet;
+import android.view.MotionEvent;
 import android.view.View;
 import android.widget.LinearLayout;
 import android.view.ViewGroup;
@@ -63,18 +64,31 @@
     private final int DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP = 20;
     // The height of a window which has not in DIP.
     private final int DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP = 5;
-
     private PhoneWindow mOwner = null;
-    boolean mWindowHasShadow = false;
-    boolean mShowDecor = false;
+    private boolean mWindowHasShadow = false;
+    private boolean mShowDecor = false;
+
+    // True if the window is being dragged.
+    private boolean mDragging = false;
+
+    // The bounds of the window and the absolute mouse pointer coordinates from before we started to
+    // drag the window. They will be used to determine the next window position.
+    private final Rect mWindowOriginalBounds = new Rect();
+    private float mStartDragX;
+    private float mStartDragY;
+    // True when the left mouse button got released while dragging.
+    private boolean mLeftMouseButtonReleased;
+
+    // Avoiding re-creation of Rect's by keeping a temporary window drag bound.
+    private final Rect mWindowDragBounds = new Rect();
 
     // The current focus state of the window for updating the window elevation.
-    boolean mWindowHasFocus = true;
+    private boolean mWindowHasFocus = true;
 
     // Cludge to address b/22668382: Set the shadow size to the maximum so that the layer
     // size calculation takes the shadow size into account. We set the elevation currently
     // to max until the first layout command has been executed.
-    boolean mAllowUpdateElevation = false;
+    private boolean mAllowUpdateElevation = false;
 
     public NonClientDecorView(Context context) {
         super(context);
@@ -103,6 +117,62 @@
         findViewById(R.id.close_window).setOnClickListener(this);
     }
 
+    @Override
+    public boolean onTouchEvent(MotionEvent e) {
+        // Note: There are no mixed events. When a new device gets used (e.g. 1. Mouse, 2. touch)
+        // the old input device events get cancelled first. So no need to remember the kind of
+        // input device we are listening to.
+        switch (e.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                // A drag action is started if we aren't dragging already and the starting event is
+                // either a left mouse button or any other input device.
+                if (!mDragging &&
+                        (e.getToolType(e.getActionIndex()) != MotionEvent.TOOL_TYPE_MOUSE ||
+                                (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0)) {
+                    mDragging = true;
+                    mWindowOriginalBounds.set(getActivityBounds());
+                    mLeftMouseButtonReleased = false;
+                    mStartDragX = e.getRawX();
+                    mStartDragY = e.getRawY();
+                }
+                break;
+
+            case MotionEvent.ACTION_MOVE:
+                if (mDragging && !mLeftMouseButtonReleased) {
+                    if (e.getToolType(e.getActionIndex()) == MotionEvent.TOOL_TYPE_MOUSE &&
+                            (e.getButtonState() & MotionEvent.BUTTON_PRIMARY) == 0) {
+                        // There is no separate mouse button up call and if the user mixes mouse
+                        // button drag actions, we stop dragging once he releases the button.
+                        mLeftMouseButtonReleased = true;
+                        break;
+                    }
+                    mWindowDragBounds.set(mWindowOriginalBounds);
+                    mWindowDragBounds.offset(Math.round(e.getRawX() - mStartDragX),
+                            Math.round(e.getRawY() - mStartDragY));
+                    setActivityBounds(mWindowDragBounds);
+                }
+                break;
+
+            case MotionEvent.ACTION_UP:
+                if (mDragging) {
+                    // Since the window is already where it should be we don't have to do anything
+                    // special at this time.
+                    mDragging = false;
+                    return true;
+                }
+                break;
+
+            case MotionEvent.ACTION_CANCEL:
+                if (mDragging) {
+                    mDragging = false;
+                    setActivityBounds(mWindowOriginalBounds);
+                    return true;
+                }
+                break;
+        }
+        return mDragging;
+    }
+
     /**
      * The phone window configuration has changed and the decor needs to be updated.
      * @param showDecor True if the decor should be shown.
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 4061149..e162cda 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.app.ActivityManager.FREEFORM_WORKSPACE_STACK_ID;
 import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
 import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
 import static android.view.WindowManager.LayoutParams.LAST_SUB_WINDOW;
@@ -38,6 +39,7 @@
 import android.os.RemoteCallbackList;
 import android.os.SystemClock;
 import android.os.WorkSource;
+import android.util.DisplayMetrics;
 import android.util.TimeUtils;
 import android.view.Display;
 import android.view.IWindowFocusObserver;
@@ -78,6 +80,10 @@
 final class WindowState implements WindowManagerPolicy.WindowState {
     static final String TAG = "WindowState";
 
+    // The minimal size of a window within the usable area of the freeform stack.
+    static final int MINIMUM_VISIBLE_WIDTH_IN_DP = 48;
+    static final int MINIMUM_VISIBLE_HEIGHT_IN_DP = 32;
+
     final WindowManagerService mService;
     final WindowManagerPolicy mPolicy;
     final Context mContext;
@@ -536,6 +542,8 @@
         mHaveFrame = true;
 
         final Task task = mAppToken != null ? getTask() : null;
+        final boolean isFreeFormWorkspace = task != null && task.mStack != null &&
+                task.mStack.mStackId == FREEFORM_WORKSPACE_STACK_ID;
         final boolean nonFullscreenTask = task != null && !task.isFullscreen();
         if (nonFullscreenTask) {
             task.getBounds(mContainingFrame);
@@ -545,10 +553,20 @@
                 // IME is up and obscuring this window. Adjust the window position so it is visible.
                 mContainingFrame.top -= mContainingFrame.bottom - cf.bottom;
             }
-            // Make sure the containing frame is within the content frame so we don't layout
-            // resized window under screen decorations.
-            if (!mContainingFrame.intersect(cf)) {
-                mContainingFrame.set(cf);
+
+            if (isFreeFormWorkspace) {
+                // In free form mode we have only to set the rectangle if it wasn't set already. No
+                // need to intersect it with the (visible) "content frame" since it is allowed to
+                // be outside the visible desktop.
+                if (mContainingFrame.isEmpty()) {
+                    mContainingFrame.set(cf);
+                }
+            } else {
+                // Make sure the containing frame is within the content frame so we don't layout
+                // resized window under screen decorations.
+                if (!mContainingFrame.intersect(cf)) {
+                    mContainingFrame.set(cf);
+                }
             }
             mDisplayFrame.set(mContainingFrame);
         } else {
@@ -652,20 +670,38 @@
 
         // Make sure the content and visible frames are inside of the
         // final window frame.
-        mContentFrame.set(Math.max(mContentFrame.left, mFrame.left),
-                Math.max(mContentFrame.top, mFrame.top),
-                Math.min(mContentFrame.right, mFrame.right),
-                Math.min(mContentFrame.bottom, mFrame.bottom));
+        if (isFreeFormWorkspace && !mFrame.isEmpty()) {
+            // Keep the frame out of the blocked system area, limit it in size to the content area
+            // and make sure that there is always a minimum visible so that the user can drag it
+            // into a usable area..
+            final int height = Math.min(mFrame.height(), mContentFrame.height());
+            final int width = Math.min(mContentFrame.width(), mFrame.width());
+            final int minVisibleHeight = calculatePixelFromDp(MINIMUM_VISIBLE_HEIGHT_IN_DP);
+            final int minVisibleWidth = calculatePixelFromDp(MINIMUM_VISIBLE_WIDTH_IN_DP);
+            final int top = Math.max(mContentFrame.top,
+                    Math.min(mFrame.top, mContentFrame.bottom - minVisibleHeight));
+            final int left = Math.max(mContentFrame.left + minVisibleWidth - width,
+                    Math.min(mFrame.left, mContentFrame.right - minVisibleWidth));
+            mFrame.set(left, top, left + width, top + height);
+            mContentFrame.set(mFrame);
+            mVisibleFrame.set(mContentFrame);
+            mStableFrame.set(mContentFrame);
+        } else {
+            mContentFrame.set(Math.max(mContentFrame.left, mFrame.left),
+                    Math.max(mContentFrame.top, mFrame.top),
+                    Math.min(mContentFrame.right, mFrame.right),
+                    Math.min(mContentFrame.bottom, mFrame.bottom));
 
-        mVisibleFrame.set(Math.max(mVisibleFrame.left, mFrame.left),
-                Math.max(mVisibleFrame.top, mFrame.top),
-                Math.min(mVisibleFrame.right, mFrame.right),
-                Math.min(mVisibleFrame.bottom, mFrame.bottom));
+            mVisibleFrame.set(Math.max(mVisibleFrame.left, mFrame.left),
+                    Math.max(mVisibleFrame.top, mFrame.top),
+                    Math.min(mVisibleFrame.right, mFrame.right),
+                    Math.min(mVisibleFrame.bottom, mFrame.bottom));
 
-        mStableFrame.set(Math.max(mStableFrame.left, mFrame.left),
-                Math.max(mStableFrame.top, mFrame.top),
-                Math.min(mStableFrame.right, mFrame.right),
-                Math.min(mStableFrame.bottom, mFrame.bottom));
+            mStableFrame.set(Math.max(mStableFrame.left, mFrame.left),
+                    Math.max(mStableFrame.top, mFrame.top),
+                    Math.min(mStableFrame.right, mFrame.right),
+                    Math.min(mStableFrame.bottom, mFrame.bottom));
+        }
 
         mOverscanInsets.set(Math.max(mOverscanFrame.left - mFrame.left, 0),
                 Math.max(mOverscanFrame.top - mFrame.top, 0),
@@ -1577,6 +1613,13 @@
         }
     }
 
+    private int calculatePixelFromDp(int dp) {
+        final Configuration serviceConfig = mService.mCurConfiguration;
+        // TODO(multidisplay): Update Dp to that of display stack is on.
+        final float density = serviceConfig.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
+        return (int)(dp * density);
+    }
+
     void dump(PrintWriter pw, String prefix, boolean dumpAll) {
         final TaskStack stack = getStack();
         pw.print(prefix); pw.print("mDisplayId="); pw.print(getDisplayId());