Refactor resetTaskIfNeededLocked for tasks.

Change-Id: I1ad8b7ffe1f17490ebcb98ea48d57f0b15f888c9
diff --git a/services/java/com/android/server/am/ActivityStack.java b/services/java/com/android/server/am/ActivityStack.java
index e0b8015..af4ace9 100644
--- a/services/java/com/android/server/am/ActivityStack.java
+++ b/services/java/com/android/server/am/ActivityStack.java
@@ -2219,7 +2219,7 @@
      * Perform a reset of the given task, if needed as part of launching it.
      * Returns the new HistoryRecord at the top of the task.
      */
-    private final ActivityRecord resetTaskIfNeededLocked(ActivityRecord taskTop,
+/*    private final ActivityRecord resetTaskIfNeededLocked(ActivityRecord taskTop,
             ActivityRecord newActivity) {
         mLastHistoryModifier = "resetTaskIfNeededLocked";
 
@@ -2563,7 +2563,343 @@
         }
         return taskTop;
     }
-    
+*/
+    /**
+     * Helper method for #resetTaskIfNeededLocked.
+     * We are inside of the task being reset...  we'll either finish this activity, push it out
+     * for another task, or leave it as-is.
+     * @param task The task containing the Activity (taskTop) that might be reset.
+     * @param forceReset
+     * @return An ActivityOptions that needs to be processed.
+     */
+    private final ActivityOptions resetTargetTaskIfNeededLocked(TaskRecord task,
+            boolean forceReset) {
+        ActivityOptions topOptions = null;
+
+        int replyChainEnd = -1;
+        boolean canMoveOptions = true;
+
+        // We only do this for activities that are not the root of the task (since if we finish
+        // the root, we may no longer have the task!).
+        final ArrayList<ActivityRecord> activities = task.mActivities;
+        final int numActivities = activities.size();
+        for (int i = numActivities - 1; i > 0; --i ) {
+            ActivityRecord target = activities.get(i);
+
+            final int flags = target.info.flags;
+            final boolean finishOnTaskLaunch =
+                    (flags & ActivityInfo.FLAG_FINISH_ON_TASK_LAUNCH) != 0;
+            final boolean allowTaskReparenting =
+                    (flags & ActivityInfo.FLAG_ALLOW_TASK_REPARENTING) != 0;
+            final boolean clearWhenTaskReset =
+                    (target.intent.getFlags() & Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET) != 0;
+
+            if (!finishOnTaskLaunch
+                    && !clearWhenTaskReset
+                    && target.resultTo != null) {
+                // If this activity is sending a reply to a previous
+                // activity, we can't do anything with it now until
+                // we reach the start of the reply chain.
+                // XXX note that we are assuming the result is always
+                // to the previous activity, which is almost always
+                // the case but we really shouldn't count on.
+                if (replyChainEnd < 0) {
+                    replyChainEnd = i;
+                }
+            } else if (!finishOnTaskLaunch
+                    && !clearWhenTaskReset
+                    && allowTaskReparenting
+                    && target.taskAffinity != null
+                    && !target.taskAffinity.equals(task.affinity)) {
+                // If this activity has an affinity for another
+                // task, then we need to move it out of here.  We will
+                // move it as far out of the way as possible, to the
+                // bottom of the activity stack.  This also keeps it
+                // correctly ordered with any activities we previously
+                // moved.
+                TaskRecord bottomTask = mTaskHistory.get(0);
+                ActivityRecord p = bottomTask.mActivities.get(0);
+                if (target.taskAffinity != null
+                        && target.taskAffinity.equals(p.task.affinity)) {
+                    // If the activity currently at the bottom has the
+                    // same task affinity as the one we are moving,
+                    // then merge it into the same task.
+                    if (VALIDATE_TASK_REPLACE) Slog.w(TAG,
+                        "resetTaskFoundIntended: would reparenting " + target + " to bottom " + p.task);
+                    target.setTask(p.task, p.thumbHolder, false);
+                    if (DEBUG_TASKS) Slog.v(TAG, "Start pushing activity " + target
+                            + " out to bottom task " + p.task);
+                } else {
+                    do {
+                        mService.mCurTask++;
+                        if (mService.mCurTask <= 0) {
+                            mService.mCurTask = 1;
+                        }
+                    } while (mTaskIdToTaskRecord.get(mService.mCurTask) != null);
+                    target.setTask(createTaskRecord(mService.mCurTask, target.info, null, false),
+                            null, false);
+                    target.task.affinityIntent = target.intent;
+                    if (DEBUG_TASKS) Slog.v(TAG, "Start pushing activity " + target
+                            + " out to new task " + target.task);
+                }
+
+                final TaskRecord targetTask = target.task;
+                final int targetTaskId = targetTask.taskId;
+                mService.mWindowManager.setAppGroupId(target.appToken, targetTaskId);
+
+                ThumbnailHolder curThumbHolder = target.thumbHolder;
+                boolean gotOptions = !canMoveOptions;
+
+                final int start = replyChainEnd < 0 ? i : replyChainEnd;
+                for (int srcPos = start; srcPos >= i; --srcPos) {
+                    p = activities.get(srcPos);
+                    if (p.finishing) {
+                        continue;
+                    }
+
+                    curThumbHolder = p.thumbHolder;
+                    canMoveOptions = false;
+                    if (!gotOptions && topOptions == null) {
+                        topOptions = p.takeOptionsLocked();
+                        if (topOptions != null) {
+                            gotOptions = true;
+                        }
+                    }
+                    if (DEBUG_ADD_REMOVE) Slog.i(TAG, "Removing activity " + p + " from task="
+                            + task + " adding to task=" + targetTask,
+                            new RuntimeException("here").fillInStackTrace());
+                    if (DEBUG_TASKS) Slog.v(TAG, "Pushing next activity " + p
+                            + " out to target's task " + target.task);
+                    p.setTask(targetTask, curThumbHolder, false);
+                    targetTask.addActivityAtBottom(p);
+                    mHistory.remove(p);
+                    mHistory.add(0, p);
+                    mService.mWindowManager.setAppGroupId(p.appToken, targetTaskId);
+                }
+
+                mService.mWindowManager.moveTaskToBottom(targetTaskId);
+                if (VALIDATE_TOKENS) {
+                    validateAppTokensLocked();
+                }
+
+                replyChainEnd = -1;
+            } else if (forceReset || finishOnTaskLaunch || clearWhenTaskReset) {
+                // If the activity should just be removed -- either
+                // because it asks for it, or the task should be
+                // cleared -- then finish it and anything that is
+                // part of its reply chain.
+                int end;
+                if (clearWhenTaskReset) {
+                    // In this case, we want to finish this activity
+                    // and everything above it, so be sneaky and pretend
+                    // like these are all in the reply chain.
+                    end = numActivities - 1;
+                } else if (replyChainEnd < 0) {
+                    end = i;
+                } else {
+                    end = replyChainEnd;
+                }
+                ActivityRecord p = null;
+                boolean gotOptions = !canMoveOptions;
+                for (int srcPos = i; srcPos <= end; srcPos++) {
+                    p = activities.get(srcPos);
+                    if (p.finishing) {
+                        continue;
+                    }
+                    canMoveOptions = false;
+                    if (!gotOptions && topOptions == null) {
+                        topOptions = p.takeOptionsLocked();
+                        if (topOptions != null) {
+                            gotOptions = true;
+                        }
+                    }
+                    if (DEBUG_TASKS || VALIDATE_TASK_REPLACE) Slog.w(TAG,
+                            "resetTaskIntendedTask: would call finishActivity on " + p);
+                    if (finishActivityLocked(p, -1, Activity.RESULT_CANCELED, null, "reset",
+                            false)) {
+                        end--;
+                        srcPos--;
+                    }
+                }
+                replyChainEnd = -1;
+            } else {
+                // If we were in the middle of a chain, well the
+                // activity that started it all doesn't want anything
+                // special, so leave it all as-is.
+                replyChainEnd = -1;
+            }
+        }
+
+        return topOptions;
+    }
+
+    /**
+     * Helper method for #resetTaskIfNeededLocked. Processes all of the activities in a given
+     * TaskRecord looking for an affinity with the task of resetTaskIfNeededLocked.taskTop.
+     * @param affinityTask The task we are looking for an affinity to.
+     * @param task Task that resetTaskIfNeededLocked.taskTop belongs to.
+     * @param topTaskIsHigher True if #task has already been processed by resetTaskIfNeededLocked.
+     * @param forceReset Flag passed in to resetTaskIfNeededLocked.
+     */
+    private final void resetAffinityTaskIfNeededLocked(TaskRecord affinityTask, TaskRecord task,
+            boolean topTaskIsHigher, boolean forceReset
+            , ActivityRecord taskTop
+            ) {
+        int replyChainEnd = -1;
+        final int taskId = task.taskId;
+        final String taskAffinity = task.affinity;
+
+        final ArrayList<ActivityRecord> activities = affinityTask.mActivities;
+        final int numActivities = activities.size();
+        // Do not operate on the root Activity.
+        for (int i = numActivities - 1; i > 0; --i) {
+            ActivityRecord target = activities.get(i);
+
+            final int flags = target.info.flags;
+            boolean finishOnTaskLaunch = (flags & ActivityInfo.FLAG_FINISH_ON_TASK_LAUNCH) != 0;
+            boolean allowTaskReparenting = (flags & ActivityInfo.FLAG_ALLOW_TASK_REPARENTING) != 0;
+
+            if (target.resultTo != null) {
+                // If this activity is sending a reply to a previous
+                // activity, we can't do anything with it now until
+                // we reach the start of the reply chain.
+                // XXX note that we are assuming the result is always
+                // to the previous activity, which is almost always
+                // the case but we really shouldn't count on.
+                if (replyChainEnd < 0) {
+                    replyChainEnd = i;
+                }
+            } else if (topTaskIsHigher
+                    && allowTaskReparenting
+                    && taskAffinity != null
+                    && taskAffinity.equals(target.taskAffinity)) {
+                // This activity has an affinity for our task. Either remove it if we are
+                // clearing or move it over to our task.  Note that
+                // we currently punt on the case where we are resetting a
+                // task that is not at the top but who has activities above
+                // with an affinity to it...  this is really not a normal
+                // case, and we will need to later pull that task to the front
+                // and usually at that point we will do the reset and pick
+                // up those remaining activities.  (This only happens if
+                // someone starts an activity in a new task from an activity
+                // in a task that is not currently on top.)
+                if (forceReset || finishOnTaskLaunch) {
+                    final int start = replyChainEnd >= 0 ? replyChainEnd : i;
+                    if (DEBUG_TASKS) Slog.v(TAG, "Finishing task at index " + start + " to " + i);
+                    for (int srcPos = start; srcPos >= i; --srcPos) {
+                        final ActivityRecord p = activities.get(srcPos);
+                        if (p.finishing) {
+                            continue;
+                        }
+                        if (VALIDATE_TASK_REPLACE) Slog.w(TAG,
+                                "resetAffinityTaskIfNeededLocked: calling finishActivity on " + p);
+                        finishActivityLocked(p, -1, Activity.RESULT_CANCELED, null, "reset",
+                                false);
+                    }
+                } else {
+                    int taskTopI = mHistory.indexOf(taskTop);
+                    final int end = replyChainEnd >= 0 ? replyChainEnd : i;
+                    if (DEBUG_TASKS) Slog.v(TAG, "Reparenting task at index " + i + " to " + end);
+                    for (int srcPos = i; srcPos <= end; ++srcPos) {
+                        final ActivityRecord p = activities.get(srcPos);
+                        if (p.finishing) {
+                            continue;
+                        }
+                        p.setTask(task, null, false);
+                        task.addActivityToTop(p);
+
+                        mHistory.remove(p);
+                        mHistory.add(taskTopI--, p);
+
+                        if (DEBUG_ADD_REMOVE) Slog.i(TAG, "Removing and adding activity " + p
+                                + " to stack at " + task,
+                                new RuntimeException("here").fillInStackTrace());
+                        if (DEBUG_TASKS) Slog.v(TAG, "Pulling activity " + p + " from " + srcPos
+                                + " in to resetting task " + task);
+                        mService.mWindowManager.setAppGroupId(p.appToken, taskId);
+                    }
+                    mService.mWindowManager.moveTaskToTop(taskId);
+                    if (VALIDATE_TOKENS) {
+                        validateAppTokensLocked();
+                    }
+                    if (VALIDATE_TASK_REPLACE) {
+                        verifyActivityRecords(false);
+                    }
+
+                    // Now we've moved it in to place...  but what if this is
+                    // a singleTop activity and we have put it on top of another
+                    // instance of the same activity?  Then we drop the instance
+                    // below so it remains singleTop.
+                    if (target.info.launchMode == ActivityInfo.LAUNCH_SINGLE_TOP) {
+                        ArrayList<ActivityRecord> taskActivities = task.mActivities;
+                        boolean found = false;
+                        int targetNdx = taskActivities.indexOf(target);
+                        if (targetNdx > 0) {
+                            ActivityRecord p = taskActivities.get(targetNdx - 1);
+                            if (p.intent.getComponent().equals(target.intent.getComponent())) {
+                                if (finishActivityLocked(p, -1, Activity.RESULT_CANCELED, null,
+                                        "replace", false)) {
+                                    taskTopI--;
+                                }
+                            }
+                        }
+                    }
+                }
+
+                replyChainEnd = -1;
+            }
+        }
+    }
+
+    private final ActivityRecord resetTaskIfNeededLocked(ActivityRecord taskTop,
+            ActivityRecord newActivity) {
+        boolean forceReset =
+                (newActivity.info.flags & ActivityInfo.FLAG_CLEAR_TASK_ON_LAUNCH) != 0;
+        if (ACTIVITY_INACTIVE_RESET_TIME > 0
+                && taskTop.task.getInactiveDuration() > ACTIVITY_INACTIVE_RESET_TIME) {
+            if ((newActivity.info.flags & ActivityInfo.FLAG_ALWAYS_RETAIN_TASK_STATE) == 0) {
+                forceReset = true;
+            }
+        }
+
+        final TaskRecord task = taskTop.task;
+
+        /** False until we evaluate the TaskRecord associated with taskTop. Switches to true
+         * for remaining tasks. Used for later tasks to reparent to task. */
+        boolean taskFound = false;
+
+        /** If ActivityOptions are moved out and need to be aborted or moved to taskTop. */
+        ActivityOptions topOptions = null;
+
+        for (int i = mTaskHistory.size() - 1; i >= 0; --i) {
+            final TaskRecord targetTask = mTaskHistory.get(i);
+
+            if (targetTask == task) {
+                topOptions = resetTargetTaskIfNeededLocked(task, forceReset);
+                taskFound = true;
+            } else {
+                resetAffinityTaskIfNeededLocked(targetTask, task, taskFound, forceReset, taskTop);
+            }
+        }
+
+        taskTop = task.getTopActivity();
+        if (topOptions != null) {
+            // If we got some ActivityOptions from an activity on top that
+            // was removed from the task, propagate them to the new real top.
+            if (taskTop != null) {
+                if (VALIDATE_TASK_REPLACE) Slog.w(TAG,
+                    "newResetTaskIfNeededLocked: would call updateOptionsLocked " + topOptions);
+                taskTop.updateOptionsLocked(topOptions);
+            } else {
+                if (VALIDATE_TASK_REPLACE) Slog.w(TAG,
+                    "newResetTaskIfNeededLocked: would call " + topOptions + " abort");
+                topOptions.abort();
+            }
+        }
+
+        return taskTop;
+    }
+
     /**
      * Perform clear operation as requested by
      * {@link Intent#FLAG_ACTIVITY_CLEAR_TOP}: search from the top of the