Support scrolling for non-resizeable tasks in side-by-side mode

Display toast when a non-resizeable task is put into side-by-side mode.

Scroll the task upon a two-finger scroll gesture.

bug: 25433902

Change-Id: I69967056a564cfe7773afb80aa7e7ea7167a791a
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 9650088..557b386 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -8809,6 +8809,7 @@
             }
             if (task.mResizeable != resizeable) {
                 task.mResizeable = resizeable;
+                mWindowManager.setTaskResizeable(taskId, resizeable);
                 mStackSupervisor.ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS);
                 mStackSupervisor.resumeTopActivitiesLocked();
             }
@@ -8827,6 +8828,16 @@
                     Slog.w(TAG, "resizeTask: taskId=" + taskId + " not found");
                     return;
                 }
+                int stackId = task.stack.mStackId;
+                // First, check if this is a non-resizeble task in docked stack or if the task size
+                // is affected by the docked stack changing size. If so, instead of resizing, we
+                // can only scroll the task. No need to update configuration.
+                if (bounds != null && !task.mResizeable
+                        && mStackSupervisor.isStackDockedInEffect(stackId)) {
+                    mWindowManager.scrollTask(task.taskId, bounds);
+                    return;
+                }
+
                 // Place the task in the right stack if it isn't there already based on
                 // the requested bounds.
                 // The stack transition logic is:
@@ -8834,7 +8845,6 @@
                 // - a non-null bounds on a non-freeform (fullscreen OR docked) task moves
                 //   that task to freeform
                 // - otherwise the task is not moved
-                int stackId = task.stack.mStackId;
                 if (!StackId.isTaskResizeAllowed(stackId)) {
                     throw new IllegalArgumentException("resizeTask not allowed on task=" + task);
                 }
diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java
index 6c68f88..cd29050 100644
--- a/services/core/java/com/android/server/am/ActivityStack.java
+++ b/services/core/java/com/android/server/am/ActivityStack.java
@@ -4700,6 +4700,7 @@
                 (r.info.flags & ActivityInfo.FLAG_SHOW_FOR_ALL_USERS) != 0, r.userId,
                 r.info.configChanges, task.voiceSession != null, r.mLaunchTaskBehind,
                 bounds, task.mOverrideConfig, !r.isHomeActivity());
+        mWindowManager.setTaskResizeable(task.taskId, task.mResizeable);
         r.taskConfigOverride = task.mOverrideConfig;
     }
 
@@ -4753,6 +4754,7 @@
         task.updateOverrideConfiguration(bounds);
         mWindowManager.setAppTask(
                 r.appToken, task.taskId, task.getLaunchBounds(), task.mOverrideConfig);
+        mWindowManager.setTaskResizeable(task.taskId, task.mResizeable);
         r.taskConfigOverride = task.mOverrideConfig;
     }
 
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index 507d76d..723c1a6 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -34,6 +34,7 @@
 import static android.content.pm.ActivityInfo.FLAG_SHOW_FOR_ALL_USERS;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
 import static com.android.server.am.ActivityManagerDebugConfig.*;
 import static com.android.server.am.ActivityManagerService.FIRST_SUPERVISOR_STACK_MSG;
 import static com.android.server.am.ActivityRecord.HOME_ACTIVITY_TYPE;
@@ -120,10 +121,13 @@
 import android.view.DisplayInfo;
 import android.view.InputEvent;
 import android.view.Surface;
+import android.widget.Toast;
+
 import com.android.internal.app.HeavyWeightSwitcherActivity;
 import com.android.internal.app.IVoiceInteractor;
 import com.android.internal.content.ReferrerIntent;
 import com.android.internal.os.TransferPipe;
+import com.android.internal.R;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.widget.LockPatternUtils;
@@ -131,7 +135,6 @@
 import com.android.server.am.ActivityStack.ActivityState;
 import com.android.server.wm.WindowManagerService;
 
