Have better separation between adding, positioning, and reparenting task

Several methods in activity manager and window manager performed adding,
positioning, and reparenting a task operation and sometimes failed silently
when things don't work due the callers using the methods for a particular
operation, but getting a different operation due to programmer error.
This CL better separate the methods responsible for adding, positioning, and
reparenting a task and also fails hard when there is an error.

Test: bit FrameworksServicesTests:com.android.server.wm.TaskWindowContainerControllerTests
Test: Manual testing existing PiP doesn't leave the device in a bad state.
Bug: 34260633
Change-Id: Id64367da30fc6214eb6f95b2bd5e58ed0e953a88
(cherry picked from commit c5cc301689649695e03f502e7d1c1492ef5e5d1e)
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index c87412e..7fd91cb 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -9572,7 +9572,7 @@
                 boolean preserveWindow = (resizeMode & RESIZE_MODE_PRESERVE_WINDOW) != 0;
                 if (stackId != task.getStackId()) {
                     mStackSupervisor.moveTaskToStackUncheckedLocked(task, stackId, ON_TOP,
-                            !FORCE_FOCUS, "resizeTask", true /* allowStackOnTop */);
+                            !FORCE_FOCUS, "resizeTask");
                     preserveWindow = false;
                 }
 
@@ -10020,7 +10020,7 @@
                 // Defer the resume so resume/pausing while moving stacks is dangerous.
                 mStackSupervisor.moveTaskToStackLocked(topTask.taskId, DOCKED_STACK_ID,
                         false /* toTop */, !FORCE_FOCUS, "swapDockedAndFullscreenStack",
-                        ANIMATE, true /* deferResume */, true /* allowStackOnTop */);
+                        ANIMATE, true /* deferResume */);
                 final int size = tasks.size();
                 for (int i = 0; i < size; i++) {
                     final int id = tasks.get(i).taskId;
@@ -10029,8 +10029,7 @@
                     }
                     mStackSupervisor.moveTaskToStackLocked(id,
                             FULLSCREEN_WORKSPACE_STACK_ID, true /* toTop */, !FORCE_FOCUS,
-                            "swapDockedAndFullscreenStack", ANIMATE, true /* deferResume */,
-                            true /* allowStackOnTop */);
+                            "swapDockedAndFullscreenStack", ANIMATE, true /* deferResume */);
                 }
 
                 // Because we deferred the resume, to avoid conflicts with stack switches while
@@ -10071,7 +10070,7 @@
                 mWindowManager.setDockedStackCreateState(createMode, initialBounds);
                 final boolean moved = mStackSupervisor.moveTaskToStackLocked(
                         taskId, DOCKED_STACK_ID, toTop, !FORCE_FOCUS, "moveTaskToDockedStack",
-                        animate, DEFER_RESUME, true /* allowStackOnTop */);
+                        animate, DEFER_RESUME);
                 if (moved) {
                     if (moveHomeStackFront) {
                         mStackSupervisor.moveHomeStackToFront("moveTaskToDockedStack");
@@ -10187,10 +10186,24 @@
             try {
                 if (DEBUG_STACK) Slog.d(TAG_STACK, "positionTaskInStack: positioning task="
                         + taskId + " in stackId=" + stackId + " at position=" + position);
+                final TaskRecord task = mStackSupervisor.anyTaskForIdLocked(taskId);
+                if (task == null) {
+                    throw new IllegalArgumentException("positionTaskInStack: no task for id="
+                            + taskId);
+                }
+
                 final ActivityStack stack = mStackSupervisor.getStack(stackId, CREATE_IF_NEEDED,
                         !ON_TOP);
 
-                stack.positionChildAt(taskId, position);
+                // TODO: Have the callers of this API call a separate reparent method if that is
+                // what they intended to do vs. having this method also do reparenting.
+                if (task.getStack() == stack) {
+                    // Change position in current stack.
+                    stack.positionChildAt(task, position);
+                } else {
+                    // Reparent to new stack.
+                    task.reparent(stackId, position, "positionTaskInStack");
+                }
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java
index abcaa24..4df0cb1 100644
--- a/services/core/java/com/android/server/am/ActivityStack.java
+++ b/services/core/java/com/android/server/am/ActivityStack.java
@@ -72,6 +72,8 @@
 import static com.android.server.wm.AppTransition.TRANSIT_TASK_OPEN_BEHIND;
 import static com.android.server.wm.AppTransition.TRANSIT_TASK_TO_BACK;
 import static com.android.server.wm.AppTransition.TRANSIT_TASK_TO_FRONT;
+import static java.lang.Integer.MAX_VALUE;
+import static java.lang.Integer.MIN_VALUE;
 
 import android.app.Activity;
 import android.app.ActivityManager;
@@ -2534,6 +2536,30 @@
         return null;
     }
 
+    /** Returns the position the input task should be placed in this stack. */
+    int getAdjustedPositionForTask(TaskRecord task, int suggestedPosition,
+            ActivityRecord starting) {
+
+        int maxPosition = mTaskHistory.size();
+        if ((starting != null && starting.okToShowLocked())
+                || (starting == null && task.okToShowLocked())) {
+            // If the task or starting activity can be shown, then whatever position is okay.
+            return Math.min(suggestedPosition, maxPosition);
+        }
+
+        // The task can't be shown, put non-current user tasks below current user tasks.
+        while (maxPosition > 0) {
+            final TaskRecord tmpTask = mTaskHistory.get(maxPosition - 1);
+            if (!mStackSupervisor.isCurrentProfileLocked(tmpTask.userId)
+                    || tmpTask.topRunningActivityLocked() == null) {
+                break;
+            }
+            maxPosition--;
+        }
+
+        return  Math.min(suggestedPosition, maxPosition);
+    }
+
     /**
      * Used from {@link ActivityStack#positionTask(TaskRecord, int)}.
      * @see ActivityManagerService#positionTaskInStack(int, int, int).
@@ -2543,32 +2569,25 @@
             insertTaskAtTop(task, null);
             return;
         }
-        // Calculate maximum possible position for this task.
-        int maxPosition = mTaskHistory.size();
-        if (!task.okToShowLocked()) {
-            // Put non-current user tasks below current user tasks.
-            while (maxPosition > 0) {
-                final TaskRecord tmpTask = mTaskHistory.get(maxPosition - 1);
-                if (!mStackSupervisor.isCurrentProfileLocked(tmpTask.userId)
-                        || tmpTask.topRunningActivityLocked() == null) {
-                    break;
-                }
-                maxPosition--;
-            }
-        }
-        position = Math.min(position, maxPosition);
+        position = getAdjustedPositionForTask(task, position, null /* starting */);
         mTaskHistory.remove(task);
         mTaskHistory.add(position, task);
-        task.positionWindowContainerAt(mStackId, position);
+        task.positionWindowContainerAt(position);
         updateTaskMovement(task, true);
     }
 
