Initial implementation of LaunchingBoundsController.

This changelist introduces the LaunchingBoundsController, a central
location for calculating launch bounds. It also defines a
positioner interface that can be registered with LaunchingBoundsController
to participate in bounds calculation.

Test: bit FrameworksServicesTests:com.android.server.am.LaunchingTaskPositionerTests
Test: bit FrameworksServicesTests:com.android.server.am.LaunchingActivityPositionerTests
Test: bit FrameworksServicesTests:com.android.server.am.LaunchingBoundsControllerTests
Bug: 64144308
Change-Id: I35eaf095e2edd562375403413050ce82618c44f2
diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java
index 941c371..0385497 100644
--- a/services/core/java/com/android/server/am/ActivityStack.java
+++ b/services/core/java/com/android/server/am/ActivityStack.java
@@ -107,7 +107,6 @@
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.content.res.Configuration;
-import android.graphics.Point;
 import android.graphics.Rect;
 import android.net.Uri;
 import android.os.Binder;
@@ -346,7 +345,6 @@
     private final SparseArray<Rect> mTmpBounds = new SparseArray<>();
     private final SparseArray<Rect> mTmpInsetBounds = new SparseArray<>();
     private final Rect mTmpRect2 = new Rect();
-    private final Point mTmpSize = new Point();
 
     /** Run all ActivityStacks through this */
     protected final ActivityStackSupervisor mStackSupervisor;
@@ -5123,24 +5121,14 @@
         addTask(task, toTop, "createTaskRecord");
         final boolean isLockscreenShown = mService.mStackSupervisor.mKeyguardController
                 .isKeyguardShowing(mDisplayId != INVALID_DISPLAY ? mDisplayId : DEFAULT_DISPLAY);
-        if (!layoutTaskInStack(task, info.windowLayout) && mBounds != null && task.isResizeable()
-                && !isLockscreenShown) {
+        if (!mStackSupervisor.getLaunchingBoundsController().layoutTask(task, info.windowLayout)
+                && mBounds != null && task.isResizeable() && !isLockscreenShown) {
             task.updateOverrideConfiguration(mBounds);
         }
         task.createWindowContainer(toTop, (info.flags & FLAG_SHOW_FOR_ALL_USERS) != 0);
         return task;
     }
 
-    boolean layoutTaskInStack(TaskRecord task, ActivityInfo.WindowLayout windowLayout) {
-        if (!task.inFreeformWindowingMode()) {
-            return false;
-        }
-        mStackSupervisor.getLaunchingTaskPositioner()
-                .updateDefaultBounds(task, mTaskHistory, windowLayout);
-
-        return true;
-    }
-
     ArrayList<TaskRecord> getAllTasks() {
         return new ArrayList<>(mTaskHistory);
     }
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index c15b5e2..c5065f1 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -295,7 +295,7 @@
     WindowManagerService mWindowManager;
     DisplayManager mDisplayManager;
 
-    LaunchingTaskPositioner mTaskPositioner = new LaunchingTaskPositioner();
+    private final LaunchingBoundsController mLaunchingBoundsController;
 
     /** Counter for next free stack ID to use for dynamic activity stacks. */
     private int mNextFreeStackId = 0;
@@ -575,6 +575,9 @@
         mHandler = new ActivityStackSupervisorHandler(looper);
         mActivityMetricsLogger = new ActivityMetricsLogger(this, mService.mContext);
         mKeyguardController = new KeyguardController(service, this);
+
+        mLaunchingBoundsController = new LaunchingBoundsController();
+        mLaunchingBoundsController.registerDefaultPositioners(this);
     }
 
     void setRecentTasks(RecentTasks recentTasks) {
@@ -2161,8 +2164,8 @@
                 || mService.mSupportsFreeformWindowManagement;
     }
 
-    LaunchingTaskPositioner getLaunchingTaskPositioner() {
-        return mTaskPositioner;
+    LaunchingBoundsController getLaunchingBoundsController() {
+        return mLaunchingBoundsController;
     }
 
     protected <T extends ActivityStack> T getStack(int stackId) {
diff --git a/services/core/java/com/android/server/am/ActivityStarter.java b/services/core/java/com/android/server/am/ActivityStarter.java
index 6f74d85..4ea51f4 100644
--- a/services/core/java/com/android/server/am/ActivityStarter.java
+++ b/services/core/java/com/android/server/am/ActivityStarter.java
@@ -151,7 +151,7 @@
     private boolean mLaunchTaskBehind;
     private int mLaunchFlags;
 
-    private Rect mLaunchBounds;
+    private Rect mLaunchBounds = new Rect();
 
     private ActivityRecord mNotTop;
     private boolean mDoResume;
@@ -210,7 +210,7 @@
         mLaunchFlags = 0;
         mLaunchMode = INVALID_LAUNCH_MODE;
 
-        mLaunchBounds = null;
+        mLaunchBounds.setEmpty();
 
         mNotTop = null;
         mDoResume = false;
@@ -1254,7 +1254,10 @@
 
         mPreferredDisplayId = getPreferedDisplayId(mSourceRecord, mStartActivity, options);
 
-        mLaunchBounds = getOverrideBounds(r, options, inTask);
+        mLaunchBounds.setEmpty();
+
+        mSupervisor.getLaunchingBoundsController().calculateBounds(inTask, null /*layout*/, r,
+                options, mLaunchBounds);
 
         mLaunchMode = r.launchMode;
 
@@ -1725,7 +1728,7 @@
                     // Target stack got cleared when we all activities were removed above.
                     // Go ahead and reset it.
                     mTargetStack = computeStackFocus(mSourceRecord, false /* newTask */,
-                            null /* bounds */, mLaunchFlags, mOptions);
+                            mLaunchFlags, mOptions);
                     mTargetStack.addTask(task,
                             !mLaunchTaskBehind /* toTop */, "startActivityUnchecked");
                 }