-
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.io.PrintWriter;
@@ -181,6 +184,7 @@
     static final int CONTAINER_CALLBACK_TASK_LIST_EMPTY = FIRST_SUPERVISOR_STACK_MSG + 11;
     static final int LAUNCH_TASK_BEHIND_COMPLETE = FIRST_SUPERVISOR_STACK_MSG + 12;
     static final int SHOW_LOCK_TASK_ESCAPE_MESSAGE_MSG = FIRST_SUPERVISOR_STACK_MSG + 13;
+    static final int SHOW_NON_RESIZEABLE_DOCK_TOAST = FIRST_SUPERVISOR_STACK_MSG + 14;
 
     private static final String VIRTUAL_DISPLAY_BASE_NAME = "ActivityViewVirtualDisplay";
 
@@ -3011,6 +3015,15 @@
         return null;
     }
 
+    /**
+     * Returns if a stack should be treated as if it's docked. Returns true if the stack is
+     * the docked stack itself, or if it's side-by-side to the docked stack.
+     */
+    boolean isStackDockedInEffect(int stackId) {
+        return stackId == DOCKED_STACK_ID ||
+                (StackId.isResizeableByDockedStack(stackId) && getStack(DOCKED_STACK_ID) != null);
+    }
+
     ActivityContainer createVirtualActivityContainer(ActivityRecord parentActivity,
             IActivityContainerCallback callback) {
         ActivityContainer activityContainer =
@@ -3378,6 +3391,10 @@
         // the visibility of the stack / windows.
         ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS);
         resumeTopActivitiesLocked();
+
+        if (!task.mResizeable && isStackDockedInEffect(stackId)) {
+            showNonResizeableDockToast();
+        }
     }
 
     boolean moveTopStackActivityToPinnedStackLocked(int stackId, Rect bounds) {
@@ -4310,6 +4327,10 @@
         }
     }
 
+    void showNonResizeableDockToast() {
+        mHandler.sendEmptyMessage(SHOW_NON_RESIZEABLE_DOCK_TOAST);
+    }
+
     void showLockTaskToast() {
         mLockTaskNotify.showToast(mLockTaskModeState);
     }
@@ -4622,6 +4643,13 @@
                         }
                     }
                 } break;
+                case SHOW_NON_RESIZEABLE_DOCK_TOAST: {
+                    final Toast toast = Toast.makeText(
+                            mService.mContext,
+                            mService.mContext.getString(R.string.dock_non_resizeble_text),
+                            Toast.LENGTH_LONG);
+                    toast.show();
+                } break;
             }
         }
     }
