Merge "Cleanup ActivityRecord#frontOfTask and its usage"
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 77fca8f..be6cdcf 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -1315,7 +1315,11 @@
          it will yield the baseIntent to any activity that it launches in the same task. This
          continues until an activity is encountered which has this attribute set to false. False
          is the default. This attribute set to true also permits activity's use of the
-         TaskDescription to change labels, colors and icons in the recent task list. -->
+         TaskDescription to change labels, colors and icons in the recent task list.
+
+         <p>NOTE: Setting this flag to <code>true</code> will not change the affinity of the task,
+         which is used for intent resolution during activity launch. The task's root activity will
+         always define its affinity. -->
     <attr name="relinquishTaskIdentity" format="boolean" />
 
     <!-- Indicate that it is okay for this activity be resumed while the previous
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 99d2903..2f7311b 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -319,8 +319,6 @@
     private ActivityState mState;    // current state we are in
     Bundle  icicle;         // last saved activity state
     PersistableBundle persistentState; // last persistently saved activity state
-    // TODO: See if this is still needed.
-    boolean frontOfTask;    // is this the root activity of its task?
     boolean launchFailed;   // set if a launched failed, to abort on 2nd try
     boolean haveState;      // have we gotten the last activity state?
     boolean stopped;        // is activity pause finished?
@@ -439,7 +437,7 @@
                 pw.print(" userId="); pw.println(mUserId);
         pw.print(prefix); pw.print("app="); pw.println(app);
         pw.print(prefix); pw.println(intent.toInsecureStringWithClip());
-        pw.print(prefix); pw.print("frontOfTask="); pw.print(frontOfTask);
+        pw.print(prefix); pw.print("rootOfTask="); pw.print(isRootOfTask());
                 pw.print(" task="); pw.println(task);
         pw.print(prefix); pw.print("taskAffinity="); pw.println(taskAffinity);
         pw.print(prefix); pw.print("mActivityComponent=");
@@ -957,7 +955,6 @@
         resultWho = _resultWho;
         requestCode = _reqCode;
         setState(INITIALIZING, "ActivityRecord ctor");
-        frontOfTask = false;
         launchFailed = false;
         stopped = false;
         delayedResume = false;
@@ -2552,7 +2549,8 @@
         }
         final TaskRecord task = r.task;
         final int activityNdx = task.mActivities.indexOf(r);
-        if (activityNdx < 0 || (onlyRoot && activityNdx > task.findEffectiveRootIndex())) {
+        if (activityNdx < 0
+                || (onlyRoot && activityNdx > task.findRootIndex(true /* effectiveRoot */))) {
             return INVALID_TASK_ID;
         }
         return task.taskId;
@@ -3803,6 +3801,27 @@
         return display != null && this == display.getResumedActivity();
     }
 