@@ -1776,8 +1779,7 @@
 
     private int setTaskFromReuseOrCreateNewTask(
             TaskRecord taskToAffiliate, ActivityStack topStack) {
-        mTargetStack = computeStackFocus(
-                mStartActivity, true, mLaunchBounds, mLaunchFlags, mOptions);
+        mTargetStack = computeStackFocus(mStartActivity, true, mLaunchFlags, mOptions);
 
         // Do no move the target stack to front yet, as we might bail if
         // isLockTaskModeViolation fails below.
@@ -1962,7 +1964,7 @@
             return START_TASK_TO_FRONT;
         }
 
-        if (mLaunchBounds != null) {
+        if (!mLaunchBounds.isEmpty()) {
             // TODO: Shouldn't we already know what stack to use by the time we get here?
             ActivityStack stack = mSupervisor.getLaunchStack(null, null, mInTask, ON_TOP);
             if (stack != mInTask.getStack()) {
@@ -1985,7 +1987,7 @@
     }
 
     void updateBounds(TaskRecord task, Rect bounds) {
-        if (bounds == null) {
+        if (bounds.isEmpty()) {
             return;
         }
 
@@ -1998,8 +2000,7 @@
     }
 
     private void setTaskToCurrentTopOrCreateNewTask() {
-        mTargetStack = computeStackFocus(mStartActivity, false, null /* bounds */, mLaunchFlags,
-                mOptions);
+        mTargetStack = computeStackFocus(mStartActivity, false, mLaunchFlags, mOptions);
         if (mDoResume) {
             mTargetStack.moveToFront("addingToTopTask");
         }
@@ -2062,8 +2063,8 @@
         }
     }
 
-    private ActivityStack computeStackFocus(ActivityRecord r, boolean newTask, Rect bounds,
-            int launchFlags, ActivityOptions aOptions) {
+    private ActivityStack computeStackFocus(ActivityRecord r, boolean newTask, int launchFlags,
+            ActivityOptions aOptions) {
         final TaskRecord task = r.getTask();
         ActivityStack stack = getLaunchStack(r, launchFlags, task, aOptions);
         if (stack != null) {
diff --git a/services/core/java/com/android/server/am/LaunchingActivityPositioner.java b/services/core/java/com/android/server/am/LaunchingActivityPositioner.java
new file mode 100644
index 0000000..5815e98
--- /dev/null
+++ b/services/core/java/com/android/server/am/LaunchingActivityPositioner.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2017 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.am;
+
+import android.app.ActivityOptions;
+import android.content.pm.ActivityInfo;
+import android.graphics.Rect;
+import com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner;
+
+/**
+ * An implementation of {@link LaunchingBoundsPositioner}, which applies the launch bounds specified
+ * inside {@link ActivityOptions#getLaunchBounds()}.
+ */
+public class LaunchingActivityPositioner implements LaunchingBoundsPositioner {
+    private final ActivityStackSupervisor mSupervisor;
+
+    LaunchingActivityPositioner(ActivityStackSupervisor activityStackSupervisor) {
+        mSupervisor = activityStackSupervisor;
+    }
+
+    @Override
+    public int onCalculateBounds(TaskRecord task, ActivityInfo.WindowLayout layout,
+            ActivityRecord activity, ActivityOptions options,  Rect current, Rect result) {
+        // We only care about figuring out bounds for activities.
+        if (activity == null) {
+            return RESULT_SKIP;
+        }
+
+        // Activity must be resizeable in the specified task.
+        if (!(mSupervisor.canUseActivityOptionsLaunchBounds(options)
+            && (activity.isResizeable() || (task != null && task.isResizeable())))) {
+            return RESULT_SKIP;
+        }
+
+        final Rect bounds = TaskRecord.validateBounds(options.getLaunchBounds());
+
+        // Bounds weren't valid.
+        if (bounds == null) {
+            return RESULT_SKIP;
+        }
+
+        result.set(bounds);
+
+        // When this is the most explicit position specification so we should not allow further
+        // modification of the position.
+        return RESULT_DONE;
+    }
+}
diff --git a/services/core/java/com/android/server/am/LaunchingBoundsController.java b/services/core/java/com/android/server/am/LaunchingBoundsController.java
new file mode 100644
index 0000000..8345ba6
--- /dev/null
+++ b/services/core/java/com/android/server/am/LaunchingBoundsController.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2017 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.am;
+
+import android.annotation.IntDef;
+import android.app.ActivityOptions;
+import android.content.pm.ActivityInfo.WindowLayout;
+import android.graphics.Rect;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner.RESULT_CONTINUE;
+import static com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner.RESULT_DONE;
+import static com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner.RESULT_SKIP;
+
+/**
+ * {@link LaunchingBoundsController} calculates the launch bounds by coordinating between registered
+ * {@link LaunchingBoundsPositioner}.
+ */
+class LaunchingBoundsController {
+    private final List<LaunchingBoundsPositioner> mPositioners = new ArrayList<>();
+
+    // Temporary {@link Rect} for calculations. This is kept separate from {@code mTmpCurrent} and
+    // {@code mTmpResult} to prevent clobbering values.
+    private final Rect mTmpRect = new Rect();
+
+    private final Rect mTmpCurrent = new Rect();
+    private final Rect mTmpResult = new Rect();
+
+    /**
+     * Creates a {@link LaunchingBoundsController} with default registered
+     * {@link LaunchingBoundsPositioner}s.
+     */
+    void registerDefaultPositioners(ActivityStackSupervisor supervisor) {
+        // {@link LaunchingTaskPositioner} handles window layout preferences.
+        registerPositioner(new LaunchingTaskPositioner());
+
+        // {@link LaunchingActivityPositioner} is the most specific positioner and thus should be
+        // registered last (applied first) out of the defaults.
+        registerPositioner(new LaunchingActivityPositioner(supervisor));
+    }
+
+    /**
+     * Returns the position calculated by the registered positioners
+     * @param task      The {@link TaskRecord} currently being positioned.
+     * @param layout    The specified {@link WindowLayout}.
+     * @param activity  The {@link ActivityRecord} currently being positioned.
+     * @param options   The {@link ActivityOptions} specified for the activity.
+     * @param result    The resulting bounds. If no bounds are set, {@link Rect#isEmpty()} will be
+     *                  true.
+     */
+    void calculateBounds(TaskRecord task, WindowLayout layout, ActivityRecord activity,
+            ActivityOptions options, Rect result) {
+        result.setEmpty();
+
+        // We start at the last registered {@link LaunchingBoundsPositioner} as this represents
+        // The positioner closest to the product level. Moving back through the list moves closer to
+        // the platform logic.
+        for (int i = mPositioners.size() - 1; i >= 0; --i) {
+            mTmpResult.setEmpty();
+            mTmpCurrent.set(result);
+            final LaunchingBoundsPositioner positioner = mPositioners.get(i);
+
+            switch(positioner.onCalculateBounds(task, layout, activity, options, mTmpCurrent,
+                    mTmpResult)) {
+                case RESULT_SKIP:
+                    // Do not apply any results when we are told to skip
+                    continue;
+                case RESULT_DONE:
+                    // Set result and return immediately.
+                    result.set(mTmpResult);
+                    return;
+                case RESULT_CONTINUE:
+                    // Set result and continue
+                    result.set(mTmpResult);
+                    break;
+            }
+        }
+    }
+
+    /**
+     * A convenience method for laying out a task.
+     * @return {@code true} if bounds were set on the task. {@code false} otherwise.
+     */
+    boolean layoutTask(TaskRecord task, WindowLayout layout) {
+        calculateBounds(task, layout, null /*activity*/, null /*options*/, mTmpRect);
+
+        if (mTmpRect.isEmpty()) {
+            return false;
+        }
+
+        task.updateOverrideConfiguration(mTmpRect);
+
+        return true;
+    }
+
+    /**
+     * Adds a positioner to participate in future bounds calculation. Note that the last registered
+     * {@link LaunchingBoundsPositioner} will be the first to calculate the bounds.
+     */
+    void registerPositioner(LaunchingBoundsPositioner positioner) {
+        if (mPositioners.contains(positioner)) {
+            return;
+        }
+
+        mPositioners.add(positioner);
+    }
+
+    /**
+     * An interface implemented by those wanting to participate in bounds calculation.
+     */
+    interface LaunchingBoundsPositioner {
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef({RESULT_SKIP, RESULT_DONE, RESULT_CONTINUE})
+        @interface Result {}
+
+        // Returned when the positioner does not want to influence the bounds calculation
+        int RESULT_SKIP = 0;
+        // Returned when the positioner has changed the bounds and would like its results to be the
+        // final bounds applied.
+        int RESULT_DONE = 1;
+        // Returned when the positioner has changed the bounds but is okay with other positioners
+        // influencing the bounds.
+        int RESULT_CONTINUE = 2;
+
+        /**
+         * Called when asked to calculate bounds.
+         * @param task      The {@link TaskRecord} currently being positioned.
+         * @param layout    The specified {@link WindowLayout}.
+         * @param activity  The {@link ActivityRecord} currently being positioned.
+         * @param options   The {@link ActivityOptions} specified for the activity.
+         * @param current   The current bounds. This can differ from the initial bounds as it
+         *                  represents the modified bounds up to this point.
+         * @param result    The {@link Rect} which the positioner should return its modified bounds.
+         *                  Any merging of the current bounds should be already applied to this
+         *                  value as well before returning.
+         * @return A {@link Result} representing the result of the bounds calculation.
+         */
+        @Result
+        int onCalculateBounds(TaskRecord task, WindowLayout layout, ActivityRecord activity,
+                ActivityOptions options, Rect current, Rect result);
+    }
+}
diff --git a/services/core/java/com/android/server/am/LaunchingTaskPositioner.java b/services/core/java/com/android/server/am/LaunchingTaskPositioner.java
index 0dc73e9..6389075 100644
--- a/services/core/java/com/android/server/am/LaunchingTaskPositioner.java
+++ b/services/core/java/com/android/server/am/LaunchingTaskPositioner.java
@@ -19,7 +19,7 @@
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
 
-import android.annotation.Nullable;
+import android.app.ActivityOptions;
 import android.content.pm.ActivityInfo;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -36,8 +36,10 @@
  * and compares corners of the task with corners of existing tasks. If some two pairs of corners are
  * sufficiently close enough, it shifts the bounds of the new task and tries again. When it exhausts
  * all possible shifts, it gives up and puts the task in the original position.
+ *
+ * Note that the only gravities of concern are the corners and the center.
  */
-class LaunchingTaskPositioner {
+class LaunchingTaskPositioner implements LaunchingBoundsController.LaunchingBoundsPositioner {
     private static final String TAG = TAG_WITH_CLASS_NAME ? "LaunchingTaskPositioner" : TAG_AM;
 
     // Determines how close window frames/corners have to be to call them colliding.
@@ -74,44 +76,50 @@
      * Tries to set task's bound in a way that it won't collide with any other task. By colliding
      * we mean that two tasks have left-top corner very close to each other, so one might get
      * obfuscated by the other one.
-     *
-     * @param task Task for which we want to find bounds that won't collide with other.
-     * @param tasks Existing tasks with which we don't want to collide.
-     * @param windowLayout Optional information from the client about how it would like to be sized
-     *                      and positioned.
      */
-    void updateDefaultBounds(TaskRecord task, ArrayList<TaskRecord> tasks,
-            @Nullable ActivityInfo.WindowLayout windowLayout) {
+    @Override
+    public int onCalculateBounds(TaskRecord task, ActivityInfo.WindowLayout layout,
+            ActivityRecord activity, ActivityOptions options, Rect current, Rect result) {
+        // We can only apply positioning if we're in a freeform stack.
+        if (task == null || task.getStack() == null || !task.inFreeformWindowingMode()) {
+            return RESULT_SKIP;
+        }
+
+        final ArrayList<TaskRecord> tasks = task.getStack().getAllTasks();
+
         updateAvailableRect(task, mAvailableRect);
 
-        if (windowLayout == null) {
-            positionCenter(task, tasks, mAvailableRect, getFreeformWidth(mAvailableRect),
-                    getFreeformHeight(mAvailableRect));
-            return;
+        if (layout == null) {
+            positionCenter(tasks, mAvailableRect, getFreeformWidth(mAvailableRect),
+                    getFreeformHeight(mAvailableRect), result);
+            return RESULT_CONTINUE;
         }
-        int width = getFinalWidth(windowLayout, mAvailableRect);
-        int height = getFinalHeight(windowLayout, mAvailableRect);
-        int verticalGravity = windowLayout.gravity & Gravity.VERTICAL_GRAVITY_MASK;
-        int horizontalGravity = windowLayout.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+
+        int width = getFinalWidth(layout, mAvailableRect);
+        int height = getFinalHeight(layout, mAvailableRect);
+        int verticalGravity = layout.gravity & Gravity.VERTICAL_GRAVITY_MASK;
+        int horizontalGravity = layout.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
         if (verticalGravity == Gravity.TOP) {
             if (horizontalGravity == Gravity.RIGHT) {
-                positionTopRight(task, tasks, mAvailableRect, width, height);
+                positionTopRight(tasks, mAvailableRect, width, height, result);
             } else {
-                positionTopLeft(task, tasks, mAvailableRect, width, height);
+                positionTopLeft(tasks, mAvailableRect, width, height, result);
             }
         } else if (verticalGravity == Gravity.BOTTOM) {
             if (horizontalGravity == Gravity.RIGHT) {
-                positionBottomRight(task, tasks, mAvailableRect, width, height);
+                positionBottomRight(tasks, mAvailableRect, width, height, result);
             } else {
-                positionBottomLeft(task, tasks, mAvailableRect, width, height);
+                positionBottomLeft(tasks, mAvailableRect, width, height, result);
             }
         } else {
             // Some fancy gravity setting that we don't support yet. We just put the activity in the
             // center.
-            Slog.w(TAG, "Received unsupported gravity: " + windowLayout.gravity
+            Slog.w(TAG, "Received unsupported gravity: " + layout.gravity
                     + ", positioning in the center instead.");
-            positionCenter(task, tasks, mAvailableRect, width, height);
+            positionCenter(tasks, mAvailableRect, width, height, result);
         }
+
+        return RESULT_CONTINUE;
     }
 
     private void updateAvailableRect(TaskRecord task, Rect availableRect) {
@@ -179,50 +187,50 @@
         return height;
     }
 
-    private void positionBottomLeft(TaskRecord task, ArrayList<TaskRecord> tasks,
-            Rect availableRect, int width, int height) {
+    private void positionBottomLeft(ArrayList<TaskRecord> tasks, Rect availableRect, int width,
+            int height, Rect result) {
         mTmpProposal.set(availableRect.left, availableRect.bottom - height,
                 availableRect.left + width, availableRect.bottom);
-        position(task, tasks, availableRect, mTmpProposal, !ALLOW_RESTART,
-                SHIFT_POLICY_HORIZONTAL_RIGHT);
+        position(tasks, availableRect, mTmpProposal, !ALLOW_RESTART, SHIFT_POLICY_HORIZONTAL_RIGHT,
+                result);
     }
 
-    private void positionBottomRight(TaskRecord task, ArrayList<TaskRecord> tasks,
-            Rect availableRect, int width, int height) {
+    private void positionBottomRight(ArrayList<TaskRecord> tasks, Rect availableRect, int width,
+            int height, Rect result) {
         mTmpProposal.set(availableRect.right - width, availableRect.bottom - height,
                 availableRect.right, availableRect.bottom);
-        position(task, tasks, availableRect, mTmpProposal, !ALLOW_RESTART,
-                SHIFT_POLICY_HORIZONTAL_LEFT);
+        position(tasks, availableRect, mTmpProposal, !ALLOW_RESTART, SHIFT_POLICY_HORIZONTAL_LEFT,
+                result);
     }
 
-    private void positionTopLeft(TaskRecord task, ArrayList<TaskRecord> tasks,
-            Rect availableRect, int width, int height) {
+    private void positionTopLeft(ArrayList<TaskRecord> tasks, Rect availableRect, int width,
+            int height, Rect result) {
         mTmpProposal.set(availableRect.left, availableRect.top,
                 availableRect.left + width, availableRect.top + height);
-        position(task, tasks, availableRect, mTmpProposal, !ALLOW_RESTART,
-                SHIFT_POLICY_HORIZONTAL_RIGHT);
+        position(tasks, availableRect, mTmpProposal, !ALLOW_RESTART, SHIFT_POLICY_HORIZONTAL_RIGHT,
+                result);
     }
 
-    private void positionTopRight(TaskRecord task, ArrayList<TaskRecord> tasks,
-            Rect availableRect, int width, int height) {
+    private void positionTopRight(ArrayList<TaskRecord> tasks, Rect availableRect, int width,
+            int height, Rect result) {
         mTmpProposal.set(availableRect.right - width, availableRect.top,
                 availableRect.right, availableRect.top + height);
-        position(task, tasks, availableRect, mTmpProposal, !ALLOW_RESTART,
-                SHIFT_POLICY_HORIZONTAL_LEFT);
+        position(tasks, availableRect, mTmpProposal, !ALLOW_RESTART, SHIFT_POLICY_HORIZONTAL_LEFT,
+                result);
     }
 
-    private void positionCenter(TaskRecord task, ArrayList<TaskRecord> tasks,
-            Rect availableRect, int width, int height) {
+    private void positionCenter(ArrayList<TaskRecord> tasks, Rect availableRect, int width,
+            int height, Rect result) {
         final int defaultFreeformLeft = getFreeformStartLeft(availableRect);
         final int defaultFreeformTop = getFreeformStartTop(availableRect);
         mTmpProposal.set(defaultFreeformLeft, defaultFreeformTop,
                 defaultFreeformLeft + width, defaultFreeformTop + height);
-        position(task, tasks, availableRect, mTmpProposal, ALLOW_RESTART,
-                SHIFT_POLICY_DIAGONAL_DOWN);
+        position(tasks, availableRect, mTmpProposal, ALLOW_RESTART, SHIFT_POLICY_DIAGONAL_DOWN,
+                result);
     }
 
-    private void position(TaskRecord task, ArrayList<TaskRecord> tasks, Rect availableRect,
-            Rect proposal, boolean allowRestart, int shiftPolicy) {
+    private void position(ArrayList<TaskRecord> tasks, Rect availableRect,
+            Rect proposal, boolean allowRestart, int shiftPolicy, Rect result) {
         mTmpOriginal.set(proposal);
         boolean restarted = false;
         while (boundsConflict(proposal, tasks)) {
@@ -252,7 +260,7 @@
                 break;
             }
         }
-        task.updateOverrideConfiguration(proposal);
+        result.set(proposal);
     }
 
     private boolean shiftedTooFar(Rect start, Rect availableRect, int shiftPolicy) {
diff --git a/services/core/java/com/android/server/am/TaskRecord.java b/services/core/java/com/android/server/am/TaskRecord.java
index c451235..899bf79 100644
--- a/services/core/java/com/android/server/am/TaskRecord.java
+++ b/services/core/java/com/android/server/am/TaskRecord.java
@@ -707,7 +707,7 @@
             } else if (toStackWindowingMode == WINDOWING_MODE_FREEFORM) {
                 Rect bounds = getLaunchBounds();
                 if (bounds == null) {
-                    toStack.layoutTaskInStack(this, null);
+                    mService.mStackSupervisor.getLaunchingBoundsController().layoutTask(this, null);
                     bounds = mBounds;
                 }
                 kept = resize(bounds, RESIZE_MODE_FORCED, !mightReplaceWindow, deferResume);
@@ -2089,7 +2089,7 @@
             if (mLastNonFullscreenBounds != null) {
                 updateOverrideConfiguration(mLastNonFullscreenBounds);
             } else {
-                inStack.layoutTaskInStack(this, null);
+                mService.mStackSupervisor.getLaunchingBoundsController().layoutTask(this, null);
             }
         } else {
             updateOverrideConfiguration(inStack.mBounds);
diff --git a/services/tests/servicestests/src/com/android/server/am/LaunchingActivityPositionerTests.java b/services/tests/servicestests/src/com/android/server/am/LaunchingActivityPositionerTests.java
new file mode 100644
index 0000000..0007e8a
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/LaunchingActivityPositionerTests.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2017 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.am;
+
+import android.app.ActivityOptions;
+import android.content.ComponentName;
+import android.content.pm.ActivityInfo;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.runner.RunWith;
+import org.junit.Before;
+import org.junit.Test;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner.RESULT_DONE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.doAnswer;
+
+import static com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner.RESULT_SKIP;
+
+/**
+ * Tests for exercising resizing bounds due to activity options.
+ *
+ * Build/Install/Run:
+ *  bit FrameworksServicesTests:com.android.server.am.LaunchingActivityPositionerTests
+ */
+@MediumTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class LaunchingActivityPositionerTests extends ActivityTestsBase {
+    private final ComponentName testActivityComponent =
+            ComponentName.unflattenFromString("com.foo/.BarActivity");
+
+    private LaunchingActivityPositioner mPositioner;
+    private ActivityManagerService mService;
+    private ActivityStack mStack;
+    private TaskRecord mTask;
+    private ActivityRecord mActivity;
+
+    private Rect mCurrent;
+    private Rect mResult;
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mService = createActivityManagerService();
+        mPositioner = new LaunchingActivityPositioner(mService.mStackSupervisor);
+        mCurrent = new Rect();
+        mResult = new Rect();
+
+
+        mStack = mService.mStackSupervisor.getDefaultDisplay().createStack(
+                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+        mTask = createTask(mService.mStackSupervisor, testActivityComponent, mStack);
+        mActivity = createActivity(mService, testActivityComponent, mTask);
+    }
+
+
+    @Test
+    public void testSkippedInvocations() throws Exception {
+        // No specified activity should be ignored
+        assertEquals(RESULT_SKIP, mPositioner.onCalculateBounds(null /*task*/, null /*layout*/,
+                null /*activity*/, null /*options*/, mCurrent, mResult));
+
+        // No specified activity options should be ignored
+        assertEquals(RESULT_SKIP, mPositioner.onCalculateBounds(null /*task*/, null /*layout*/,
+                mActivity, null /*options*/, mCurrent, mResult));
+
+        // launch bounds specified should be ignored.
+        final ActivityOptions options = ActivityOptions.makeBasic();
+        assertEquals(RESULT_SKIP, mPositioner.onCalculateBounds(null /*task*/, null /*layout*/,
+                mActivity, options /*options*/, mCurrent, mResult));
+
+        // Non-resizeable records should be ignored
+        mActivity.info.resizeMode = ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
+        assertFalse(mActivity.isResizeable());
+        assertEquals(RESULT_SKIP, mPositioner.onCalculateBounds(null /*task*/, null /*layout*/,
+                mActivity, options /*options*/, mCurrent, mResult));
+
+        // make record resizeable
+        mActivity.info.resizeMode = ActivityInfo.RESIZE_MODE_RESIZEABLE;
+        assertTrue(mActivity.isResizeable());
+
+        assertEquals(RESULT_SKIP, mPositioner.onCalculateBounds(null /*task*/, null /*layout*/,
+                mActivity, options /*options*/, mCurrent, mResult));
+
+        // Does not support freeform
+        mService.mSupportsFreeformWindowManagement = false;
+        assertFalse(mService.mStackSupervisor.canUseActivityOptionsLaunchBounds(options));
+        assertEquals(RESULT_SKIP, mPositioner.onCalculateBounds(null /*task*/, null /*layout*/,
+                mActivity, options /*options*/, mCurrent, mResult));
+
+        mService.mSupportsFreeformWindowManagement = true;
+        options.setLaunchBounds(new Rect());
+        assertTrue(mService.mStackSupervisor.canUseActivityOptionsLaunchBounds(options));
+
+        // Invalid bounds
+        assertEquals(RESULT_SKIP, mPositioner.onCalculateBounds(null /*task*/, null /*layout*/,
+                mActivity, options /*options*/, mCurrent, mResult));
+        options.setLaunchBounds(new Rect(0, 0, -1, -1));
+        assertEquals(RESULT_SKIP, mPositioner.onCalculateBounds(null /*task*/, null /*layout*/,
+                mActivity, options /*options*/, mCurrent, mResult));
+
+        // Valid bounds should cause the positioner to be applied.
+        options.setLaunchBounds(new Rect(0, 0, 100, 100));
+        assertEquals(RESULT_DONE, mPositioner.onCalculateBounds(null /*task*/, null /*layout*/,
+                mActivity, options /*options*/, mCurrent, mResult));
+    }
+
+    @Test
+    public void testBoundsExtraction() throws Exception {
+        // Make activity resizeable and enable freeform mode.
+        mActivity.info.resizeMode = ActivityInfo.RESIZE_MODE_RESIZEABLE;
+        mService.mSupportsFreeformWindowManagement = true;
+
+        ActivityOptions options = ActivityOptions.makeBasic();
+        final Rect proposedBounds = new Rect(20, 30, 45, 40);
+        options.setLaunchBounds(proposedBounds);
+
+        assertEquals(RESULT_DONE, mPositioner.onCalculateBounds(null /*task*/, null /*layout*/,
+                mActivity, options /*options*/, mCurrent, mResult));
+        assertEquals(mResult, proposedBounds);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/LaunchingBoundsControllerTests.java b/services/tests/servicestests/src/com/android/server/am/LaunchingBoundsControllerTests.java
new file mode 100644
index 0000000..f24a273
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/LaunchingBoundsControllerTests.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2017 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.am;
+
+import android.app.ActivityOptions;
+import android.content.pm.ActivityInfo;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.runner.RunWith;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import static com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner.RESULT_DONE;
+import static com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner.RESULT_CONTINUE;
+import static com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner.RESULT_SKIP;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Tests for exercising {@link LaunchingBoundsController}.
+ *
+ * Build/Install/Run:
+ *  bit FrameworksServicesTests:com.android.server.am.LaunchingBoundsControllerTests
+ */
+@MediumTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class LaunchingBoundsControllerTests extends ActivityTestsBase {
+    private LaunchingBoundsController mController;
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mController = new LaunchingBoundsController();
+    }
+
+    /**
+     * Ensures positioners further down the chain are not called when RESULT_DONE is returned.
+     */
+    @Test
+    public void testEarlyExit() {
+        final LaunchingBoundsPositioner ignoredPositioner = mock(LaunchingBoundsPositioner.class);
+        final LaunchingBoundsPositioner earlyExitPositioner =
+                (task, layout, activity, options, current, result) -> RESULT_DONE;
+
+        mController.registerPositioner(ignoredPositioner);
+        mController.registerPositioner(earlyExitPositioner);
+
+        mController.calculateBounds(null /*task*/, null /*layout*/, null /*activity*/,
+                null /*options*/, new Rect());
+        verify(ignoredPositioner, never()).onCalculateBounds(any(), any(), any(), any(), any(),
+                any());
+    }
+
+    /**
+     * Ensures that positioners are called in the correct order.
+     */
+    @Test
+    public void testRegistration() {
+        LaunchingBoundsPositioner earlyExitPositioner =
+                new InstrumentedPositioner(RESULT_DONE, new Rect());
+
+        final LaunchingBoundsPositioner firstPositioner = spy(earlyExitPositioner);
+
+        mController.registerPositioner(firstPositioner);
+
+        mController.calculateBounds(null /*task*/, null /*layout*/, null /*activity*/,
+                null /*options*/, new Rect());
+        verify(firstPositioner, times(1)).onCalculateBounds(any(), any(), any(), any(), any(),
+                any());
+
+        final LaunchingBoundsPositioner secondPositioner = spy(earlyExitPositioner);
+
+        mController.registerPositioner(secondPositioner);
+
+        mController.calculateBounds(null /*task*/, null /*layout*/, null /*activity*/,
+                null /*options*/, new Rect());
+        verify(firstPositioner, times(1)).onCalculateBounds(any(), any(), any(), any(), any(),
+                any());
+        verify(secondPositioner, times(1)).onCalculateBounds(any(), any(), any(), any(), any(),
+                any());
+    }
+
+    /**
+     * Makes sure positioners further down the registration chain are called.
+     */
+    @Test
+    public void testPassThrough() {
+        final LaunchingBoundsPositioner positioner1 = mock(LaunchingBoundsPositioner.class);
+        final InstrumentedPositioner positioner2 = new InstrumentedPositioner(RESULT_CONTINUE,
+                new Rect (0, 0, 30, 20));
+
+        mController.registerPositioner(positioner1);
+        mController.registerPositioner(positioner2);
+
+        mController.calculateBounds(null /*task*/, null /*layout*/, null /*activity*/,
+                null /*options*/, new Rect());
+
+        verify(positioner1, times(1)).onCalculateBounds(any(), any(), any(), any(),
+                eq(positioner2.getLaunchBounds()), any());
+    }
+
+    /**
+     * Ensures skipped results are not propagated.
+     */
+    @Test
+    public void testSkip() {
+        final InstrumentedPositioner positioner1 =
+                new InstrumentedPositioner(RESULT_SKIP, new Rect(0, 0, 10, 10));
+
+
+        final InstrumentedPositioner positioner2 =
+                new InstrumentedPositioner(RESULT_CONTINUE, new Rect(0, 0, 20, 30));
+
+        mController.registerPositioner(positioner1);
+        mController.registerPositioner(positioner2);
+
+        final Rect resultBounds = new Rect();
+
+        mController.calculateBounds(null /*task*/, null /*layout*/, null /*activity*/,
+                null /*options*/, resultBounds);
+
+        assertEquals(resultBounds, positioner2.getLaunchBounds());
+    }
+
+    public static class InstrumentedPositioner implements LaunchingBoundsPositioner {
+        private int mReturnVal;
+        private Rect mBounds;
+        InstrumentedPositioner(int returnVal, Rect bounds) {
+            mReturnVal = returnVal;
+            mBounds = bounds;
+        }
+
+        @Override
+        public int onCalculateBounds(TaskRecord task, ActivityInfo.WindowLayout layout,
+                ActivityRecord activity, ActivityOptions options, Rect current, Rect result) {
+            result.set(mBounds);
+            return mReturnVal;
+        }
+
+        Rect getLaunchBounds() {
+            return mBounds;
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/LaunchBoundsTests.java b/services/tests/servicestests/src/com/android/server/am/LaunchingTaskPositionerTests.java
similarity index 81%
rename from services/tests/servicestests/src/com/android/server/am/LaunchBoundsTests.java
rename to services/tests/servicestests/src/com/android/server/am/LaunchingTaskPositionerTests.java
index e6d6831..0d64981 100644
--- a/services/tests/servicestests/src/com/android/server/am/LaunchBoundsTests.java
+++ b/services/tests/servicestests/src/com/android/server/am/LaunchingTaskPositionerTests.java
@@ -17,15 +17,12 @@
 package com.android.server.am;
 
 import android.content.ComponentName;
-import android.content.pm.ActivityInfo;
 import android.content.pm.ActivityInfo.WindowLayout;
-import android.graphics.Point;
 import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
 import android.support.test.filters.MediumTest;
 import android.support.test.runner.AndroidJUnit4;
 
-import android.view.Display;
 import android.view.Gravity;
 import org.junit.runner.RunWith;
 import org.junit.Before;
@@ -33,10 +30,11 @@
 
 import org.mockito.invocation.InvocationOnMock;
 
-import java.util.ArrayList;
-
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+
+import static com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner.RESULT_CONTINUE;
+
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -48,16 +46,14 @@
  * Tests for exercising resizing bounds.
  *
  * Build/Install/Run:
- *  bit FrameworksServicesTests:com.android.server.am.LaunchBoundsTests
+ *  bit FrameworksServicesTests:com.android.server.am.LaunchingTaskPositionerTests
  */
 @MediumTest
 @Presubmit
 @RunWith(AndroidJUnit4.class)
-public class LaunchBoundsTests extends ActivityTestsBase {
+public class LaunchingTaskPositionerTests extends ActivityTestsBase {
     private final ComponentName testActivityComponent =
             ComponentName.unflattenFromString("com.foo/.BarActivity");
-    private final ComponentName testActivityComponent2 =
-            ComponentName.unflattenFromString("com.foo/.BarActivity2");
 
     private final static int STACK_WIDTH = 100;
     private final static int STACK_HEIGHT = 200;
@@ -68,6 +64,11 @@
     private ActivityStack mStack;
     private TaskRecord mTask;
 
+    private LaunchingTaskPositioner mPositioner;
+
+    private Rect mCurrent;
+    private Rect mResult;
+
     @Before
     @Override
     public void setUp() throws Exception {
@@ -81,6 +82,11 @@
         // We must create the task after resizing to make sure it does not inherit the stack
         // dimensions on resize.
         mTask = createTask(mService.mStackSupervisor, testActivityComponent, mStack);
+
+        mPositioner = new LaunchingTaskPositioner();
+
+        mResult = new Rect();
+        mCurrent = new Rect();
     }
 
     /**
@@ -101,12 +107,9 @@
      */
     @Test
     public void testLaunchNoWindowLayout() throws Exception {
-        final Rect expectedTaskBounds = getDefaultBounds(Gravity.NO_GRAVITY);
-
-        mStack.layoutTaskInStack(mTask, null);
-
-        // We expect the task to be placed in the middle of the screen with margins applied.
-        assertEquals(mTask.mBounds, expectedTaskBounds);
+        assertEquals(RESULT_CONTINUE, mPositioner.onCalculateBounds(mTask, null /*layout*/,
+                null /*record*/, null /*options*/, mCurrent, mResult));
+        assertEquals(getDefaultBounds(Gravity.NO_GRAVITY), mResult);
     }
 
     /**
@@ -116,11 +119,10 @@
      */
     @Test
     public void testlaunchEmptyWindowLayout() throws Exception {
-        final Rect expectedTaskBounds = getDefaultBounds(Gravity.NO_GRAVITY);
-
-        WindowLayout layout = new WindowLayout(0, 0, 0, 0, 0, 0, 0);
-        mStack.layoutTaskInStack(mTask, layout);
-        assertEquals(mTask.mBounds, expectedTaskBounds);
+        assertEquals(RESULT_CONTINUE, mPositioner.onCalculateBounds(mTask,
+                new WindowLayout(0, 0, 0, 0, Gravity.NO_GRAVITY, 0, 0), null /*activity*/,
+                null /*options*/, mCurrent, mResult));
+        assertEquals(mResult, getDefaultBounds(Gravity.NO_GRAVITY));
     }
 
     /**
@@ -149,9 +151,15 @@
     }
 
     private void testGravity(int gravity) {
-        final WindowLayout gravityLayout = new WindowLayout(0, 0, 0, 0, gravity, 0, 0);
-        mStack.layoutTaskInStack(mTask, gravityLayout);
-        assertEquals(mTask.mBounds, getDefaultBounds(gravity));
+        try {
+            assertEquals(RESULT_CONTINUE, mPositioner.onCalculateBounds(mTask,
+                    new WindowLayout(0, 0, 0, 0, gravity, 0, 0), null /*activity*/,
+                    null /*options*/, mCurrent, mResult));
+            assertEquals(mResult, getDefaultBounds(gravity));
+        } finally {
+            mCurrent.setEmpty();
+            mResult.setEmpty();
+        }
     }
 
     /**
@@ -174,7 +182,7 @@
         final WindowLayout layout = new WindowLayout(0, 0, 0, 0, gravity, 0, 0);
 
         // layout first task
-        mStack.layoutTaskInStack(mTask, layout /*windowLayout*/);
+        mService.mStackSupervisor.getLaunchingBoundsController().layoutTask(mTask, layout);
 
         // Second task will be laid out on top of the first so starting bounds is the same.
         final Rect expectedBounds = new Rect(mTask.mBounds);
@@ -192,7 +200,9 @@
                     mStack);
 
             // layout second task
-            mStack.layoutTaskInStack(secondTask, layout /*windowLayout*/);
+            assertEquals(RESULT_CONTINUE,
+                    mPositioner.onCalculateBounds(secondTask, layout, null /*activity*/,
+                            null /*options*/, mCurrent, mResult));
 
             if ((gravity & (Gravity.TOP | Gravity.RIGHT)) == (Gravity.TOP | Gravity.RIGHT)
                     || (gravity & (Gravity.BOTTOM | Gravity.RIGHT))
@@ -207,7 +217,7 @@
                         LaunchingTaskPositioner.getVerticalStep(mStack.mBounds));
             }
 
-            assertEquals(secondTask.mBounds, expectedBounds);
+            assertEquals(mResult, expectedBounds);
         } finally {
             // Remove task and activity to prevent influencing future tests
             if (activity != null) {