diff --git a/services/core/java/com/android/server/am/TaskRecord.java b/services/core/java/com/android/server/am/TaskRecord.java
index 755db6f..1e529dab 100644
--- a/services/core/java/com/android/server/am/TaskRecord.java
+++ b/services/core/java/com/android/server/am/TaskRecord.java
@@ -1271,7 +1271,11 @@
             mBounds = null;
             mOverrideConfig = Configuration.EMPTY;
         } else {
-            mBounds = new Rect(bounds);
+            if (mBounds == null) {
+                mBounds = new Rect(bounds);
+            } else {
+                mBounds.set(bounds);
+            }
             if (stack == null || StackId.persistTaskBounds(stack.mStackId)) {
                 mLastNonFullscreenBounds = mBounds;
             }
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 328c043..e9e09ec 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -90,6 +90,9 @@
     /** Detect user tapping outside of current focused stack bounds .*/
     Region mTouchExcludeRegion = new Region();
 
+    /** Detect user tapping in a non-resizeable task in docked or fullscreen stack .*/
+    Region mNonResizeableRegion = new Region();
+
     /** Save allocating when calculating rects */
     private Rect mTmpRect = new Rect();
     private Rect mTmpRect2 = new Rect();
@@ -274,7 +277,12 @@
 
     int taskIdFromPoint(int x, int y) {
         for (int stackNdx = mStacks.size() - 1; stackNdx >= 0; --stackNdx) {
-            final ArrayList<Task> tasks = mStacks.get(stackNdx).getTasks();
+            TaskStack stack = mStacks.get(stackNdx);
+            stack.getBounds(mTmpRect);
+            if (!mTmpRect.contains(x, y)) {
+                continue;
+            }
+            final ArrayList<Task> tasks = stack.getTasks();
             for (int taskNdx = tasks.size() - 1; taskNdx >= 0; --taskNdx) {
                 final Task task = tasks.get(taskNdx);
                 final WindowState win = task.getTopVisibleAppMainWindow();
@@ -339,6 +347,7 @@
         mTouchExcludeRegion.set(mBaseDisplayRect);
         final int delta = mService.dipToPixel(RESIZE_HANDLE_WIDTH_IN_DP, mDisplayMetrics);
         boolean addBackFocusedTask = false;
+        mNonResizeableRegion.setEmpty();
         for (int stackNdx = mStacks.size() - 1; stackNdx >= 0; --stackNdx) {
             TaskStack stack = mStacks.get(stackNdx);
             final ArrayList<Task> tasks = stack.getTasks();
@@ -381,6 +390,11 @@
                     }
                     mTouchExcludeRegion.op(mTmpRect, Region.Op.DIFFERENCE);
                 }
+                if (task.isDockedInEffect() && !task.isResizeable()) {
+                    stack.getBounds(mTmpRect);
+                    mNonResizeableRegion.op(mTmpRect, Region.Op.UNION);
+                    break;
+                }
             }
         }
         // If we removed the focused task above, add it back and only leave its
@@ -390,7 +404,7 @@
             mTouchExcludeRegion.op(mTmpRect2, Region.Op.UNION);
         }
         if (mTapDetector != null) {
-            mTapDetector.setTouchExcludeRegion(mTouchExcludeRegion);
+            mTapDetector.setTouchExcludeRegion(mTouchExcludeRegion, mNonResizeableRegion);
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java
index 1f351cb..5511136 100644
--- a/services/core/java/com/android/server/wm/InputMonitor.java
+++ b/services/core/java/com/android/server/wm/InputMonitor.java
@@ -194,6 +194,14 @@
         inputWindowHandle.frameRight = frame.right;
         inputWindowHandle.frameBottom = frame.bottom;
 
+        if (child.isDockedInEffect()) {
+            // Adjust to account for non-resizeable tasks that's scrolled
+            inputWindowHandle.frameLeft += child.mXOffset;
+            inputWindowHandle.frameTop += child.mYOffset;
+            inputWindowHandle.frameRight += child.mXOffset;
+            inputWindowHandle.frameBottom += child.mYOffset;
+        }
+
         if (child.mGlobalScale != 1) {
             // If we are scaling the window, input coordinates need
             // to be inversely scaled to map from what is on screen
@@ -204,7 +212,8 @@
         }
 
         if (DEBUG_INPUT) {
-            Slog.d(WindowManagerService.TAG, "addInputWindowHandle: " + inputWindowHandle);
+            Slog.d(WindowManagerService.TAG, "addInputWindowHandle: "
+                    + child + ", " + inputWindowHandle);
         }
         addInputWindowHandleLw(inputWindowHandle);
     }
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 46cd7cd..51e48a3 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -20,6 +20,7 @@
 import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
 import static android.app.ActivityManager.StackId.HOME_STACK_ID;
 import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.app.ActivityManager.RESIZE_MODE_SYSTEM_SCREEN_ROTATION;
 import static com.android.server.wm.WindowManagerService.TAG;
 import static com.android.server.wm.WindowManagerService.DEBUG_RESIZE;
@@ -72,6 +73,9 @@
     // For handling display rotations.
     private Rect mTmpRect2 = new Rect();
 