-    private void insertTaskAtTop(TaskRecord task, ActivityRecord newActivity) {
-        insertTaskAtTop(task, newActivity, true /* allowStackOnTop */);
+    private void insertTaskAtTop(TaskRecord task, ActivityRecord starting) {
+        updateTaskReturnToForTopInsertion(task);
+        // TODO: Better place to put all the code below...may be addTask...
+        mTaskHistory.remove(task);
+        // Now put task at top.
+        final int position = getAdjustedPositionForTask(task, mTaskHistory.size(), starting);
+        mTaskHistory.add(position, task);
+        updateTaskMovement(task, true);
+        task.moveWindowContainerToTop(true /* includingParents */);
     }
 
-    private void insertTaskAtTop(TaskRecord task, ActivityRecord newActivity,
-            boolean allowStackOnTop) {
+    private void updateTaskReturnToForTopInsertion(TaskRecord task) {
         boolean isLastTaskOverHome = false;
         // If the moving task is over home stack, transfer its return type to next task
         if (task.isOverHomeStack()) {
@@ -2585,49 +2604,27 @@
         if (isOnHomeDisplay()) {
             ActivityStack lastStack = mStackSupervisor.getLastStack();
             final boolean fromHomeOrRecents = lastStack.isHomeOrRecentsStack();
-            final boolean fromOnTopLauncher = lastStack.topTask() != null &&
-                    lastStack.topTask().isOnTopLauncher();
+            final TaskRecord topTask = lastStack.topTask();
+            final boolean fromOnTopLauncher = topTask != null && topTask.isOnTopLauncher();
             if (fromOnTopLauncher) {
                 // Since an on-top launcher will is moved to back when tasks are launched from it,
                 // those tasks should first try to return to a non-home activity.
                 // This also makes sure that non-home activities are visible under a transparent
                 // non-home activity.
                 task.setTaskToReturnTo(APPLICATION_ACTIVITY_TYPE);
-            } else if (!isHomeOrRecentsStack() && (fromHomeOrRecents || topTask() != task)) {
+            } else if (!isHomeOrRecentsStack() && (fromHomeOrRecents || topTask != task)) {
                 // If it's a last task over home - we default to keep its return to type not to
                 // make underlying task focused when this one will be finished.
                 int returnToType = isLastTaskOverHome
                         ? task.getTaskToReturnTo() : APPLICATION_ACTIVITY_TYPE;
                 if (fromHomeOrRecents && StackId.allowTopTaskToReturnHome(mStackId)) {
-                    returnToType = lastStack.topTask() == null
-                            ? HOME_ACTIVITY_TYPE : lastStack.topTask().taskType;
+                    returnToType = topTask == null ? HOME_ACTIVITY_TYPE : topTask.taskType;
                 }
                 task.setTaskToReturnTo(returnToType);
             }
         } else {
             task.setTaskToReturnTo(APPLICATION_ACTIVITY_TYPE);
         }
-
-        mTaskHistory.remove(task);
-        // Now put task at top.
-        int taskNdx = mTaskHistory.size();
-        final boolean notShownWhenLocked =
-                (newActivity != null && !newActivity.okToShowLocked())
-                || (newActivity == null && !task.okToShowLocked());
-        if (notShownWhenLocked) {
-            // Put non-current user tasks below current user tasks.
-            while (--taskNdx >= 0) {
-                final TaskRecord tmpTask = mTaskHistory.get(taskNdx);
-                if (!mStackSupervisor.isCurrentProfileLocked(tmpTask.userId)
-                        || tmpTask.topRunningActivityLocked() == null) {
-                    break;
-                }
-            }
-            ++taskNdx;
-        }
-        mTaskHistory.add(taskNdx, task);
-        updateTaskMovement(task, true);
-        task.moveWindowContainerToTop(allowStackOnTop /* includingParents */);
     }
 
     final void startActivityLocked(ActivityRecord r, ActivityRecord focusedTopActivity,
@@ -4891,38 +4888,49 @@
     }
 
     void addTask(final TaskRecord task, final boolean toTop, String reason) {
-        addTask(task, toTop, reason, true /* allowStackOnTop */);
+        addTask(task, toTop ? MAX_VALUE : 0, reason);
+        if (toTop) {
+            // TODO: figure-out a way to remove this call.
+            task.moveWindowContainerToTop(true /* includingParents */);
+        }
     }
 
-    void addTask(final TaskRecord task, final boolean toTop, String reason,
-            boolean allowStackOnTop) {
+    // TODO: This shouldn't allow automatic reparenting. Remove the call to preAddTask and deal
+    // with the fall-out...
+    void addTask(final TaskRecord task, int position, String reason) {
+        // TODO: Is this remove really needed? Need to look into the call path for the other addTask
+        mTaskHistory.remove(task);
+        position = getAdjustedPositionForTask(task, position, null /* starting */);
+        final boolean toTop = position >= mTaskHistory.size();
         final ActivityStack prevStack = preAddTask(task, reason, toTop);
 
         task.setStack(this);
+
         if (toTop) {
-            insertTaskAtTop(task, null, allowStackOnTop);
-        } else {
-            mTaskHistory.add(0, task);
-            updateTaskMovement(task, false);
+            updateTaskReturnToForTopInsertion(task);
         }
+
+        mTaskHistory.add(position, task);
+        updateTaskMovement(task, toTop);
+
         postAddTask(task, prevStack);
     }
 
-    void positionChildAt(int taskId, int index) {
-        final TaskRecord task = mStackSupervisor.anyTaskForIdLocked(taskId);
-        if (task == null) {
-            Slog.w(TAG, "positionTaskInStackLocked: no task for id=" + taskId);
-            return;
+    void positionChildAt(TaskRecord task, int index) {
+
+        if (task.getStack() != this) {
+            throw new IllegalArgumentException("AS.positionChildAt: task=" + task
+                    + " is not a child of stack=" + this + " current parent=" + task.getStack());
         }
 
         task.updateOverrideConfigurationForStack(this);
 
         final ActivityRecord topRunningActivity = task.topRunningActivityLocked();
         final boolean wasResumed = topRunningActivity == task.getStack().mResumedActivity;
-        final ActivityStack prevStack = preAddTask(task, "positionTask", !ON_TOP);
         task.setStack(this);
         insertTaskAtPosition(task, index);
-        postAddTask(task, prevStack);
+        postAddTask(task, null /* prevStack */);
+
         if (wasResumed) {
             if (mResumedActivity != null) {
                 Log.wtf(TAG, "mResumedActivity was already set when moving mResumedActivity from"
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index 8cf0708..527e918 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -1933,7 +1933,7 @@
                 }
                 if (stackId != currentStack.mStackId) {
                     currentStack = moveTaskToStackUncheckedLocked(task, stackId, ON_TOP,
-                            !FORCE_FOCUS, reason, true /* allowStackOnTop */);
+                            !FORCE_FOCUS, reason);
                     stackId = currentStack.mStackId;
                     // moveTaskToStackUncheckedLocked() should already placed the task on top,
                     // still need moveTaskToFrontLocked() below for any transition settings.
@@ -2238,18 +2238,16 @@
                     }
                     moveTaskToStackLocked(tasks.get(i).taskId,
                             FULLSCREEN_WORKSPACE_STACK_ID, onTop, onTop /*forceFocus*/,
-                            "moveTasksToFullscreenStack - onTop", ANIMATE, DEFER_RESUME,
-                            true /* allowStackOnTop */);
+                            "moveTasksToFullscreenStack - onTop", ANIMATE, DEFER_RESUME);
                 }
 
                 ensureActivitiesVisibleLocked(null, 0, PRESERVE_WINDOWS);
                 resumeFocusedStackTopActivityLocked();
             } else {
                 for (int i = 0; i < size; i++) {
-                    moveTaskToStackLocked(tasks.get(i).taskId, FULLSCREEN_WORKSPACE_STACK_ID,
-                            true /* onTop */, false /* forceFocus */,
-                            "moveTasksToFullscreenStack - NOT_onTop", !ANIMATE, DEFER_RESUME,
-                            false /* allowStackOnTop */);
+                    final TaskRecord task = tasks.get(i);
+                    task.reparent(FULLSCREEN_WORKSPACE_STACK_ID, MAX_VALUE,
+                            "moveTasksToFullscreenStack - NOT_onTop");
                 }
             }
         } finally {
@@ -2385,7 +2383,8 @@
                     final int insertPosition = isFullscreenStackVisible
                             ? Math.max(0, fullscreenStack.getChildCount() - 1)
                             : fullscreenStack.getChildCount();
-                    fullscreenStack.positionChildAt(tasks.get(i).taskId, insertPosition);
+                    final TaskRecord task = tasks.get(i);
+                    task.reparent(FULLSCREEN_WORKSPACE_STACK_ID, insertPosition, "removeStack");
                 }
                 ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS);
                 resumeFocusedStackTopActivityLocked();
@@ -2605,13 +2604,10 @@
      * @param toTop True if the task should be placed at the top of the stack.
      * @param forceFocus if focus should be moved to the new stack
      * @param reason Reason the task is been moved.
-     * @param allowStackOnTop If stack movement should be moved to the top due to the addition of
-     *                        the task to the stack. E.g. Moving the stack to the front because it
-     *                        should be focused because it now contains the focused activity.
      * @return The stack the task was moved to.
      */
     ActivityStack moveTaskToStackUncheckedLocked(TaskRecord task, int stackId, boolean toTop,
-            boolean forceFocus, String reason, boolean allowStackOnTop) {
+            boolean forceFocus, String reason) {
 
         if (StackId.isMultiWindowStack(stackId) && !mService.mSupportsMultiWindow) {
             throw new IllegalStateException("moveTaskToStackUncheckedLocked: Device doesn't "
@@ -2642,15 +2638,14 @@
         // if a docked stack is created below which will lead to the stack we are moving from and
         // its resizeable tasks being resized.
         task.mTemporarilyUnresizable = true;
-        final ActivityStack stack = getStack(stackId, CREATE_IF_NEEDED, toTop && allowStackOnTop);
+        final ActivityStack stack = getStack(stackId, CREATE_IF_NEEDED, toTop);
         task.mTemporarilyUnresizable = false;
-        task.reparentWindowContainer(stack.mStackId, toTop ? MAX_VALUE : MIN_VALUE);
-        stack.addTask(task, toTop, reason, allowStackOnTop);
+        task.reparent(stack.mStackId, toTop ? MAX_VALUE : 0, reason);
 
         // If the task had focus before (or we're requested to move focus),
         // move focus to the new stack by moving the stack to the front.
         stack.moveToFrontAndResumeStateIfNeeded(
-                r, allowStackOnTop && (forceFocus || wasFocused || wasFront), wasResumed, reason);
+                r, forceFocus || wasFocused || wasFront, wasResumed, reason);
 
         return stack;
     }
@@ -2658,11 +2653,11 @@
     boolean moveTaskToStackLocked(int taskId, int stackId, boolean toTop, boolean forceFocus,
             String reason, boolean animate) {
         return moveTaskToStackLocked(taskId, stackId, toTop, forceFocus, reason, animate,
-                false /* deferResume */, true /* allowStackOnTop */);
+                false /* deferResume */);
     }
 
     boolean moveTaskToStackLocked(int taskId, int stackId, boolean toTop, boolean forceFocus,
-            String reason, boolean animate, boolean deferResume, boolean allowStackOnTop) {
+            String reason, boolean animate, boolean deferResume) {
         final TaskRecord task = anyTaskForIdLocked(taskId);
         if (task == null) {
             Slog.w(TAG, "moveTaskToStack: no task for id=" + taskId);
@@ -2702,7 +2697,7 @@
         boolean kept = true;
         try {
             final ActivityStack stack = moveTaskToStackUncheckedLocked(
-                    task, stackId, toTop, forceFocus, reason + " moveTaskToStack", allowStackOnTop);
+                    task, stackId, toTop, forceFocus, reason + " moveTaskToStack");
             stackId = stack.mStackId;
 
             if (!animate) {
diff --git a/services/core/java/com/android/server/am/ActivityStarter.java b/services/core/java/com/android/server/am/ActivityStarter.java
index 547e0b2..81a5d3b 100644
--- a/services/core/java/com/android/server/am/ActivityStarter.java
+++ b/services/core/java/com/android/server/am/ActivityStarter.java
@@ -1777,7 +1777,7 @@
             int stackId = mInTask.getLaunchStackId();
             if (stackId != mInTask.getStackId()) {
                 final ActivityStack stack = mSupervisor.moveTaskToStackUncheckedLocked(mInTask,
-                        stackId, ON_TOP, !FORCE_FOCUS, "inTaskToFront", true /* allowStackOnTop */);
+                        stackId, ON_TOP, !FORCE_FOCUS, "inTaskToFront");
                 stackId = stack.mStackId;
             }
             if (StackId.resizeStackWithLaunchBounds(stackId)) {
diff --git a/services/core/java/com/android/server/am/TaskRecord.java b/services/core/java/com/android/server/am/TaskRecord.java
index 9e22c50..4c4c444 100644
--- a/services/core/java/com/android/server/am/TaskRecord.java
+++ b/services/core/java/com/android/server/am/TaskRecord.java
@@ -101,6 +101,8 @@
 import static com.android.server.am.ActivityRecord.HOME_ACTIVITY_TYPE;
 import static com.android.server.am.ActivityRecord.RECENTS_ACTIVITY_TYPE;
 import static com.android.server.am.ActivityRecord.STARTING_WINDOW_SHOWN;
+import static com.android.server.am.ActivityStack.REMOVE_TASK_MODE_MOVING;
+import static com.android.server.am.ActivityStackSupervisor.CREATE_IF_NEEDED;
 import static com.android.server.am.ActivityStackSupervisor.PRESERVE_WINDOWS;
 
 final class TaskRecord extends ConfigurationContainer {
@@ -512,8 +514,8 @@
     }
 
     // TODO: Remove once we have a stack controller.
-    void positionWindowContainerAt(int stackId, int index) {
-        mWindowContainerController.positionAt(stackId, index, mBounds, getOverrideConfiguration());
+    void positionWindowContainerAt(int position) {
+        mWindowContainerController.positionAt(position, mBounds, getOverrideConfiguration());
     }
 
     // TODO: Replace with moveChildToTop?
@@ -534,9 +536,36 @@
         mWindowContainerController.getBounds(bounds);
     }
 
-    // TODO: make this part of adding it to the stack?
-    void reparentWindowContainer(int stackId, int position) {
-        mWindowContainerController.reparent(stackId, position);
+    // TODO: Should we be doing all the stuff in ASS.moveTaskToStackLocked?
+    void reparent(int stackId, int position, String reason) {
+        mService.mWindowManager.deferSurfaceLayout();
+
+        try {
+            final ActivityStackSupervisor supervisor = mService.mStackSupervisor;
+            final ActivityStack newStack = supervisor.getStack(stackId,
+                    CREATE_IF_NEEDED, false /* toTop */);
+            // Adjust the position for the new parent stack as needed.
+            position = newStack.getAdjustedPositionForTask(this, position, null /* starting */);
+
+            // Must reparent first in window manager to avoid a situation where AM can delete the
+            // we are coming from in WM before we reparent because it became empty.
+            mWindowContainerController.reparent(stackId, position);
+
+            final ActivityStack prevStack = mStack;
+            prevStack.removeTask(this, reason, REMOVE_TASK_MODE_MOVING);
+            newStack.addTask(this, position, reason);
+
+            supervisor.scheduleReportPictureInPictureModeChangedIfNeeded(this, prevStack);
+
+            if (voiceSession != null) {
+                try {
+                    voiceSession.taskStarted(intent, taskId);
+                } catch (RemoteException e) {
+                }
+            }
+        } finally {
+            mService.mWindowManager.continueSurfaceLayout();
+        }
     }
 
     void cancelWindowTransition() {
@@ -978,6 +1007,8 @@
         addActivityAtIndex(mActivities.size(), r);
     }
 
+    // TODO: Figure-out if any of the call points expect the window container to be reparented and
+    // correct them to use the right method.
     void addActivityAtIndex(int index, ActivityRecord r) {
         // Remove r first, and if it wasn't already in the list and it's fullscreen, count it.
         if (!mActivities.remove(r) && r.fullscreen) {
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index b1b7542..2d50e3a 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -38,6 +38,7 @@
 import android.view.DisplayInfo;
 import android.view.Surface;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.EventLogTags;
 
 import java.io.PrintWriter;
@@ -104,32 +105,40 @@
     }
 
     DisplayContent getDisplayContent() {
-        return mStack.getDisplayContent();
+        return mStack != null ? mStack.getDisplayContent() : null;
+    }
+
+    int getAdjustedAddPosition(int suggestedPosition) {
+        final int size = mChildren.size();
+        if (suggestedPosition >= size) {
+            return Math.min(size, suggestedPosition);
+        }
+
+        for (int pos = 0; pos < size && pos < suggestedPosition; ++pos) {
+            // TODO: Confirm that this is the behavior we want long term.
+            if (mChildren.get(pos).removed) {
+                // suggestedPosition assumes removed tokens are actually gone.
+                ++suggestedPosition;
+            }
+        }
+        return Math.min(size, suggestedPosition);
     }
 
     @Override
-    void addChild(AppWindowToken wtoken, int addPos) {
-        final int lastPos = mChildren.size();
-        if (addPos >= lastPos) {
-            addPos = lastPos;
-        } else {
-            for (int pos = 0; pos < lastPos && pos < addPos; ++pos) {
-                if (mChildren.get(pos).removed) {
-                    // addPos assumes removed tokens are actually gone.
-                    ++addPos;
-                }
-            }
-        }
-
-        final WindowContainer parent = wtoken.getParent();
-        if (parent != null) {
-            parent.removeChild(wtoken);
-        }
-        super.addChild(wtoken, addPos);
+    void addChild(AppWindowToken wtoken, int position) {
+        position = getAdjustedAddPosition(position);
+        super.addChild(wtoken, position);
         wtoken.mTask = this;
         mDeferRemoval = false;
     }
 
+    @Override
+    void positionChildAt(int position, AppWindowToken child, boolean includingParents) {
+        position = getAdjustedAddPosition(position);
+        super.positionChildAt(position, child, includingParents);
+        mDeferRemoval = false;
+    }
+
     private boolean hasWindowsAlive() {
         for (int i = mChildren.size() - 1; i >= 0; i--) {
             if (mChildren.get(i).hasWindowsAlive()) {
@@ -139,9 +148,14 @@
         return false;
     }
 
+    @VisibleForTesting
+    boolean shouldDeferRemoval() {
+        return hasWindowsAlive() && mStack.isAnimating();
+    }
+
     @Override
     void removeIfPossible() {
-        if (hasWindowsAlive() && mStack.isAnimating()) {
+        if (shouldDeferRemoval()) {
             if (DEBUG_STACK) Slog.i(TAG, "removeTask: deferring removing taskId=" + mTaskId);
             mDeferRemoval = true;
             return;
@@ -166,7 +180,8 @@
 
     void reparent(TaskStack stack, int position) {
         if (stack == mStack) {
-            return;
+            throw new IllegalArgumentException(
+                    "task=" + this + " already child of stack=" + mStack);
         }
         if (DEBUG_STACK) Slog.i(TAG, "reParentTask: removing taskId=" + mTaskId
                 + " from stack=" + mStack);
@@ -176,25 +191,8 @@
     }
 
     /** @see com.android.server.am.ActivityManagerService#positionTaskInStack(int, int, int). */
-    void positionTaskInStack(TaskStack stack, int position, Rect bounds,
-            Configuration overrideConfig) {
-        if (mStack == null) {
-            // There is an assumption that task already has a stack at this point, so lets make
-            // sure we comply with it.
-            throw new IllegalStateException("Trying to position task that has no parent.");
-        }
-        if (stack != mStack) {
-            // Task is already attached to a different stack. First we need to remove it from there
-            // and add to top of the target stack. We will move it proper position afterwards.
-            if (DEBUG_STACK) Slog.i(TAG, "positionTaskInStack: removing taskId=" + mTaskId
-                    + " from stack=" + mStack);
-            EventLog.writeEvent(WM_TASK_REMOVED, mTaskId, "positionTaskInStack");
-            mStack.removeChild(this);
-            stack.addTask(this, position);
-        } else {
-            stack.positionChildAt(position, this, true /* includingParents */);
-        }
-
+    void positionAt(int position, Rect bounds, Configuration overrideConfig) {
+        mStack.positionChildAt(position, this, false /* includingParents */);
         resizeLocked(bounds, overrideConfig, false /* force */);
 
         for (int activityNdx = mChildren.size() - 1; activityNdx >= 0; --activityNdx) {
diff --git a/services/core/java/com/android/server/wm/TaskWindowContainerController.java b/services/core/java/com/android/server/wm/TaskWindowContainerController.java
index 0e4d048..bbc9ed2 100644
--- a/services/core/java/com/android/server/wm/TaskWindowContainerController.java
+++ b/services/core/java/com/android/server/wm/TaskWindowContainerController.java
@@ -18,10 +18,10 @@
 
 import android.app.ActivityManager.TaskSnapshot;
 import android.content.res.Configuration;
-import android.graphics.GraphicBuffer;
 import android.graphics.Rect;
 import android.util.EventLog;
 import android.util.Slog;
+import com.android.internal.annotations.VisibleForTesting;
 
 import static com.android.server.EventLogTags.WM_TASK_CREATED;
 import static com.android.server.wm.DragResizeMode.DRAG_RESIZE_MODE_DOCKED_DIVIDER;
@@ -59,13 +59,21 @@
                         + stackId);
             }
             EventLog.writeEvent(WM_TASK_CREATED, taskId, stackId);
-            final Task task = new Task(taskId, stack, userId, mService, bounds, overrideConfig,
-                    isOnTopLauncher, resizeMode, homeTask, this);
+            final Task task = createTask(taskId, stack, userId, bounds, overrideConfig, resizeMode,
+                    homeTask, isOnTopLauncher);
             final int position = toTop ? POSITION_TOP : POSITION_BOTTOM;
             stack.addTask(task, position, showForAllUsers, true /* moveParents */);
         }
     }
 
+    @VisibleForTesting
+    Task createTask(int taskId, TaskStack stack, int userId, Rect bounds,
+            Configuration overrideConfig, int resizeMode, boolean homeTask,
+            boolean isOnTopLauncher) {
+        return new Task(taskId, stack, userId, mService, bounds, overrideConfig, isOnTopLauncher,
+                resizeMode, homeTask, this);
+    }
+
     @Override
     public void removeContainer() {
         synchronized(mWindowMap) {
@@ -78,7 +86,7 @@
         }
     }
 
-    public void positionChildAt(AppWindowContainerController childController, int index) {
+    public void positionChildAt(AppWindowContainerController childController, int position) {
         synchronized(mService.mWindowMap) {
             final AppWindowToken aToken = childController.mContainer;
             if (aToken == null) {
@@ -91,7 +99,7 @@
             if (task == null) {
                 throw new IllegalArgumentException("positionChildAt: invalid task=" + this);
             }
-            task.addChild(aToken, index);
+            task.positionChildAt(position, aToken, false /* includeParents */);
         }
     }
 
@@ -106,9 +114,7 @@
             }
             final TaskStack stack = mService.mStackIdToStack.get(stackId);
             if (stack == null) {
-                if (DEBUG_STACK) Slog.i(TAG_WM,
-                        "reparent: could not find stackId=" + stackId);
-                return;
+                throw new IllegalArgumentException("reparent: could not find stackId=" + stackId);
             }
             mContainer.reparent(stack, position);
             final DisplayContent displayContent = stack.getDisplayContent();
@@ -140,22 +146,22 @@
     }
 
     // TODO: Move to positionChildAt() in stack controller once we have a stack controller.
-    public void positionAt(int stackId, int index, Rect bounds, Configuration overrideConfig) {
+    public void positionAt(int position, Rect bounds, Configuration overrideConfig) {
         synchronized (mWindowMap) {
             if (DEBUG_STACK) Slog.i(TAG_WM, "positionChildAt: positioning taskId=" + mTaskId
-                    + " in stackId=" + stackId + " at " + index);
+                    + " at " + position);
             if (mContainer == null) {
                 if (DEBUG_STACK) Slog.i(TAG_WM,
-                        "positionTaskInStack: could not find taskId=" + mTaskId);
+                        "positionAt: could not find taskId=" + mTaskId);
                 return;
             }
-            final TaskStack stack = mService.mStackIdToStack.get(stackId);
+            final TaskStack stack = mContainer.mStack;
             if (stack == null) {
                 if (DEBUG_STACK) Slog.i(TAG_WM,
-                        "positionTaskInStack: could not find stackId=" + stackId);
+                        "positionAt: could not find stack for task=" + mContainer);
                 return;
             }
-            mContainer.positionTaskInStack(stack, index, bounds, overrideConfig);
+            mContainer.positionAt(position, bounds, overrideConfig);
             final DisplayContent displayContent = stack.getDisplayContent();
             displayContent.setLayoutNeeded();
             mService.mWindowPlacerLocked.performSurfacePlacement();
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index fd7ea6d..e231da8 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -234,13 +234,20 @@
      */
     @CallSuper
     void positionChildAt(int position, E child, boolean includingParents) {
+
+        if (child.getParent() != this) {
+            throw new IllegalArgumentException("removeChild: container=" + child.getName()
+                    + " is not a child of container=" + getName()
+                    + " current parent=" + child.getParent());
+        }
+
         if ((position < 0 && position != POSITION_BOTTOM)
                 || (position > mChildren.size() && position != POSITION_TOP)) {
             throw new IllegalArgumentException("positionAt: invalid position=" + position
                     + ", children number=" + mChildren.size());
         }
 
-        if (position == mChildren.size() - 1) {
+        if (position >= mChildren.size() - 1) {
             position = POSITION_TOP;
         } else if (position == 0) {
             position = POSITION_BOTTOM;
diff --git a/services/tests/servicestests/src/com/android/server/wm/AppWindowContainerControllerTests.java b/services/tests/servicestests/src/com/android/server/wm/AppWindowContainerControllerTests.java
index 7f1c273..26accc3 100644
--- a/services/tests/servicestests/src/com/android/server/wm/AppWindowContainerControllerTests.java
+++ b/services/tests/servicestests/src/com/android/server/wm/AppWindowContainerControllerTests.java
@@ -77,35 +77,8 @@
     }
 
     private TestAppWindowContainerController createAppWindowController() {
-        final TaskStack stack = createTaskStackOnDisplay(sDisplayContent);
         final TestTaskWindowContainerController taskController =
-                new TestTaskWindowContainerController(stack.mStackId);
-        final IApplicationToken token = new TestIApplicationToken();
-        return new TestAppWindowContainerController(taskController, token);
-    }
-
-    private class TestAppWindowContainerController extends AppWindowContainerController {
-
-        final IApplicationToken mToken;
-
-        TestAppWindowContainerController(TestTaskWindowContainerController taskController,
-                IApplicationToken token) {
-            super(taskController, token, null /* listener */, 0 /* index */,
-                    SCREEN_ORIENTATION_UNSPECIFIED, true /* fullscreen */,
-                    true /* showForAllUsers */, 0 /* configChanges */, false /* voiceInteraction */,
-                    false /* launchTaskBehind */, false /* alwaysFocusable */,
-                    0 /* targetSdkVersion */, 0 /* rotationAnimationHint */,
-                    0 /* inputDispatchingTimeoutNanos */, sWm);
-            mToken = token;
-        }
-    }
-
-    private class TestIApplicationToken implements IApplicationToken {
-
-        private final Binder mBinder = new Binder();
-        @Override
-        public IBinder asBinder() {
-            return mBinder;
-        }
+                new TestTaskWindowContainerController();
+        return new TestAppWindowContainerController(taskController);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/wm/TaskWindowContainerControllerTests.java b/services/tests/servicestests/src/com/android/server/wm/TaskWindowContainerControllerTests.java
index 0dd31c3..f84bf60 100644
--- a/services/tests/servicestests/src/com/android/server/wm/TaskWindowContainerControllerTests.java
+++ b/services/tests/servicestests/src/com/android/server/wm/TaskWindowContainerControllerTests.java
@@ -22,6 +22,11 @@
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
 /**
  * Test class for {@link TaskWindowContainerController}.
  *
@@ -32,7 +37,73 @@
 @Presubmit
 @org.junit.runner.RunWith(AndroidJUnit4.class)
 public class TaskWindowContainerControllerTests extends WindowTestsBase {
-// TODO Add tests once StackWindowContainerController is created.
+
     @Test
-    public void dummyTest() throws Exception {}
+    public void testRemoveContainer() throws Exception {
+        final TestTaskWindowContainerController taskController =
+                new TestTaskWindowContainerController();
+        final TestAppWindowContainerController appController =
+                new TestAppWindowContainerController(taskController);
+
+        taskController.removeContainer();
+        // Assert that the container was removed.
+        assertNull(taskController.mContainer);
+        assertNull(appController.mContainer);
+    }
+
+    @Test
+    public void testRemoveContainer_DeferRemoval() throws Exception {
+        final TestTaskWindowContainerController taskController =
+                new TestTaskWindowContainerController();
+        final TestAppWindowContainerController appController =
+                new TestAppWindowContainerController(taskController);
+
+        final TestTask task = (TestTask) taskController.mContainer;
+        final AppWindowToken app = appController.mContainer;
+        task.mShouldDeferRemoval = true;
+
+        taskController.removeContainer();
+        // For the case of deferred removal the task controller will no longer be connected to the
+        // container, but the app controller will still be connected to the its container until
+        // the task window container is removed.
+        assertNull(taskController.mContainer);
+        assertNull(task.getController());
+        assertNotNull(appController.mContainer);
+        assertNotNull(app.getController());
+
+        task.removeImmediately();
+        assertNull(appController.mContainer);
+        assertNull(app.getController());
+    }
+
+    @Test
+    public void testReparent() throws Exception {
+        final TaskStack stack1 = createTaskStackOnDisplay(sDisplayContent);
+        final TestTaskWindowContainerController taskController =
+                new TestTaskWindowContainerController(stack1.mStackId);
+        final TaskStack stack2 = createTaskStackOnDisplay(sDisplayContent);
+        final TestTaskWindowContainerController taskController2 =
+                new TestTaskWindowContainerController(stack2.mStackId);
+
+        boolean gotException = false;
+        try {
+            taskController.reparent(stack1.mStackId, 0);
+        } catch (IllegalArgumentException e) {
+            gotException = true;
+        }
+        assertTrue("Should not be able to reparent to the same parent", gotException);
+
+        gotException = false;
+        try {
+            taskController.reparent(sNextStackId + 1, 0);
+        } catch (IllegalArgumentException e) {
+            gotException = true;
+        }
+        assertTrue("Should not be able to reparent to a stackId that doesn't exist", gotException);
+
+        taskController.reparent(stack2.mStackId, 0);
+        assertEquals(stack2, taskController.mContainer.getParent());
+        assertEquals(0, ((TestTask) taskController.mContainer).positionInParent());
+        assertEquals(1, ((TestTask) taskController2.mContainer).positionInParent());
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/servicestests/src/com/android/server/wm/WindowContainerTests.java
index b57329c..4f740ac 100644
--- a/services/tests/servicestests/src/com/android/server/wm/WindowContainerTests.java
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowContainerTests.java
@@ -315,7 +315,7 @@
         gotException = false;
         try {
             // Check response to position that's bigger than child number.
-            root.positionChildAt(2, child1, false /* includingParents */);
+            root.positionChildAt(3, child1, false /* includingParents */);
         } catch (IllegalArgumentException e) {
             gotException = true;
         }
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java
index 466bd67..53e5a2a 100644
--- a/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java
@@ -16,6 +16,10 @@
 
 package com.android.server.wm;
 
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Binder;
+import android.view.IApplicationToken;
 import org.junit.Assert;
 import org.junit.Before;
 
@@ -28,6 +32,7 @@
 import static android.app.ActivityManager.StackId.FIRST_DYNAMIC_STACK_ID;
 import static android.app.AppOpsManager.OP_NONE;
 import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
 import static android.content.res.Configuration.EMPTY;
 import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
 import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW;
@@ -50,7 +55,7 @@
     static WindowManagerService sWm = null;
     private final IWindow mIWindow = new TestIWindow();
     private final Session mMockSession = mock(Session.class);
-    private static int sNextStackId = FIRST_DYNAMIC_STACK_ID;
+    static int sNextStackId = FIRST_DYNAMIC_STACK_ID;
     private static int sNextTaskId = 0;
 
     private static boolean sOneTimeSetupDone = false;
@@ -201,16 +206,78 @@
         }
     }
 
+    /* Used so we can gain access to some protected members of the {@link Task} class */
+    class TestTask extends Task {
+
+        boolean mShouldDeferRemoval = false;
+
+        TestTask(int taskId, TaskStack stack, int userId, WindowManagerService service, Rect bounds,
+                Configuration overrideConfig, boolean isOnTopLauncher, int resizeMode,
+                boolean homeTask, TaskWindowContainerController controller) {
+            super(taskId, stack, userId, service, bounds, overrideConfig, isOnTopLauncher,
+                    resizeMode, homeTask, controller);
+        }
+
+        boolean shouldDeferRemoval() {
+            return mShouldDeferRemoval;
+        }
+
+        int positionInParent() {
+            return getParent().mChildren.indexOf(this);
+        }
+    }
+
     /**
      * Used so we can gain access to some protected members of {@link TaskWindowContainerController}
      * class.
      */
     class TestTaskWindowContainerController extends TaskWindowContainerController {
 
+        TestTaskWindowContainerController() {
+            this(createTaskStackOnDisplay(sDisplayContent).mStackId);
+        }
+
         TestTaskWindowContainerController(int stackId) {
             super(sNextTaskId++, stackId, 0 /* userId */, null /* bounds */,
                     EMPTY /* overrideConfig*/, RESIZE_MODE_UNRESIZEABLE, false /* homeTask*/,
                     false /* isOnTopLauncher */, true /* toTop*/, true /* showForAllUsers */);
         }
+
+        @Override
+        TestTask createTask(int taskId, TaskStack stack, int userId, Rect bounds,
+                Configuration overrideConfig, int resizeMode, boolean homeTask,
+                boolean isOnTopLauncher) {
+            return new TestTask(taskId, stack, userId, mService, bounds, overrideConfig,
+                    isOnTopLauncher, resizeMode, homeTask, this);
+        }
+    }
+
+    class TestAppWindowContainerController extends AppWindowContainerController {
+
+        final IApplicationToken mToken;
+
+        TestAppWindowContainerController(TestTaskWindowContainerController taskController) {
+            this(taskController, new TestIApplicationToken());
+        }
+
+        TestAppWindowContainerController(TestTaskWindowContainerController taskController,
+                IApplicationToken token) {
+            super(taskController, token, null /* listener */, 0 /* index */,
+                    SCREEN_ORIENTATION_UNSPECIFIED, true /* fullscreen */,
+                    true /* showForAllUsers */, 0 /* configChanges */, false /* voiceInteraction */,
+                    false /* launchTaskBehind */, false /* alwaysFocusable */,
+                    0 /* targetSdkVersion */, 0 /* rotationAnimationHint */,
+                    0 /* inputDispatchingTimeoutNanos */, sWm);
+            mToken = token;
+        }
+    }
+
+    class TestIApplicationToken implements IApplicationToken {
+
+        private final Binder mBinder = new Binder();
+        @Override
+        public IBinder asBinder() {
+            return mBinder;
+        }
     }
 }