+
+    /**
+     * Check if this is the root of the task - first activity that is not finishing, starting from
+     * the bottom of the task. If all activities are finishing - then this method will return
+     * {@code true} if the activity is at the bottom.
+     *
+     * NOTE: This is different from 'effective root' - an activity that defines the task identity.
+     */
+    boolean isRootOfTask() {
+        if (task == null) {
+            return false;
+        }
+        final ActivityRecord rootActivity = task.getRootActivity();
+        if (rootActivity != null) {
+            return this == rootActivity;
+        }
+        // No non-finishing activity found. In this case the bottom-most activity is considered to
+        // be the root.
+        return task.getChildAt(0) == this;
+    }
+
     void registerRemoteAnimations(RemoteAnimationDefinition definition) {
         if (mAppWindowToken == null) {
             Slog.w(TAG_WM, "Attempted to register remote animations with non-existing app"
@@ -3846,7 +3865,7 @@
         writeIdentifierToProto(proto, IDENTIFIER);
         proto.write(STATE, mState.toString());
         proto.write(VISIBLE, visible);
-        proto.write(FRONT_OF_TASK, frontOfTask);
+        proto.write(FRONT_OF_TASK, isRootOfTask());
         if (hasProcess()) {
             proto.write(PROC_ID, app.getPid());
         }
diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java
index 79098f1..235fd2e 100644
--- a/services/core/java/com/android/server/wm/ActivityStack.java
+++ b/services/core/java/com/android/server/wm/ActivityStack.java
@@ -1895,7 +1895,7 @@
         // last of activity of the last task the stack will be empty and must
         // be cleared immediately.
         boolean forceIdle = mStackSupervisor.mStoppingActivities.size() > MAX_STOPPING_TO_FORCE
-                || (r.frontOfTask && mTaskHistory.size() <= 1);
+                || (r.isRootOfTask() && mTaskHistory.size() <= 1);
         if (scheduleIdle || forceIdle) {
             if (DEBUG_PAUSE) Slog.v(TAG_PAUSE, "Scheduling idle now: forceIdle="
                     + forceIdle + "immediate=" + !idleDelayed);
@@ -3209,8 +3209,6 @@
             r.createAppWindowToken();
         }
 
-        task.setFrontOfTask();
-
         // The transition animation and starting window are not needed if {@code allowMoveToFront}
         // is false, because the activity won't be visible.
         if ((!isHomeOrRecentsStack() || numActivities() > 0) && allowMoveToFront) {
@@ -3322,20 +3320,17 @@
     }
 
     /**
-     * Perform a reset of the given task, if needed as part of launching it.
-     * Returns the new HistoryRecord at the top of the task.
-     */
-    /**
-     * 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.
+     * Helper method for {@link #resetTaskIfNeededLocked(ActivityRecord, ActivityRecord)}.
+     * Performs a reset of the given task, if needed for new activity start.
      * @param task The task containing the Activity (taskTop) that might be reset.
-     * @param forceReset
+     * @param forceReset Flag indicating if clear task was requested
      * @return An ActivityOptions that needs to be processed.
      */
     private ActivityOptions resetTargetTaskIfNeededLocked(TaskRecord task, boolean forceReset) {
         ActivityOptions topOptions = null;
 
+        // Tracker of the end of currently handled reply chain (sublist) of activities. What happens
+        // to activities in the same chain will depend on what the end activity of the chain needs.
         int replyChainEnd = -1;
         boolean canMoveOptions = true;
 
@@ -3343,11 +3338,14 @@
         // the root, we may no longer have the task!).
         final ArrayList<ActivityRecord> activities = task.mActivities;
         final int numActivities = activities.size();
-        final int rootActivityNdx = task.findEffectiveRootIndex();
-        for (int i = numActivities - 1; i > rootActivityNdx; --i ) {
+        int lastActivityNdx = task.findRootIndex(true /* effectiveRoot */);
+        if (lastActivityNdx == -1) {
+            lastActivityNdx = 0;
+        }
+        for (int i = numActivities - 1; i > lastActivityNdx; --i) {
             ActivityRecord target = activities.get(i);
-            if (target.frontOfTask)
-                break;
+            // TODO: Why is this needed? Looks like we're breaking the loop before we reach the root
+            if (target.isRootOfTask()) break;
 
             final int flags = target.info.flags;
             final boolean finishOnTaskLaunch =
@@ -3380,12 +3378,13 @@
                 // bottom of the activity stack.  This also keeps it
                 // correctly ordered with any activities we previously
                 // moved.
+                // TODO: We should probably look for other stacks also, since corresponding task
+                // with the same affinity is unlikely to be in the same stack.
                 final TaskRecord targetTask;
                 final ActivityRecord bottom =
                         !mTaskHistory.isEmpty() && !mTaskHistory.get(0).mActivities.isEmpty() ?
                                 mTaskHistory.get(0).mActivities.get(0) : null;
-                if (bottom != null && target.taskAffinity != null
-                        && target.taskAffinity.equals(bottom.getTaskRecord().affinity)) {
+                if (bottom != null && target.taskAffinity.equals(bottom.getTaskRecord().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.
@@ -3395,7 +3394,8 @@
                 } else {
                     targetTask = createTaskRecord(
                             mStackSupervisor.getNextTaskIdForUserLocked(target.mUserId),
-                            target.info, null, null, null, false);
+                            target.info, null /* intent */, null /* voiceSession */,
+                            null /* voiceInteractor */, false /* toTop */);
                     targetTask.affinityIntent = target.intent;
                     if (DEBUG_TASKS) Slog.v(TAG_TASKS, "Start pushing activity " + target
                             + " out to new task " + targetTask);
@@ -3476,28 +3476,34 @@
     }
 
     /**
-     * Helper method for #resetTaskIfNeededLocked. Processes all of the activities in a given
-     * TaskRecord looking for an affinity with the task of resetTaskIfNeededLocked.taskTop.
+     * Helper method for {@link #resetTaskIfNeededLocked(ActivityRecord, ActivityRecord)}.
+     * 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.
+     * @param forceReset Flag indicating if clear task was requested
      */
+    // TODO: Consider merging with #resetTargetTaskIfNeededLocked() above
     private int resetAffinityTaskIfNeededLocked(TaskRecord affinityTask, TaskRecord task,
             boolean topTaskIsHigher, boolean forceReset, int taskInsertionPoint) {
+        // Tracker of the end of currently handled reply chain (sublist) of activities. What happens
+        // to activities in the same chain will depend on what the end activity of the chain needs.
         int replyChainEnd = -1;
-        final int taskId = task.taskId;
         final String taskAffinity = task.affinity;
 
         final ArrayList<ActivityRecord> activities = affinityTask.mActivities;
         final int numActivities = activities.size();
-        final int rootActivityNdx = affinityTask.findEffectiveRootIndex();
 
         // Do not operate on or below the effective root Activity.
-        for (int i = numActivities - 1; i > rootActivityNdx; --i) {
+        int lastActivityNdx = affinityTask.findRootIndex(true /* effectiveRoot */);
+        if (lastActivityNdx == -1) {
+            lastActivityNdx = 0;
+        }
+        for (int i = numActivities - 1; i > lastActivityNdx; --i) {
             ActivityRecord target = activities.get(i);
-            if (target.frontOfTask)
-                break;
+            // TODO: Why is this needed? Looks like we're breaking the loop before we reach the root
+            if (target.isRootOfTask()) break;
 
             final int flags = target.info.flags;
             boolean finishOnTaskLaunch = (flags & ActivityInfo.FLAG_FINISH_ON_TASK_LAUNCH) != 0;
@@ -3590,11 +3596,11 @@
                 (newActivity.info.flags & ActivityInfo.FLAG_CLEAR_TASK_ON_LAUNCH) != 0;
         final TaskRecord task = taskTop.getTaskRecord();
 
-        /** False until we evaluate the TaskRecord associated with taskTop. Switches to true
-         * for remaining tasks. Used for later tasks to reparent to 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. */
+        // If ActivityOptions are moved out and need to be aborted or moved to taskTop.
         ActivityOptions topOptions = null;
 
         // Preserve the location for reparenting in the new task.
@@ -4009,7 +4015,6 @@
             final ArrayList<ActivityRecord> activities = task.mActivities;
             final int index = activities.indexOf(r);
             if (index < (activities.size() - 1)) {
-                task.setFrontOfTask();
                 if ((r.intent.getFlags() & Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET) != 0) {
                     // If the caller asked that this activity (and all above it)
                     // be cleared when the task is reset, don't lose that information,
@@ -4244,7 +4249,8 @@
         // of a document, unless simply finishing it will return them to the the
         // correct app behind.
         final TaskRecord task = srec.getTaskRecord();
-        if (srec.frontOfTask && task.getBaseIntent() != null && task.getBaseIntent().isDocument()) {
+        if (srec.isRootOfTask() && task.getBaseIntent() != null
+                && task.getBaseIntent().isDocument()) {
             // Okay, this activity is at the root of its task.  What to do, what to do...
             if (!inFrontOfStandardStack()) {
                 // Finishing won't return to an application, so we need to recreate.
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 6fd2ebc..0a8f870 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -1557,7 +1557,7 @@
                 }
 
                 if (top != null) {
-                    if (top.frontOfTask) {
+                    if (top.isRootOfTask()) {
                         // Activity aliases may mean we use different intents for the top activity,
                         // so make sure the task now has the identity of the new intent.
                         top.getTaskRecord().setIntent(mStartActivity);
@@ -2260,7 +2260,7 @@
                         || LAUNCH_SINGLE_TOP == mLaunchMode)
                     && intentActivity.mActivityComponent.equals(
                             mStartActivity.mActivityComponent)) {
-                if (intentActivity.frontOfTask) {
+                if (intentActivity.isRootOfTask()) {
                     intentActivity.getTaskRecord().setIntent(mStartActivity);
                 }
                 deliverNewIntent(intentActivity);
diff --git a/services/core/java/com/android/server/wm/TaskRecord.java b/services/core/java/com/android/server/wm/TaskRecord.java
index 7c7fdae..d1c0c07 100644
--- a/services/core/java/com/android/server/wm/TaskRecord.java
+++ b/services/core/java/com/android/server/wm/TaskRecord.java
@@ -1113,16 +1113,14 @@
         return intent != null ? intent : affinityIntent;
     }
 
-    /** Returns the first non-finishing activity from the root. */
+    /** Returns the first non-finishing activity from the bottom. */
     ActivityRecord getRootActivity() {
-        for (int i = 0; i < mActivities.size(); i++) {
-            final ActivityRecord r = mActivities.get(i);
-            if (r.finishing) {
-                continue;
-            }
-            return r;
+        final int rootActivityIndex = findRootIndex(false /* effectiveRoot */);
+        if (rootActivityIndex == -1) {
+            // There are no non-finishing activities in the task.
+            return null;
         }
-        return null;
+        return mActivities.get(rootActivityIndex);
     }
 
     ActivityRecord getTopActivity() {
@@ -1237,27 +1235,6 @@
                 || topRunningActivityLocked() != null;
     }
 
-    /** Call after activity movement or finish to make sure that frontOfTask is set correctly */
-    final void setFrontOfTask() {
-        boolean foundFront = false;
-        final int numActivities = mActivities.size();
-        for (int activityNdx = 0; activityNdx < numActivities; ++activityNdx) {
-            final ActivityRecord r = mActivities.get(activityNdx);
-            if (foundFront || r.finishing) {
-                r.frontOfTask = false;
-            } else {
-                r.frontOfTask = true;
-                // Set frontOfTask false for every following activity.
-                foundFront = true;
-            }
-        }
-        if (!foundFront && numActivities > 0) {
-            // All activities of this task are finishing. As we ought to have a frontOfTask
-            // activity, make the bottom activity front.
-            mActivities.get(0).frontOfTask = true;
-        }
-    }
-
     /**
      * Reorder the history stack so that the passed activity is brought to the front.
      */
@@ -1272,8 +1249,6 @@
         // Make sure window manager is aware of the position change.
         mTask.positionChildAtTop(newTop.mAppWindowToken);
         updateEffectiveIntent();
-
-        setFrontOfTask();
     }
 
     void addActivityToTop(ActivityRecord r) {
@@ -1658,6 +1633,7 @@
 
     /** Updates the last task description values. */
     void updateTaskDescription() {
+        // TODO(AM refactor): Cleanup to use findRootIndex()
         // Traverse upwards looking for any break between main task activities and
         // utility activities.
         int activityNdx;
@@ -1736,8 +1712,19 @@
         }
     }
 
-    int findEffectiveRootIndex() {
-        int effectiveNdx = 0;
+    /**
+     * Find the index of the root activity in the task. It will be the first activity from the
+     * bottom that is not finishing.
+     *
+     * @param effectiveRoot Flag indicating whether 'effective root' should be returned - an
+     *                      activity that defines the task identity (its base intent). It's the
+     *                      first one that does not have
+     *                      {@link ActivityInfo#FLAG_RELINQUISH_TASK_IDENTITY}.
+     * @return index of the 'root' or 'effective' root in the list of activities, -1 if no eligible
+     *         activity was found.
+     */
+    int findRootIndex(boolean effectiveRoot) {
+        int effectiveNdx = -1;
         final int topActivityNdx = mActivities.size() - 1;
         for (int activityNdx = 0; activityNdx <= topActivityNdx; ++activityNdx) {
             final ActivityRecord r = mActivities.get(activityNdx);
@@ -1745,15 +1732,22 @@
                 continue;
             }
             effectiveNdx = activityNdx;
-            if ((r.info.flags & FLAG_RELINQUISH_TASK_IDENTITY) == 0) {
+            if (!effectiveRoot || (r.info.flags & FLAG_RELINQUISH_TASK_IDENTITY) == 0) {
                 break;
             }
         }
         return effectiveNdx;
     }
 
+    // TODO (AM refactor): Invoke automatically when there is a change in children
+    @VisibleForTesting
     void updateEffectiveIntent() {
-        final int effectiveRootIndex = findEffectiveRootIndex();
+        int effectiveRootIndex = findRootIndex(true /* effectiveRoot */);
+        if (effectiveRootIndex == -1) {
+            // All activities in the task are either finishing or relinquish task identity.
+            // But we still want to update the intent, so let's use the bottom activity.
+            effectiveRootIndex = 0;
+        }
         final ActivityRecord r = mActivities.get(effectiveRootIndex);
         setIntent(r);
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java
index 0ecd878..a0302f6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java
@@ -16,11 +16,13 @@
 
 package com.android.server.wm;
 
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME;
+import static android.content.pm.ActivityInfo.FLAG_RELINQUISH_TASK_IDENTITY;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
@@ -29,6 +31,9 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 
 import static org.hamcrest.Matchers.not;
@@ -36,6 +41,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -51,6 +57,7 @@
 import android.content.pm.ApplicationInfo;
 import android.content.res.Configuration;
 import android.graphics.Rect;
+import android.os.IBinder;
 import android.platform.test.annotations.Presubmit;
 import android.service.voice.IVoiceInteractionSession;
 import android.util.DisplayMetrics;
@@ -443,6 +450,342 @@
                 task.isSameIntentFilter(defaultActivity));
     }
 
+    /** Test that root activity index is reported correctly for several activities in the task. */
+    @Test
+    public void testFindRootIndex() {
+        final TaskRecord task = getTestTask();
+        // Add an extra activity on top of the root one
+        new ActivityBuilder(mService).setTask(task).build();
+
+        assertEquals("The root activity in the task must be reported.",
+                0, task.findRootIndex(false /* effectiveRoot*/));
+    }
+
+    /**
+     * Test that root activity index is reported correctly for several activities in the task when
+     * the activities on the bottom are finishing.
+     */
+    @Test
+    public void testFindRootIndex_finishing() {
+        final TaskRecord task = getTestTask();
+        // Add extra two activities and mark the two on the bottom as finishing.
+        final ActivityRecord activity0 = task.getChildAt(0);
+        activity0.finishing = true;
+        final ActivityRecord activity1 = new ActivityBuilder(mService).setTask(task).build();
+        activity1.finishing = true;
+        new ActivityBuilder(mService).setTask(task).build();
+
+        assertEquals("The first non-finishing activity in the task must be reported.",
+                2, task.findRootIndex(false /* effectiveRoot*/));
+    }
+
+    /**
+     * Test that root activity index is reported correctly for several activities in the task when
+     * looking for the 'effective root'.
+     */
+    @Test
+    public void testFindRootIndex_effectiveRoot() {
+        final TaskRecord task = getTestTask();
+        // Add an extra activity on top of the root one
+        new ActivityBuilder(mService).setTask(task).build();
+
+        assertEquals("The root activity in the task must be reported.",
+                0, task.findRootIndex(true /* effectiveRoot*/));
+    }
+
+    /**
+     * Test that root activity index is reported correctly when looking for the 'effective root' in
+     * case when bottom activities are relinquishing task identity or finishing.
+     */
+    @Test
+    public void testFindRootIndex_effectiveRoot_finishingAndRelinquishing() {
+        final TaskRecord task = getTestTask();
+        // Add extra two activities. Mark the one on the bottom with "relinquishTaskIdentity" and
+        // one above as finishing.
+        final ActivityRecord activity0 = task.getChildAt(0);
+        activity0.info.flags |= FLAG_RELINQUISH_TASK_IDENTITY;
+        final ActivityRecord activity1 = new ActivityBuilder(mService).setTask(task).build();
+        activity1.finishing = true;
+        new ActivityBuilder(mService).setTask(task).build();
+
+        assertEquals("The first non-finishing activity and non-relinquishing task identity "
+                        + "must be reported.", 2, task.findRootIndex(true /* effectiveRoot*/));
+    }
+
+    /**
+     * Test that root activity index is reported correctly when looking for the 'effective root'
+     * for the case when there is only a single activity that also has relinquishTaskIdentity set.
+     */
+    @Test
+    public void testFindRootIndex_effectiveRoot_relinquishingAndSingleActivity() {
+        final TaskRecord task = getTestTask();
+        // Set relinquishTaskIdentity for the only activity in the task
+        task.getChildAt(0).info.flags |= FLAG_RELINQUISH_TASK_IDENTITY;
+
+        assertEquals("The root activity in the task must be reported.",
+                0, task.findRootIndex(true /* effectiveRoot*/));
+    }
+
+    /**
+     * Test that the topmost activity index is reported correctly when looking for the
+     * 'effective root' for the case when all activities have relinquishTaskIdentity set.
+     */
+    @Test
+    public void testFindRootIndex_effectiveRoot_relinquishingMultipleActivities() {
+        final TaskRecord task = getTestTask();
+        // Set relinquishTaskIdentity for all activities in the task
+        final ActivityRecord activity0 = task.getChildAt(0);
+        activity0.info.flags |= FLAG_RELINQUISH_TASK_IDENTITY;
+        final ActivityRecord activity1 = new ActivityBuilder(mService).setTask(task).build();
+        activity1.info.flags |= FLAG_RELINQUISH_TASK_IDENTITY;
+
+        assertEquals("The topmost activity in the task must be reported.",
+                task.getChildCount() - 1, task.findRootIndex(true /* effectiveRoot*/));
+    }
+
+    /** Test that bottom-most activity is reported in {@link TaskRecord#getRootActivity()}. */
+    @Test
+    public void testGetRootActivity() {
+        final TaskRecord task = getTestTask();
+        // Add an extra activity on top of the root one
+        new ActivityBuilder(mService).setTask(task).build();
+
+        assertEquals("The root activity in the task must be reported.",
+                task.getChildAt(0), task.getRootActivity());
+    }
+
+    /**
+     * Test that first non-finishing activity is reported in {@link TaskRecord#getRootActivity()}.
+     */
+    @Test
+    public void testGetRootActivity_finishing() {
+        final TaskRecord task = getTestTask();
+        // Add an extra activity on top of the root one
+        new ActivityBuilder(mService).setTask(task).build();
+        // Mark the root as finishing
+        task.getChildAt(0).finishing = true;
+
+        assertEquals("The first non-finishing activity in the task must be reported.",
+                task.getChildAt(1), task.getRootActivity());
+    }
+
+    /**
+     * Test that relinquishTaskIdentity flag is ignored in {@link TaskRecord#getRootActivity()}.
+     */
+    @Test
+    public void testGetRootActivity_relinquishTaskIdentity() {
+        final TaskRecord task = getTestTask();
+        // Mark the bottom-most activity with FLAG_RELINQUISH_TASK_IDENTITY.
+        final ActivityRecord activity0 = task.getChildAt(0);
+        activity0.info.flags |= FLAG_RELINQUISH_TASK_IDENTITY;
+        // Add an extra activity on top of the root one.
+        new ActivityBuilder(mService).setTask(task).build();
+
+        assertEquals("The root activity in the task must be reported.",
+                task.getChildAt(0), task.getRootActivity());
+    }
+
+    /**
+     * Test that no activity is reported in {@link TaskRecord#getRootActivity()} when all activities
+     * in the task are finishing.
+     */
+    @Test
+    public void testGetRootActivity_allFinishing() {
+        final TaskRecord task = getTestTask();
+        // Mark the bottom-most activity as finishing.
+        final ActivityRecord activity0 = task.getChildAt(0);
+        activity0.finishing = true;
+        // Add an extra activity on top of the root one and mark it as finishing
+        final ActivityRecord activity1 = new ActivityBuilder(mService).setTask(task).build();
+        activity1.finishing = true;
+
+        assertNull("No activity must be reported if all are finishing", task.getRootActivity());
+    }
+
+    /**
+     * Test that first non-finishing activity is the root of task.
+     */
+    @Test
+    public void testIsRootActivity() {
+        final TaskRecord task = getTestTask();
+        // Mark the bottom-most activity as finishing.
+        final ActivityRecord activity0 = task.getChildAt(0);
+        activity0.finishing = true;
+        // Add an extra activity on top of the root one.
+        final ActivityRecord activity1 = new ActivityBuilder(mService).setTask(task).build();
+
+        assertFalse("Finishing activity must not be the root of task", activity0.isRootOfTask());
+        assertTrue("Non-finishing activity must be the root of task", activity1.isRootOfTask());
+    }
+
+    /**
+     * Test that if all activities in the task are finishing, then the one on the bottom is the
+     * root of task.
+     */
+    @Test
+    public void testIsRootActivity_allFinishing() {
+        final TaskRecord task = getTestTask();
+        // Mark the bottom-most activity as finishing.
+        final ActivityRecord activity0 = task.getChildAt(0);
+        activity0.finishing = true;
+        // Add an extra activity on top of the root one and mark it as finishing
+        final ActivityRecord activity1 = new ActivityBuilder(mService).setTask(task).build();
+        activity1.finishing = true;
+
+        assertTrue("Bottom activity must be the root of task", activity0.isRootOfTask());
+        assertFalse("Finishing activity on top must not be the root of task",
+                activity1.isRootOfTask());
+    }
+
+    /**
+     * Test {@link ActivityRecord#getTaskForActivityLocked(IBinder, boolean)}.
+     */
+    @Test
+    public void testGetTaskForActivity() {
+        final TaskRecord task0 = getTestTask();
+        final ActivityRecord activity0 = task0.getChildAt(0);
+
+        final TaskRecord task1 = getTestTask();
+        final ActivityRecord activity1 = task0.getChildAt(0);
+
+        assertEquals(task0.taskId,
+                ActivityRecord.getTaskForActivityLocked(activity0.appToken, false /* onlyRoot */));
+        assertEquals(task1.taskId,
+                ActivityRecord.getTaskForActivityLocked(activity1.appToken,  false /* onlyRoot */));
+    }
+
+    /**
+     * Test {@link ActivityRecord#getTaskForActivityLocked(IBinder, boolean)} with finishing
+     * activity.
+     */
+    @Test
+    public void testGetTaskForActivity_onlyRoot_finishing() {
+        final TaskRecord task = getTestTask();
+        // Make the current root activity finishing
+        final ActivityRecord activity0 = task.getChildAt(0);
+        activity0.finishing = true;
+        // Add an extra activity on top - this will be the new root
+        final ActivityRecord activity1 = new ActivityBuilder(mService).setTask(task).build();
+        // Add one more on top
+        final ActivityRecord activity2 = new ActivityBuilder(mService).setTask(task).build();
+
+        assertEquals(task.taskId,
+                ActivityRecord.getTaskForActivityLocked(activity0.appToken, true /* onlyRoot */));
+        assertEquals(task.taskId,
+                ActivityRecord.getTaskForActivityLocked(activity1.appToken, true /* onlyRoot */));
+        assertEquals("No task must be reported for activity that is above root", INVALID_TASK_ID,
+                ActivityRecord.getTaskForActivityLocked(activity2.appToken, true /* onlyRoot */));
+    }
+
+    /**
+     * Test {@link ActivityRecord#getTaskForActivityLocked(IBinder, boolean)} with activity that
+     * relinquishes task identity.
+     */
+    @Test
+    public void testGetTaskForActivity_onlyRoot_relinquishTaskIdentity() {
+        final TaskRecord task = getTestTask();
+        // Make the current root activity relinquish task identity
+        final ActivityRecord activity0 = task.getChildAt(0);
+        activity0.info.flags |= FLAG_RELINQUISH_TASK_IDENTITY;
+        // Add an extra activity on top - this will be the new root
+        final ActivityRecord activity1 = new ActivityBuilder(mService).setTask(task).build();
+        // Add one more on top
+        final ActivityRecord activity2 = new ActivityBuilder(mService).setTask(task).build();
+
+        assertEquals(task.taskId,
+                ActivityRecord.getTaskForActivityLocked(activity0.appToken, true /* onlyRoot */));
+        assertEquals(task.taskId,
+                ActivityRecord.getTaskForActivityLocked(activity1.appToken, true /* onlyRoot */));
+        assertEquals("No task must be reported for activity that is above root", INVALID_TASK_ID,
+                ActivityRecord.getTaskForActivityLocked(activity2.appToken, true /* onlyRoot */));
+    }
+
+    /**
+     * Test {@link ActivityRecord#getTaskForActivityLocked(IBinder, boolean)} allowing non-root
+     * entries.
+     */
+    @Test
+    public void testGetTaskForActivity_notOnlyRoot() {
+        final TaskRecord task = getTestTask();
+        // Mark the bottom-most activity as finishing.
+        final ActivityRecord activity0 = task.getChildAt(0);
+        activity0.finishing = true;
+
+        // Add an extra activity on top of the root one and make it relinquish task identity
+        final ActivityRecord activity1 = new ActivityBuilder(mService).setTask(task).build();
+        activity1.info.flags |= FLAG_RELINQUISH_TASK_IDENTITY;
+
+        // Add one more activity on top
+        final ActivityRecord activity2 = new ActivityBuilder(mService).setTask(task).build();
+
+        assertEquals(task.taskId,
+                ActivityRecord.getTaskForActivityLocked(activity0.appToken, false /* onlyRoot */));
+        assertEquals(task.taskId,
+                ActivityRecord.getTaskForActivityLocked(activity1.appToken, false /* onlyRoot */));
+        assertEquals(task.taskId,
+                ActivityRecord.getTaskForActivityLocked(activity2.appToken, false /* onlyRoot */));
+    }
+
+    /**
+     * Test {@link TaskRecord#updateEffectiveIntent()}.
+     */
+    @Test
+    public void testUpdateEffectiveIntent() {
+        // Test simple case with a single activity.
+        final TaskRecord task = getTestTask();
+        final ActivityRecord activity0 = task.getChildAt(0);
+
+        spyOn(task);
+        task.updateEffectiveIntent();
+        verify(task).setIntent(eq(activity0));
+    }
+
+    /**
+     * Test {@link TaskRecord#updateEffectiveIntent()} with root activity marked as finishing. This
+     * should make the task use the second activity when updating the intent.
+     */
+    @Test
+    public void testUpdateEffectiveIntent_rootFinishing() {
+        // Test simple case with a single activity.
+        final TaskRecord task = getTestTask();
+        final ActivityRecord activity0 = task.getChildAt(0);
+        // Mark the bottom-most activity as finishing.
+        activity0.finishing = true;
+        // Add an extra activity on top of the root one
+        final ActivityRecord activity1 = new ActivityBuilder(mService).setTask(task).build();
+
+        spyOn(task);
+        task.updateEffectiveIntent();
+        verify(task).setIntent(eq(activity1));
+    }
+
+    /**
+     * Test {@link TaskRecord#updateEffectiveIntent()} when all activities are finishing or
+     * relinquishing task identity. In this case the root activity should still be used when
+     * updating the intent (legacy behavior).
+     */
+    @Test
+    public void testUpdateEffectiveIntent_allFinishing() {
+        // Test simple case with a single activity.
+        final TaskRecord task = getTestTask();
+        final ActivityRecord activity0 = task.getChildAt(0);
+        // Mark the bottom-most activity as finishing.
+        activity0.finishing = true;
+        // Add an extra activity on top of the root one and make it relinquish task identity
+        final ActivityRecord activity1 = new ActivityBuilder(mService).setTask(task).build();
+        activity1.finishing = true;
+
+        // Task must still update the intent using the root activity (preserving legacy behavior).
+        spyOn(task);
+        task.updateEffectiveIntent();
+        verify(task).setIntent(eq(activity0));
+    }
+
+    private TaskRecord getTestTask() {
+        final ActivityStack stack = new StackBuilder(mRootActivityContainer).build();
+        return stack.getChildAt(0);
+    }
+
     private void testStackBoundsConfiguration(int windowingMode, Rect parentBounds, Rect bounds,
             Rect expectedConfigBounds) {