+    // Whether the task is resizeable
+    private boolean mResizeable;
+
     // Whether the task is currently being drag-resized
     private boolean mDragResizing;
 
@@ -227,6 +231,14 @@
         return boundsChange;
     }
 
+    void setResizeable(boolean resizeable) {
+        mResizeable = resizeable;
+    }
+
+    boolean isResizeable() {
+        return mResizeable;
+    }
+
     boolean resizeLocked(Rect bounds, Configuration configuration, boolean forced) {
         int boundsChanged = setBounds(bounds, configuration);
         if (forced) {
@@ -241,6 +253,45 @@
         return true;
     }
 
+    boolean scrollLocked(Rect bounds) {
+        // shift the task bound if it doesn't fully cover the stack area
+        mStack.getDimBounds(mTmpRect);
+        if (mService.mCurConfiguration.orientation == ORIENTATION_LANDSCAPE) {
+            if (bounds.left > mTmpRect.left) {
+                bounds.left = mTmpRect.left;
+                bounds.right = mTmpRect.left + mBounds.width();
+            } else if (bounds.right < mTmpRect.right) {
+                bounds.left = mTmpRect.right - mBounds.width();
+                bounds.right = mTmpRect.right;
+            }
+        } else {
+            if (bounds.top > mTmpRect.top) {
+                bounds.top = mTmpRect.top;
+                bounds.bottom = mTmpRect.top + mBounds.height();
+            } else if (bounds.bottom < mTmpRect.bottom) {
+                bounds.top = mTmpRect.bottom - mBounds.height();
+                bounds.bottom = mTmpRect.bottom;
+            }
+        }
+
+        if (bounds.equals(mBounds)) {
+            return false;
+        }
+        // Normal setBounds() does not allow non-null bounds for fullscreen apps.
+        // We only change bounds for the scrolling case without change it size,
+        // on resizing path we should still want the validation.
+        mBounds.set(bounds);
+        for (int activityNdx = mAppTokens.size() - 1; activityNdx >= 0; --activityNdx) {
+            final ArrayList<WindowState> windows = mAppTokens.get(activityNdx).allAppWindows;
+            for (int winNdx = windows.size() - 1; winNdx >= 0; --winNdx) {
+                final WindowState win = windows.get(winNdx);
+                win.mXOffset = bounds.left;
+                win.mYOffset = bounds.top;
+            }
+        }
+        return true;
+    }
+
     /** Return true if the current bound can get outputted to the rest of the system as-is. */
     private boolean useCurrentBounds() {
         final DisplayContent displayContent = mStack.getDisplayContent();
@@ -418,6 +469,19 @@
         return mStack != null && mStack.mStackId == DOCKED_STACK_ID;
     }
 
+    boolean isResizeableByDockedStack() {
+        return mStack != null && getDisplayContent().getDockedStackLocked() != null &&
+                StackId.isTaskResizeableByDockedStack(mStack.mStackId);
+    }
+
+    /**
+     * Whether the task should be treated as if it's docked. Returns true if the task
+     * is currently in docked workspace, or it's side-by-side to a docked task.
+     */
+    boolean isDockedInEffect() {
+        return inDockedWorkspace() || isResizeableByDockedStack();
+    }
+
     WindowState getTopVisibleAppMainWindow() {
         final AppWindowToken token = getTopVisibleAppToken();
         return token != null ? token.findMainWindow() : null;
diff --git a/services/core/java/com/android/server/wm/TaskPositioner.java b/services/core/java/com/android/server/wm/TaskPositioner.java
index 09118c7..32c3205 100644
--- a/services/core/java/com/android/server/wm/TaskPositioner.java
+++ b/services/core/java/com/android/server/wm/TaskPositioner.java
@@ -337,12 +337,20 @@
         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);
+        if (mTask.isDockedInEffect()) {
+            // If this is a docked task or if task size is affected by docked stack changing size,
+            // we can only be here if the task is not resizeable and we're handling a two-finger
+            // scrolling. Use the original task bounds to position the task, the dim bounds
+            // is cropped and doesn't move.
+            mTask.getBounds(mTmpRect);
+        } else {
+            // 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);
+        }
 
         if (resize) {
             if (startX < mTmpRect.left) {
@@ -371,7 +379,7 @@
     /** Returns true if the move operation should be ended. */
     private boolean notifyMoveLocked(float x, float y) {
         if (DEBUG_TASK_POSITIONING) {
-            Slog.d(TAG, "notifyMoveLw: {" + x + "," + y + "}");
+            Slog.d(TAG, "notifyMoveLocked: {" + x + "," + y + "}");
         }
 
         if (mCtrlType != CTRL_NONE) {
@@ -401,15 +409,45 @@
 
         // This is a moving operation.
         mTask.mStack.getDimBounds(mTmpRect);
-        mTmpRect.inset(mMinVisibleWidth, mMinVisibleHeight);
-        if (!mTmpRect.contains((int) x, (int) y)) {
-            // We end the moving operation if position is outside the stack bounds.
-            return true;
+
+        // If this is a non-resizeable task put into side-by-side mode, we are
+        // handling a two-finger scrolling action. No need to shrink the bounds.
+        if (!mTask.isDockedInEffect()) {
+            mTmpRect.inset(mMinVisibleWidth, mMinVisibleHeight);
         }
+
+        boolean dragEnded = false;
+        final int nX = (int) x;
+        final int nY = (int) y;
+        if (!mTmpRect.contains(nX, nY)) {
+            // We end the moving operation if position is outside the stack bounds.
+            // In this case we need to clamp the position to stack bounds and calculate
+            // the final window drag bounds.
+            x = Math.min(Math.max(x, mTmpRect.left), mTmpRect.right);
+            y = Math.min(Math.max(y, mTmpRect.top), mTmpRect.bottom);
+            dragEnded = true;
+        }
+
+        updateWindowDragBounds(nX, nY);
+        updateDimLayerVisibility(nX);
+        return dragEnded;
+    }
+
+    private void updateWindowDragBounds(int x, int y) {
         mWindowDragBounds.set(mWindowOriginalBounds);
-        mWindowDragBounds.offset(Math.round(x - mStartDragX), Math.round(y - mStartDragY));
-        updateDimLayerVisibility((int) x);
-        return false;
+        if (mTask.isDockedInEffect()) {
+            // Offset the bounds without clamp, the bounds will be shifted later
+            // by window manager before applying the scrolling.
+            if (mService.mCurConfiguration.orientation == ORIENTATION_LANDSCAPE) {
+                mWindowDragBounds.offset(Math.round(x - mStartDragX), 0);
+            } else {
+                mWindowDragBounds.offset(0, Math.round(y - mStartDragY));
+            }
+        } else {
+            mWindowDragBounds.offset(Math.round(x - mStartDragX), Math.round(y - mStartDragY));
+        }
+        if (DEBUG_TASK_POSITIONING) Slog.d(TAG,
+                "updateWindowDragBounds: " + mWindowDragBounds);
     }
 
     private void updateDimLayerVisibility(int x) {
diff --git a/services/core/java/com/android/server/wm/TaskStack.java b/services/core/java/com/android/server/wm/TaskStack.java
index 9d07fb0..ffd168f 100644
--- a/services/core/java/com/android/server/wm/TaskStack.java
+++ b/services/core/java/com/android/server/wm/TaskStack.java
@@ -125,7 +125,18 @@
             Configuration config = configs.get(task.mTaskId);
             if (config != null) {
                 Rect bounds = taskBounds.get(task.mTaskId);
-                task.setBounds(bounds, config);
+                if (!task.isResizeable() && task.isDockedInEffect()) {
+                    // This is a non-resizeable task that's docked (or side-by-side to the docked
+                    // stack). It might have been scrolled previously, and after the stack resizing,
+                    // it might no longer fully cover the stack area.
+                    // Save the old bounds and re-apply the scroll. This adjusts the bounds to
+                    // fit the new stack bounds.
+                    task.getBounds(mTmpRect);
+                    task.setBounds(bounds, config);
+                    task.scrollLocked(mTmpRect);
+                } else {
+                    task.setBounds(bounds, config);
+                }
             } else {
                 Slog.wtf(TAG, "No config for task: " + task + ", is there a mismatch with AM?");
             }
diff --git a/services/core/java/com/android/server/wm/TaskTapPointerEventListener.java b/services/core/java/com/android/server/wm/TaskTapPointerEventListener.java
index f5b83bb..2f890be 100644
--- a/services/core/java/com/android/server/wm/TaskTapPointerEventListener.java
+++ b/services/core/java/com/android/server/wm/TaskTapPointerEventListener.java
@@ -19,7 +19,7 @@
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.view.DisplayInfo;
-import android.view.InputDevice;
+import android.view.GestureDetector;
 import android.view.MotionEvent;
 import android.view.WindowManagerPolicy.PointerEventListener;
 
@@ -44,6 +44,10 @@
     private final WindowManagerService mService;
     private final DisplayContent mDisplayContent;
     private final Rect mTmpRect = new Rect();
+    private final Region mNonResizeableRegion = new Region();
+    private boolean mTwoFingerScrolling;
+    private boolean mInGestureDetection;
+    private GestureDetector mGestureDetector;
     private int mPointerIconShape = STYLE_NOT_SPECIFIED;
 
     public TaskTapPointerEventListener(WindowManagerService service,
@@ -54,8 +58,18 @@
         mMotionSlop = (int)(info.logicalDensityDpi * TAP_MOTION_SLOP_INCHES);
     }
 
+    // initialize the object, note this must be done outside WindowManagerService
+    // ctor, otherwise it may cause recursion as some code in GestureDetector ctor
+    // depends on WMS being already created.
+    void init() {
+        mGestureDetector = new GestureDetector(
+                mService.mContext, new TwoFingerScrollListener(), mService.mH);
+    }
+
     @Override
     public void onPointerEvent(MotionEvent motionEvent) {
+        doGestureDetection(motionEvent);
+
         final int action = motionEvent.getAction();
         switch (action & MotionEvent.ACTION_MASK) {
             case MotionEvent.ACTION_DOWN: {
@@ -84,6 +98,9 @@
                         mPointerId = -1;
                     }
                 }
+                if (motionEvent.getPointerCount() != 2) {
+                    stopTwoFingerScroll();
+                }
                 break;
             }
 
@@ -143,14 +160,72 @@
                     }
                     mPointerId = -1;
                 }
+                stopTwoFingerScroll();
                 break;
             }
         }
     }
 
-    void setTouchExcludeRegion(Region newRegion) {
+    private void doGestureDetection(MotionEvent motionEvent) {
+        if (mGestureDetector == null || mNonResizeableRegion.isEmpty()) {
+            return;
+        }
+        final int action = motionEvent.getAction() & MotionEvent.ACTION_MASK;
+        final int x = (int) motionEvent.getX();
+        final int y = (int) motionEvent.getY();
+        final boolean isTouchInside = mNonResizeableRegion.contains(x, y);
+        if (mInGestureDetection || action == MotionEvent.ACTION_DOWN && isTouchInside) {
+            // If we receive the following actions, or the pointer goes out of the area
+            // we're interested in, stop detecting and cancel the current detection.
+            mInGestureDetection = isTouchInside
+                    && action != MotionEvent.ACTION_UP
+                    && action != MotionEvent.ACTION_POINTER_UP
+                    && action != MotionEvent.ACTION_CANCEL;
+            if (mInGestureDetection) {
+                mGestureDetector.onTouchEvent(motionEvent);
+            } else {
+                MotionEvent cancelEvent = motionEvent.copy();
+                cancelEvent.cancel();
+                mGestureDetector.onTouchEvent(cancelEvent);
+                stopTwoFingerScroll();
+            }
+        }
+    }
+
+    private void onTwoFingerScroll(MotionEvent e) {
+        final int x = (int)e.getX(0);
+        final int y = (int)e.getY(0);
+        if (!mTwoFingerScrolling) {
+            mTwoFingerScrolling = true;
+            mService.mH.obtainMessage(
+                    H.TWO_FINGER_SCROLL_START, x, y, mDisplayContent).sendToTarget();
+        }
+    }
+
+    private void stopTwoFingerScroll() {
+        if (mTwoFingerScrolling) {
+            mTwoFingerScrolling = false;
+            mService.mH.obtainMessage(H.FINISH_TASK_POSITIONING).sendToTarget();
+        }
+    }
+
+    private final class TwoFingerScrollListener extends GestureDetector.SimpleOnGestureListener {
+        @Override
+        public boolean onScroll(MotionEvent e1, MotionEvent e2,
+                float distanceX, float distanceY) {
+            if (e2.getPointerCount() == 2) {
+                onTwoFingerScroll(e2);
+                return true;
+            }
+            stopTwoFingerScroll();
+            return false;
+        }
+    }
+
+    void setTouchExcludeRegion(Region newRegion, Region nonResizeableRegion) {
         synchronized (this) {
            mTouchExcludeRegion.set(newRegion);
+           mNonResizeableRegion.set(nonResizeableRegion);
         }
     }
 }
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index b80b895..21d3bba 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -3564,7 +3564,7 @@
         }
     }
 
-    void setFocusTaskRegion() {
+    void setFocusTaskRegionLocked() {
         if (mFocusedApp != null) {
             final Task task = mFocusedApp.mTask;
             final DisplayContent displayContent = task.getDisplayContent();
@@ -3599,7 +3599,7 @@
             if (changed) {
                 mFocusedApp = newFocus;
                 mInputMonitor.setFocusedAppLw(newFocus);
-                setFocusTaskRegion();
+                setFocusTaskRegionLocked();
             }
 
             if (moveFocusNow && changed) {
@@ -4857,6 +4857,21 @@
         }
     }
 
+    public void scrollTask(int taskId, Rect bounds) {
+        synchronized (mWindowMap) {
+            Task task = mTaskIdToTask.get(taskId);
+            if (task == null) {
+                throw new IllegalArgumentException("scrollTask: taskId " + taskId
+                        + " not found.");
+            }
+
+            if (task.scrollLocked(bounds)) {
+                task.getDisplayContent().layoutNeeded = true;
+                mInputMonitor.setUpdateInputWindowsNeededLw();
+                mWindowPlacerLocked.performSurfacePlacement();
+            }
+        }
+    }
     /**
      * Starts deferring layout passes. Useful when doing multiple changes but to optimize
      * performance, only one layout pass should be done. This can be called multiple times, and
@@ -7073,6 +7088,26 @@
         return true;
     }
 
+    private void startScrollingTask(DisplayContent displayContent, int startX, int startY) {
+        if (DEBUG_TASK_POSITIONING) Slog.d(TAG,
+                "startScrollingTask: " + "{" + startX + ", " + startY + "}");
+
+        Task task = null;
+        synchronized (mWindowMap) {
+            int taskId = displayContent.taskIdFromPoint(startX, startY);
+            if (taskId >= 0) {
+                task = mTaskIdToTask.get(taskId);
+            }
+            if (task == null || !task.isDockedInEffect() || !startPositioningLocked(
+                    task.getTopVisibleAppMainWindow(), false /*resize*/, startX, startY)) {
+                return;
+            }
+        }
+        try {
+            mActivityManager.setFocusedTask(task.mTaskId);
+        } catch(RemoteException e) {}
+    }
+
     private void startResizingTask(DisplayContent displayContent, int startX, int startY) {
         Task task = null;
         synchronized (mWindowMap) {
@@ -7089,11 +7124,10 @@
 
     private boolean startPositioningLocked(
             WindowState win, boolean resize, float startX, float startY) {
-        if (WindowManagerService.DEBUG_TASK_POSITIONING) {
-            Slog.d(TAG, "startPositioningLocked: win=" + win +
-                    ", resize=" + resize + ", {" + startX + ", " + startY + "}");
-        }
-        if (win == null || win.getAppToken() == null || !win.inFreeformWorkspace()) {
+        if (DEBUG_TASK_POSITIONING) Slog.d(TAG, "startPositioningLocked: "
+            + "win=" + win + ", resize=" + resize + ", {" + startX + ", " + startY + "}");
+
+        if (win == null || win.getAppToken() == null) {
             Slog.w(TAG, "startPositioningLocked: Bad window " + win);
             return false;
         }
@@ -7127,7 +7161,7 @@
     }
 
     private void finishPositioning() {
-        if (WindowManagerService.DEBUG_TASK_POSITIONING) {
+        if (DEBUG_TASK_POSITIONING) {
             Slog.d(TAG, "finishPositioning");
         }
         synchronized (mWindowMap) {
@@ -7340,6 +7374,7 @@
             if (displayContent != null) {
                 mAnimator.addDisplayLocked(displayId);
                 displayContent.initializeDisplayBaseInfo();
+                displayContent.mTapDetector.init();
             }
         }
     }
@@ -7405,6 +7440,8 @@
         public static final int RESIZE_STACK = 43;
         public static final int RESIZE_TASK = 44;
 
+        public static final int TWO_FINGER_SCROLL_START = 45;
+
         /**
          * Used to denote that an integer field in a message will not be used.
          */
@@ -7871,6 +7908,11 @@
                 }
                 break;
 
+                case TWO_FINGER_SCROLL_START: {
+                    startScrollingTask((DisplayContent)msg.obj, msg.arg1, msg.arg2);
+                }
+                break;
+
                 case TAP_DOWN_OUTSIDE_TASK: {
                     startResizingTask((DisplayContent)msg.obj, msg.arg1, msg.arg2);
                 }
@@ -10141,6 +10183,15 @@
         }
     }
 
+    public void setTaskResizeable(int taskId, boolean resizeable) {
+        synchronized (mWindowMap) {
+            Task task = mTaskIdToTask.get(taskId);
+            if (task != null) {
+                task.setResizeable(resizeable);
+            }
+        }
+    }
+
     static int dipToPixel(int dip, DisplayMetrics displayMetrics) {
         return (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, displayMetrics);
     }
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 3aa088d..a2ca170 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1430,7 +1430,13 @@
     }
 
     boolean inDockedWorkspace() {
-        return mAppToken != null && mAppToken.mTask != null && mAppToken.mTask.inDockedWorkspace();
+        Task task = getTask();
+        return task != null && task.inDockedWorkspace();
+    }
+
+    boolean isDockedInEffect() {
+        Task task = getTask();
+        return task != null && task.isDockedInEffect();
     }
 
     int getTouchableRegion(Region region, int flags) {
diff --git a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
index da7f45e..54d18e9 100644
--- a/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
+++ b/services/core/java/com/android/server/wm/WindowSurfacePlacer.java
@@ -514,7 +514,7 @@
         if (updateInputWindowsNeeded) {
             mService.mInputMonitor.updateInputWindowsLw(false /*force*/);
         }
-        mService.setFocusTaskRegion();
+        mService.setFocusTaskRegionLocked();
 
         // Check to see if we are now in a state where the screen should
         // be enabled, because the window obscured flags have changed.