1/Clean up RecentTasks list

- Move some logic into the RecentTasks list
- Added some callbacks for future handling of trimming recent tasks based
  on additional policy
- Fix issue when cleaning up tasks where the wrong activity/app info can
  be fetched since the tmp caches were not cleared
- Add some initial tests for existing task list behaviour.

Bug: 34270611
Test: runtest --path frameworks/base/services/tests/servicestests/src/com/android/server/am/RecentTasksTest.java

Change-Id: I26730461fa5fb3f5afdabe092438c229381f4f2b
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index a1a7a32..90a5ca2 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -31,7 +31,6 @@
 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.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
 import static android.content.pm.PackageManager.FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS;
@@ -130,7 +129,6 @@
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PROCESS_OBSERVERS;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PROVIDER;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PSS;
-import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_RECENTS;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_SERVICE;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_STACK;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_SWITCH;
@@ -5960,16 +5958,7 @@
 
                 if (appInfo != null) {
                     forceStopPackageLocked(packageName, appInfo.uid, "clear data");
-                    // Remove all tasks match the cleared application package and user
-                    for (int i = mRecentTasks.size() - 1; i >= 0; i--) {
-                        final TaskRecord tr = mRecentTasks.get(i);
-                        final String taskPackageName =
-                                tr.getBaseIntent().getComponent().getPackageName();
-                        if (tr.userId != resolvedUserId) continue;
-                        if (!taskPackageName.equals(packageName)) continue;
-                        mStackSupervisor.removeTaskByIdLocked(tr.taskId, false,
-                                REMOVE_FROM_RECENTS);
-                    }
+                    mRecentTasks.removeTasksByPackageName(packageName, resolvedUserId);
                 }
             }
 
@@ -6550,7 +6539,7 @@
         }
 
         // Clean-up disabled tasks
-        cleanupDisabledPackageTasksLocked(packageName, disabledClasses, userId);
+        mRecentTasks.cleanupDisabledPackageTasksLocked(packageName, disabledClasses, userId);
 
         // Clean-up disabled services.
         mServices.bringDownDisabledPackageServicesLocked(
@@ -9797,35 +9786,12 @@
     public List<IBinder> getAppTasks(String callingPackage) {
         int callingUid = Binder.getCallingUid();
         long ident = Binder.clearCallingIdentity();
-
-        synchronized(this) {
-            ArrayList<IBinder> list = new ArrayList<IBinder>();
-            try {
-                if (DEBUG_ALL) Slog.v(TAG, "getAppTasks");
-
-                final int N = mRecentTasks.size();
-                for (int i = 0; i < N; i++) {
-                    TaskRecord tr = mRecentTasks.get(i);
-                    // Skip tasks that do not match the caller.  We don't need to verify
-                    // callingPackage, because we are also limiting to callingUid and know
-                    // that will limit to the correct security sandbox.
-                    if (tr.effectiveUid != callingUid) {
-                        continue;
-                    }
-                    Intent intent = tr.getBaseIntent();
-                    if (intent == null ||
-                            !callingPackage.equals(intent.getComponent().getPackageName())) {
-                        continue;
-                    }
-                    ActivityManager.RecentTaskInfo taskInfo =
-                            createRecentTaskInfoFromTaskRecord(tr);
-                    AppTaskImpl taskImpl = new AppTaskImpl(taskInfo.persistentId, callingUid);
-                    list.add(taskImpl.asBinder());
-                }
-            } finally {
-                Binder.restoreCallingIdentity(ident);
+        try {
+            synchronized(this) {
+                return mRecentTasks.getAppTasksList(callingUid, callingPackage);
             }
-            return list;
+        } finally {
+            Binder.restoreCallingIdentity(ident);
         }
     }
 
@@ -9848,58 +9814,6 @@
         return list;
     }
 
-    /**
-     * Creates a new RecentTaskInfo from a TaskRecord.
-     */
-    private ActivityManager.RecentTaskInfo createRecentTaskInfoFromTaskRecord(TaskRecord tr) {
-        // Update the task description to reflect any changes in the task stack
-        tr.updateTaskDescription();
-
-        // Compose the recent task info
-        ActivityManager.RecentTaskInfo rti = new ActivityManager.RecentTaskInfo();
-        rti.id = tr.getTopActivity() == null ? INVALID_TASK_ID : tr.taskId;
-        rti.persistentId = tr.taskId;
-        rti.baseIntent = new Intent(tr.getBaseIntent());
-        rti.origActivity = tr.origActivity;
-        rti.realActivity = tr.realActivity;
-        rti.description = tr.lastDescription;
-        rti.stackId = tr.getStackId();
-        rti.userId = tr.userId;
-        rti.taskDescription = new ActivityManager.TaskDescription(tr.lastTaskDescription);
-        rti.firstActiveTime = tr.firstActiveTime;
-        rti.lastActiveTime = tr.lastActiveTime;
-        rti.affiliatedTaskId = tr.mAffiliatedTaskId;
-        rti.affiliatedTaskColor = tr.mAffiliatedTaskColor;
-        rti.numActivities = 0;
-        if (tr.mBounds != null) {
-            rti.bounds = new Rect(tr.mBounds);
-        }
-        rti.supportsSplitScreenMultiWindow = tr.supportsSplitScreenWindowingMode();
-        rti.resizeMode = tr.mResizeMode;
-        rti.configuration.setTo(tr.getConfiguration());
-
-        ActivityRecord base = null;
-        ActivityRecord top = null;
-        ActivityRecord tmp;
-
-        for (int i = tr.mActivities.size() - 1; i >= 0; --i) {
-            tmp = tr.mActivities.get(i);
-            if (tmp.finishing) {
-                continue;
-            }
-            base = tmp;
-            if (top == null || (top.state == ActivityState.INITIALIZING)) {
-                top = base;
-            }
-            rti.numActivities++;
-        }
-
-        rti.baseActivity = (base != null) ? base.intent.getComponent() : null;
-        rti.topActivity = (top != null) ? top.intent.getComponent() : null;
-
-        return rti;
-    }
-
     private boolean isGetTasksAllowed(String caller, int callingPid, int callingUid) {
         boolean allowed = checkPermission(android.Manifest.permission.REAL_GET_TASKS,
                 callingPid, callingUid) == PackageManager.PERMISSION_GRANTED;
@@ -9933,119 +9847,15 @@
         final int callingUid = Binder.getCallingUid();
         userId = mUserController.handleIncomingUser(Binder.getCallingPid(), callingUid, userId,
                 false, ALLOW_FULL_ONLY, "getRecentTasks", null);
+        final boolean allowed = isGetTasksAllowed("getRecentTasks", Binder.getCallingPid(),
+                callingUid);
+        final boolean detailed = checkCallingPermission(
+                android.Manifest.permission.GET_DETAILED_TASKS)
+                        == PackageManager.PERMISSION_GRANTED;
 
-        final boolean includeProfiles = (flags & ActivityManager.RECENT_INCLUDE_PROFILES) != 0;
-        final boolean withExcluded = (flags&ActivityManager.RECENT_WITH_EXCLUDED) != 0;
         synchronized (this) {
-            final boolean allowed = isGetTasksAllowed("getRecentTasks", Binder.getCallingPid(),
+            return mRecentTasks.getRecentTasks(maxNum, flags, allowed, detailed, userId,
                     callingUid);
-            final boolean detailed = checkCallingPermission(
-                    android.Manifest.permission.GET_DETAILED_TASKS)
-                    == PackageManager.PERMISSION_GRANTED;
-
-            if (!isUserRunning(userId, ActivityManager.FLAG_AND_UNLOCKED)) {
-                Slog.i(TAG, "user " + userId + " is still locked. Cannot load recents");
-                return ParceledListSlice.emptyList();
-            }
-            mRecentTasks.loadUserRecentsLocked(userId);
-
-            final int recentsCount = mRecentTasks.size();
-            ArrayList<ActivityManager.RecentTaskInfo> res =
-                    new ArrayList<>(maxNum < recentsCount ? maxNum : recentsCount);
-
-            final Set<Integer> includedUsers;
-            if (includeProfiles) {
-                includedUsers = mUserController.getProfileIds(userId);
-            } else {
-                includedUsers = new HashSet<>();
-            }
-            includedUsers.add(Integer.valueOf(userId));
-
-            for (int i = 0; i < recentsCount && maxNum > 0; i++) {
-                TaskRecord tr = mRecentTasks.get(i);
-                // Only add calling user or related users recent tasks
-                if (!includedUsers.contains(Integer.valueOf(tr.userId))) {
-                    if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "Skipping, not user: " + tr);
-                    continue;
-                }
-
-                if (tr.realActivitySuspended) {
-                    if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "Skipping, activity suspended: " + tr);
-                    continue;
-                }
-
-                // Return the entry if desired by the caller.  We always return
-                // the first entry, because callers always expect this to be the
-                // foreground app.  We may filter others if the caller has
-                // not supplied RECENT_WITH_EXCLUDED and there is some reason
-                // we should exclude the entry.
-
-                if (i == 0
-                        || withExcluded
-                        || (tr.intent == null)
-                        || ((tr.intent.getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
-                                == 0)) {
-                    if (!allowed) {
-                        // If the caller doesn't have the GET_TASKS permission, then only
-                        // allow them to see a small subset of tasks -- their own and home.
-                        if (!tr.isActivityTypeHome() && tr.effectiveUid != callingUid) {
-                            if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "Skipping, not allowed: " + tr);
-                            continue;
-                        }
-                    }
-                    final ActivityStack stack = tr.getStack();
-                    if ((flags & ActivityManager.RECENT_IGNORE_HOME_AND_RECENTS_STACK_TASKS) != 0) {
-                        if (stack != null && stack.isHomeOrRecentsStack()) {
-                            if (DEBUG_RECENTS) Slog.d(TAG_RECENTS,
-                                    "Skipping, home or recents stack task: " + tr);
-                            continue;
-                        }
-                    }
-                    if ((flags & ActivityManager.RECENT_INGORE_DOCKED_STACK_TOP_TASK) != 0) {
-                        if (stack != null && stack.inSplitScreenPrimaryWindowingMode()
-                                && stack.topTask() == tr) {
-                            if (DEBUG_RECENTS) Slog.d(TAG_RECENTS,
-                                    "Skipping, top task in docked stack: " + tr);
-                            continue;
-                        }
-                    }
-                    if ((flags & ActivityManager.RECENT_INGORE_PINNED_STACK_TASKS) != 0) {
-                        if (stack != null && stack.inPinnedWindowingMode()) {
-                            if (DEBUG_RECENTS) Slog.d(TAG_RECENTS,
-                                    "Skipping, pinned stack task: " + tr);
-                            continue;
-                        }
-                    }
-                    if (tr.autoRemoveRecents && tr.getTopActivity() == null) {
-                        // Don't include auto remove tasks that are finished or finishing.
-                        if (DEBUG_RECENTS) Slog.d(TAG_RECENTS,
-                                "Skipping, auto-remove without activity: " + tr);
-                        continue;
-                    }
-                    if ((flags&ActivityManager.RECENT_IGNORE_UNAVAILABLE) != 0
-                            && !tr.isAvailable) {
-                        if (DEBUG_RECENTS) Slog.d(TAG_RECENTS,
-                                "Skipping, unavail real act: " + tr);
-                        continue;
-                    }
-
-                    if (!tr.mUserSetupComplete) {
-                        // Don't include task launched while user is not done setting-up.
-                        if (DEBUG_RECENTS) Slog.d(TAG_RECENTS,
-                                "Skipping, user setup not complete: " + tr);
-                        continue;
-                    }
-
-                    ActivityManager.RecentTaskInfo rti = createRecentTaskInfoFromTaskRecord(tr);
-                    if (!detailed) {
-                        rti.baseIntent.replaceExtras((Bundle)null);
-                    }
-
-                    res.add(rti);
-                    maxNum--;
-                }
-            }
-            return new ParceledListSlice<>(res);
         }
     }
 
@@ -10117,22 +9927,10 @@
                 TaskRecord task = new TaskRecord(this,
                         mStackSupervisor.getNextTaskIdForUserLocked(r.userId),
                         ainfo, intent, description);
-
-                int trimIdx = mRecentTasks.trimForTaskLocked(task, false);
-                if (trimIdx >= 0) {
-                    // If this would have caused a trim, then we'll abort because that
-                    // means it would be added at the end of the list but then just removed.
+                if (!mRecentTasks.addToBottom(task)) {
                     return INVALID_TASK_ID;
                 }
 
-                final int N = mRecentTasks.size();
-                if (N >= (ActivityManager.getMaxRecentTasksStatic()-1)) {
-                    final TaskRecord tr = mRecentTasks.remove(N - 1);
-                    tr.removedFromRecents();
-                }
-
-                task.inRecents = true;
-                mRecentTasks.add(task);
                 r.getStack().addTask(task, false, "addAppTask");
 
                 // TODO: Send the thumbnail to WM to store it.
@@ -10349,38 +10147,6 @@
         mWindowManager.executeAppTransition();
     }
 
-    private void removeTasksByPackageNameLocked(String packageName, int userId) {
-        // Remove all tasks with activities in the specified package from the list of recent tasks
-        for (int i = mRecentTasks.size() - 1; i >= 0; i--) {
-            TaskRecord tr = mRecentTasks.get(i);
-            if (tr.userId != userId) continue;
-
-            ComponentName cn = tr.intent.getComponent();
-            if (cn != null && cn.getPackageName().equals(packageName)) {
-                // If the package name matches, remove the task.
-                mStackSupervisor.removeTaskByIdLocked(tr.taskId, true, REMOVE_FROM_RECENTS);
-            }
-        }
-    }
-
-    private void cleanupDisabledPackageTasksLocked(String packageName, Set<String> filterByClasses,
-            int userId) {
-
-        for (int i = mRecentTasks.size() - 1; i >= 0; i--) {
-            TaskRecord tr = mRecentTasks.get(i);
-            if (userId != UserHandle.USER_ALL && tr.userId != userId) {
-                continue;
-            }
-
-            ComponentName cn = tr.intent.getComponent();
-            final boolean sameComponent = cn != null && cn.getPackageName().equals(packageName)
-                    && (filterByClasses == null || filterByClasses.contains(cn.getClassName()));
-            if (sameComponent) {
-                mStackSupervisor.removeTaskByIdLocked(tr.taskId, false, REMOVE_FROM_RECENTS);
-            }
-        }
-    }
-
     @Override
     public void removeStack(int stackId) {
         enforceCallingPermission(Manifest.permission.MANAGE_ACTIVITY_STACKS, "removeStack()");
@@ -15133,7 +14899,9 @@
                 }
             } else if ("recents".equals(cmd) || "r".equals(cmd)) {
                 synchronized (this) {
-                    dumpRecentsLocked(fd, pw, args, opti, true, dumpPackage);
+                    if (mRecentTasks != null) {
+                        mRecentTasks.dump(pw, true /* dumpAll */, dumpPackage);
+                    }
                 }
             } else if ("broadcasts".equals(cmd) || "b".equals(cmd)) {
                 String[] newArgs;
@@ -15354,7 +15122,9 @@
                 if (dumpAll) {
                     pw.println("-------------------------------------------------------------------------------");
                 }
-                dumpRecentsLocked(fd, pw, args, opti, dumpAll, dumpPackage);
+                if (mRecentTasks != null) {
+                    mRecentTasks.dump(pw, dumpAll, dumpPackage);
+                }
                 pw.println();
                 if (dumpAll) {
                     pw.println("-------------------------------------------------------------------------------");
@@ -15424,7 +15194,9 @@
                 if (dumpAll) {
                     pw.println("-------------------------------------------------------------------------------");
                 }
-                dumpRecentsLocked(fd, pw, args, opti, dumpAll, dumpPackage);
+                if (mRecentTasks != null) {
+                    mRecentTasks.dump(pw, dumpAll, dumpPackage);
+                }
                 pw.println();
                 if (dumpAll) {
                     pw.println("-------------------------------------------------------------------------------");
@@ -15510,42 +15282,6 @@
         }
     }
 
-    void dumpRecentsLocked(FileDescriptor fd, PrintWriter pw, String[] args,
-            int opti, boolean dumpAll, String dumpPackage) {
-        pw.println("ACTIVITY MANAGER RECENT TASKS (dumpsys activity recents)");
-
-        boolean printedAnything = false;
-
-        if (mRecentTasks != null && mRecentTasks.size() > 0) {
-            boolean printedHeader = false;
-
-            final int N = mRecentTasks.size();
-            for (int i=0; i<N; i++) {
-                TaskRecord tr = mRecentTasks.get(i);
-                if (dumpPackage != null) {
-                    if (tr.realActivity == null ||
-                            !dumpPackage.equals(tr.realActivity.getPackageName())) {
-                        continue;
-                    }
-                }
-                if (!printedHeader) {
-                    pw.println("  Recent tasks:");
-                    printedHeader = true;
-                    printedAnything = true;
-                }
-                pw.print("  * Recent #"); pw.print(i); pw.print(": ");
-                        pw.println(tr);
-                if (dumpAll) {
-                    mRecentTasks.get(i).dump(pw, "    ");
-                }
-            }
-        }
-
-        if (!printedAnything) {
-            pw.println("  (nothing)");
-        }
-    }
-
     void dumpAssociationsLocked(FileDescriptor fd, PrintWriter pw, String[] args,
             int opti, boolean dumpAll, boolean dumpClient, String dumpPackage) {
         pw.println("ACTIVITY MANAGER ASSOCIATIONS (dumpsys activity associations)");
@@ -19360,7 +19096,7 @@
                                         // Remove all permissions granted from/to this package
                                         removeUriPermissionsForPackageLocked(ssp, userId, true);
 
-                                        removeTasksByPackageNameLocked(ssp, userId);
+                                        mRecentTasks.removeTasksByPackageName(ssp, userId);
 
                                         mServices.forceStopPackageLocked(ssp, userId);
 
@@ -24368,125 +24104,6 @@
     }
 
     /**
-     * An implementation of IAppTask, that allows an app to manage its own tasks via
-     * {@link android.app.ActivityManager.AppTask}.  We keep track of the callingUid to ensure that
-     * only the process that calls getAppTasks() can call the AppTask methods.
-     */
-    class AppTaskImpl extends IAppTask.Stub {
-        private int mTaskId;
-        private int mCallingUid;
-
-        public AppTaskImpl(int taskId, int callingUid) {
-            mTaskId = taskId;
-            mCallingUid = callingUid;
-        }
-
-        private void checkCaller() {
-            if (mCallingUid != Binder.getCallingUid()) {
-                throw new SecurityException("Caller " + mCallingUid
-                        + " does not match caller of getAppTasks(): " + Binder.getCallingUid());
-            }
-        }
-
-        @Override
-        public void finishAndRemoveTask() {
-            checkCaller();
-
-            synchronized (ActivityManagerService.this) {
-                long origId = Binder.clearCallingIdentity();
-                try {
-                    // We remove the task from recents to preserve backwards
-                    if (!mStackSupervisor.removeTaskByIdLocked(mTaskId, false,
-                            REMOVE_FROM_RECENTS)) {
-                        throw new IllegalArgumentException("Unable to find task ID " + mTaskId);
-                    }
-                } finally {
-                    Binder.restoreCallingIdentity(origId);
-                }
-            }
-        }
-
-        @Override
-        public ActivityManager.RecentTaskInfo getTaskInfo() {
-            checkCaller();
-
-            synchronized (ActivityManagerService.this) {
-                long origId = Binder.clearCallingIdentity();
-                try {
-                    TaskRecord tr = mStackSupervisor.anyTaskForIdLocked(mTaskId);
-                    if (tr == null) {
-                        throw new IllegalArgumentException("Unable to find task ID " + mTaskId);
-                    }
-                    return createRecentTaskInfoFromTaskRecord(tr);
-                } finally {
-                    Binder.restoreCallingIdentity(origId);
-                }
-            }
-        }
-
-        @Override
-        public void moveToFront() {
-            checkCaller();
-            // Will bring task to front if it already has a root activity.
-            final long origId = Binder.clearCallingIdentity();
-            try {
-                synchronized (this) {
-                    mStackSupervisor.startActivityFromRecentsInner(mTaskId, null);
-                }
-            } finally {
-                Binder.restoreCallingIdentity(origId);
-            }
-        }
-
-        @Override
-        public int startActivity(IBinder whoThread, String callingPackage,
-                Intent intent, String resolvedType, Bundle bOptions) {
-            checkCaller();
-
-            int callingUser = UserHandle.getCallingUserId();
-            TaskRecord tr;
-            IApplicationThread appThread;
-            synchronized (ActivityManagerService.this) {
-                tr = mStackSupervisor.anyTaskForIdLocked(mTaskId);
-                if (tr == null) {
-                    throw new IllegalArgumentException("Unable to find task ID " + mTaskId);
-                }
-                appThread = IApplicationThread.Stub.asInterface(whoThread);
-                if (appThread == null) {
-                    throw new IllegalArgumentException("Bad app thread " + appThread);
-                }
-            }
-            return mActivityStarter.startActivityMayWait(appThread, -1, callingPackage, intent,
-                    resolvedType, null, null, null, null, 0, 0, null, null,
-                    null, bOptions, false, callingUser, tr, "AppTaskImpl");
-        }
-
-        @Override
-        public void setExcludeFromRecents(boolean exclude) {
-            checkCaller();
-
-            synchronized (ActivityManagerService.this) {
-                long origId = Binder.clearCallingIdentity();
-                try {
-                    TaskRecord tr = mStackSupervisor.anyTaskForIdLocked(mTaskId);
-                    if (tr == null) {
-                        throw new IllegalArgumentException("Unable to find task ID " + mTaskId);
-                    }
-                    Intent intent = tr.getBaseIntent();
-                    if (exclude) {
-                        intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
-                    } else {
-                        intent.setFlags(intent.getFlags()
-                                & ~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
-                    }
-                } finally {
-                    Binder.restoreCallingIdentity(origId);
-                }
-            }
-        }
-    }
-
-    /**
      * Kill processes for the user with id userId and that depend on the package named packageName
      */
     @Override
diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java
index 7075e67..63eca3f 100644
--- a/services/core/java/com/android/server/am/ActivityStack.java
+++ b/services/core/java/com/android/server/am/ActivityStack.java
@@ -16,7 +16,6 @@
 
 package com.android.server.am;
 
-import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
@@ -34,8 +33,8 @@
 import static android.content.pm.ActivityInfo.FLAG_SHOW_FOR_ALL_USERS;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.FLAG_CAN_SHOW_WITH_INSECURE_KEYGUARD;
-
 import static android.view.Display.INVALID_DISPLAY;
+
 import static com.android.server.am.ActivityDisplay.POSITION_BOTTOM;
 import static com.android.server.am.ActivityDisplay.POSITION_TOP;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_ADD_REMOVE;
@@ -99,12 +98,13 @@
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.ActivityManager.RunningTaskInfo;
-import android.app.ActivityManager.StackId;
 import android.app.ActivityOptions;
 import android.app.AppGlobals;
 import android.app.IActivityController;
 import android.app.ResultInfo;
 import android.app.WindowConfiguration;
+import android.app.WindowConfiguration.ActivityType;
+import android.app.WindowConfiguration.WindowingMode;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
@@ -2169,7 +2169,7 @@
         mResumedActivity = r;
         r.state = ActivityState.RESUMED;
         mService.setResumedActivityUncheckLocked(r, reason);
-        mStackSupervisor.addRecentActivity(r);
+        mStackSupervisor.mRecentTasks.add(r.getTask());
     }
 
     private boolean resumeTopActivityInnerLocked(ActivityRecord prev, ActivityOptions options) {
@@ -4467,7 +4467,9 @@
         // Don't refocus if invisible to current user
         final ActivityRecord top = tr.getTopActivity();
         if (top == null || !top.okToShowLocked()) {
-            mStackSupervisor.addRecentActivity(top);
+            if (top != null) {
+                mStackSupervisor.mRecentTasks.add(top.getTask());
+            }
             ActivityOptions.abort(options);
             return;
         }
@@ -5072,7 +5074,7 @@
             if (task.autoRemoveFromRecents() || isVoiceSession) {
                 // Task creator asked to remove this when done, or this task was a voice
                 // interaction, so it should not remain on the recent tasks list.
-                mStackSupervisor.removeTaskFromRecents(task);
+                mStackSupervisor.mRecentTasks.remove(task);
             }
 
             task.removeWindowContainer();
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index c5cb5bb..543d492 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -46,6 +46,7 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 import static android.view.Display.TYPE_VIRTUAL;
+
 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_PICTURE_IN_PICTURE_EXPANDED_TO_FULLSCREEN;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_ALL;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_FOCUS;
@@ -85,12 +86,13 @@
 import static com.android.server.am.TaskRecord.REPARENT_KEEP_STACK_AT_FRONT;
 import static com.android.server.am.TaskRecord.REPARENT_LEAVE_STACK_IN_PLACE;
 import static com.android.server.am.TaskRecord.REPARENT_MOVE_STACK_TO_FRONT;
+import static com.android.server.am.proto.ActivityStackSupervisorProto.CONFIGURATION_CONTAINER;
 import static com.android.server.am.proto.ActivityStackSupervisorProto.DISPLAYS;
 import static com.android.server.am.proto.ActivityStackSupervisorProto.FOCUSED_STACK_ID;
 import static com.android.server.am.proto.ActivityStackSupervisorProto.KEYGUARD_CONTROLLER;
 import static com.android.server.am.proto.ActivityStackSupervisorProto.RESUMED_ACTIVITY;
-import static com.android.server.am.proto.ActivityStackSupervisorProto.CONFIGURATION_CONTAINER;
 import static com.android.server.wm.AppTransition.TRANSIT_DOCK_TASK_FROM_RECENTS;
+
 import static java.lang.Integer.MAX_VALUE;
 
 import android.Manifest;
@@ -101,7 +103,6 @@
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.ActivityManager.RunningTaskInfo;
-import android.app.ActivityManager.StackId;
 import android.app.ActivityManager.StackInfo;
 import android.app.ActivityManagerInternal.SleepToken;
 import android.app.ActivityOptions;
@@ -175,7 +176,8 @@
 import java.util.List;
 import java.util.Set;
 
-public class ActivityStackSupervisor extends ConfigurationContainer implements DisplayListener {
+public class ActivityStackSupervisor extends ConfigurationContainer implements DisplayListener,
+        RecentTasks.Callbacks {
     private static final String TAG = TAG_WITH_CLASS_NAME ? "ActivityStackSupervisor" : TAG_AM;
     private static final String TAG_FOCUS = TAG + POSTFIX_FOCUS;
     private static final String TAG_IDLE = TAG + POSTFIX_IDLE;
@@ -285,7 +287,7 @@
 
     final ActivityManagerService mService;
 
-    private RecentTasks mRecentTasks;
+    RecentTasks mRecentTasks;
 
     final ActivityStackSupervisorHandler mHandler;
 
@@ -577,6 +579,7 @@
 
     void setRecentTasks(RecentTasks recentTasks) {
         mRecentTasks = recentTasks;
+        mRecentTasks.registerCallback(this);
     }
 
     /**
@@ -750,7 +753,7 @@
         // Otherwise, check the recent tasks and return if we find it there and we are not restoring
         // the task from recents
         if (DEBUG_RECENTS) Slog.v(TAG_RECENTS, "Looking for task id=" + id + " in recents");
-        final TaskRecord task = mRecentTasks.taskForIdLocked(id);
+        final TaskRecord task = mRecentTasks.getTask(id);
 
         if (task == null) {
             if (DEBUG_RECENTS) {
@@ -859,7 +862,7 @@
         // [u*MAX_TASK_IDS_PER_USER, (u+1)*MAX_TASK_IDS_PER_USER-1], so if MAX_TASK_IDS_PER_USER
         // was 10, user 0 could only have taskIds 0 to 9, user 1: 10 to 19, user 2: 20 to 29, so on.
         int candidateTaskId = nextTaskIdForUser(currentTaskId, userId);
-        while (mRecentTasks.taskIdTakenForUserLocked(candidateTaskId, userId)
+        while (mRecentTasks.containsTaskId(candidateTaskId, userId)
                 || anyTaskForIdLocked(
                         candidateTaskId, MATCH_TASK_IN_STACKS_OR_RECENT_TASKS) != null) {
             candidateTaskId = nextTaskIdForUser(candidateTaskId, userId);
@@ -2893,23 +2896,9 @@
         return false;
     }
 
-    void addRecentActivity(ActivityRecord r) {
-        if (r == null) {
-            return;
-        }
-        final TaskRecord task = r.getTask();
-        mRecentTasks.addLocked(task);
-        task.touchActiveTime();
-    }
-
-    void removeTaskFromRecents(TaskRecord task) {
-        mRecentTasks.remove(task);
-        task.removedFromRecents();
-    }
-
     void cleanUpRemovedTaskLocked(TaskRecord tr, boolean killProcess, boolean removeFromRecents) {
         if (removeFromRecents) {
-            removeTaskFromRecents(tr);
+            mRecentTasks.remove(tr);
         }
         ComponentName component = tr.getBaseIntent().getComponent();
         if (component == null) {
@@ -2989,7 +2978,8 @@
     }
 
     /**
-     * Restores a recent task to a stack
+     * Called to restore the state of the task into the stack that it's supposed to go into.
+     *
      * @param task The recent task to be restored.
      * @param aOptions The activity options to use for restoration.
      * @return true if the task has been restored successfully.
@@ -3020,6 +3010,16 @@
         return true;
     }
 
+    @Override
+    public void onRecentTaskAdded(TaskRecord task) {
+        task.touchActiveTime();
+    }
+
+    @Override
+    public void onRecentTaskRemoved(TaskRecord task) {
+        task.removedFromRecents();
+    }
+
     /**
      * Move stack with all its existing content to specified display.
      * @param stackId Id of stack to move.
@@ -3500,7 +3500,7 @@
         final ActivityStack stack = task.getStack();
 
         r.mLaunchTaskBehind = false;
-        mRecentTasks.addLocked(task);
+        mRecentTasks.add(task);
         mService.mTaskChangeNotificationController.notifyTaskStackChanged();
         r.setVisibility(false);
 
diff --git a/services/core/java/com/android/server/am/ActivityStarter.java b/services/core/java/com/android/server/am/ActivityStarter.java
index 8300083..cceb576 100644
--- a/services/core/java/com/android/server/am/ActivityStarter.java
+++ b/services/core/java/com/android/server/am/ActivityStarter.java
@@ -1239,8 +1239,8 @@
                 mSupervisor.resumeFocusedStackTopActivityLocked(mTargetStack, mStartActivity,
                         mOptions);
             }
-        } else {
-            mSupervisor.addRecentActivity(mStartActivity);
+        } else if (mStartActivity != null) {
+            mSupervisor.mRecentTasks.add(mStartActivity.getTask());
         }
         mSupervisor.updateUserStackLocked(mStartActivity.userId, mTargetStack);
 
diff --git a/services/core/java/com/android/server/am/AppTaskImpl.java b/services/core/java/com/android/server/am/AppTaskImpl.java
new file mode 100644
index 0000000..a4e2e70
--- /dev/null
+++ b/services/core/java/com/android/server/am/AppTaskImpl.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.am;
+
+import static com.android.server.am.ActivityStackSupervisor.REMOVE_FROM_RECENTS;
+
+import android.app.ActivityManager;
+import android.app.IAppTask;
+import android.app.IApplicationThread;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.UserHandle;
+
+/**
+ * An implementation of IAppTask, that allows an app to manage its own tasks via
+ * {@link android.app.ActivityManager.AppTask}.  We keep track of the callingUid to ensure that
+ * only the process that calls getAppTasks() can call the AppTask methods.
+ */
+class AppTaskImpl extends IAppTask.Stub {
+    private ActivityManagerService mService;
+
+    private int mTaskId;
+    private int mCallingUid;
+
+    public AppTaskImpl(ActivityManagerService service, int taskId, int callingUid) {
+        mService = service;
+        mTaskId = taskId;
+        mCallingUid = callingUid;
+    }
+
+    private void checkCaller() {
+        if (mCallingUid != Binder.getCallingUid()) {
+            throw new SecurityException("Caller " + mCallingUid
+                    + " does not match caller of getAppTasks(): " + Binder.getCallingUid());
+        }
+    }
+
+    @Override
+    public void finishAndRemoveTask() {
+        checkCaller();
+
+        synchronized (mService) {
+            long origId = Binder.clearCallingIdentity();
+            try {
+                // We remove the task from recents to preserve backwards
+                if (!mService.mStackSupervisor.removeTaskByIdLocked(mTaskId, false,
+                        REMOVE_FROM_RECENTS)) {
+                    throw new IllegalArgumentException("Unable to find task ID " + mTaskId);
+                }
+            } finally {
+                Binder.restoreCallingIdentity(origId);
+            }
+        }
+    }
+
+    @Override
+    public ActivityManager.RecentTaskInfo getTaskInfo() {
+        checkCaller();
+
+        synchronized (mService) {
+            long origId = Binder.clearCallingIdentity();
+            try {
+                TaskRecord tr = mService.mStackSupervisor.anyTaskForIdLocked(mTaskId);
+                if (tr == null) {
+                    throw new IllegalArgumentException("Unable to find task ID " + mTaskId);
+                }
+                return RecentTasks.createRecentTaskInfo(tr);
+            } finally {
+                Binder.restoreCallingIdentity(origId);
+            }
+        }
+    }
+
+    @Override
+    public void moveToFront() {
+        checkCaller();
+        // Will bring task to front if it already has a root activity.
+        final long origId = Binder.clearCallingIdentity();
+        try {
+            synchronized (this) {
+                mService.mStackSupervisor.startActivityFromRecentsInner(mTaskId, null);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(origId);
+        }
+    }
+
+    @Override
+    public int startActivity(IBinder whoThread, String callingPackage,
+            Intent intent, String resolvedType, Bundle bOptions) {
+        checkCaller();
+
+        int callingUser = UserHandle.getCallingUserId();
+        TaskRecord tr;
+        IApplicationThread appThread;
+        synchronized (mService) {
+            tr = mService.mStackSupervisor.anyTaskForIdLocked(mTaskId);
+            if (tr == null) {
+                throw new IllegalArgumentException("Unable to find task ID " + mTaskId);
+            }
+            appThread = IApplicationThread.Stub.asInterface(whoThread);
+            if (appThread == null) {
+                throw new IllegalArgumentException("Bad app thread " + appThread);
+            }
+        }
+        return mService.mActivityStarter.startActivityMayWait(appThread, -1, callingPackage,
+                intent, resolvedType, null, null, null, null, 0, 0, null, null,
+                null, bOptions, false, callingUser, tr, "AppTaskImpl");
+    }
+
+    @Override
+    public void setExcludeFromRecents(boolean exclude) {
+        checkCaller();
+
+        synchronized (mService) {
+            long origId = Binder.clearCallingIdentity();
+            try {
+                TaskRecord tr = mService.mStackSupervisor.anyTaskForIdLocked(mTaskId);
+                if (tr == null) {
+                    throw new IllegalArgumentException("Unable to find task ID " + mTaskId);
+                }
+                Intent intent = tr.getBaseIntent();
+                if (exclude) {
+                    intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+                } else {
+                    intent.setFlags(intent.getFlags()
+                            & ~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+                }
+            } finally {
+                Binder.restoreCallingIdentity(origId);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/am/RecentTasks.java b/services/core/java/com/android/server/am/RecentTasks.java
index 365c5b1..9091786 100644
--- a/services/core/java/com/android/server/am/RecentTasks.java
+++ b/services/core/java/com/android/server/am/RecentTasks.java
@@ -16,15 +16,24 @@
 
 package com.android.server.am;
 
+import static android.app.ActivityManager.FLAG_AND_UNLOCKED;
+import static android.app.ActivityManager.RECENT_IGNORE_HOME_AND_RECENTS_STACK_TASKS;
+import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE;
+import static android.app.ActivityManager.RECENT_INCLUDE_PROFILES;
+import static android.app.ActivityManager.RECENT_INGORE_DOCKED_STACK_TOP_TASK;
+import static android.app.ActivityManager.RECENT_INGORE_PINNED_STACK_TASKS;
+import static android.app.ActivityManager.RECENT_WITH_EXCLUDED;
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_RECENTS;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_TASKS;
 import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_RECENTS;
 import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_TASKS;
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.server.am.ActivityStackSupervisor.REMOVE_FROM_RECENTS;
 import static com.android.server.am.TaskRecord.INVALID_TASK_ID;
 
 import com.google.android.collect.Sets;
@@ -37,43 +46,83 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
 import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.os.Binder;
+import android.os.Bundle;
 import android.os.Environment;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.util.ArraySet;
+import android.util.MutableBoolean;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.am.ActivityStack.ActivityState;
+
 import java.io.File;
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Set;
+import java.util.function.BiConsumer;
 
 /**
- * Class for managing the recent tasks list.
+ * Class for managing the recent tasks list. The list is ordered by most recent (index 0) to the
+ * least recent.
  */
-class RecentTasks extends ArrayList<TaskRecord> {
+class RecentTasks {
     private static final String TAG = TAG_WITH_CLASS_NAME ? "RecentTasks" : TAG_AM;
     private static final String TAG_RECENTS = TAG + POSTFIX_RECENTS;
     private static final String TAG_TASKS = TAG + POSTFIX_TASKS;
+    private static final boolean TRIMMED = true;
 
-    // Maximum number recent bitmaps to keep in memory.
-    private static final int MAX_RECENT_BITMAPS = 3;
     private static final int DEFAULT_INITIAL_CAPACITY = 5;
 
     // Whether or not to move all affiliated tasks to the front when one of the tasks is launched
     private static final boolean MOVE_AFFILIATED_TASKS_TO_FRONT = false;
 
+    // Comparator to sort by taskId
+    private static final Comparator<TaskRecord> TASK_ID_COMPARATOR =
+            (lhs, rhs) -> rhs.taskId - lhs.taskId;
+
+    // Placeholder variables to keep track of activities/apps that are no longer avialble while
+    // iterating through the recents list
+    private static final ActivityInfo NO_ACTIVITY_INFO_TOKEN = new ActivityInfo();
+    private static final ApplicationInfo NO_APPLICATION_INFO_TOKEN = new ApplicationInfo();
+
+    /**
+     * Callbacks made when manipulating the list.
+     */
+    interface Callbacks {
+        /**
+         * Called when a task is added to the recent tasks list.
+         */
+        void onRecentTaskAdded(TaskRecord task);
+
+        /**
+         * Called when a task is removed from the recent tasks list.
+         */
+        void onRecentTaskRemoved(TaskRecord task);
+    }
+
     /**
      * Save recent tasks information across reboots.
      */
     private final TaskPersister mTaskPersister;
     private final ActivityManagerService mService;
+
+    /**
+     * Mapping of user id -> whether recent tasks have been loaded for that user.
+     */
     private final SparseBooleanArray mUsersWithRecentsLoaded = new SparseBooleanArray(
             DEFAULT_INITIAL_CAPACITY);
 
@@ -81,21 +130,49 @@
      * Stores for each user task ids that are taken by tasks residing in persistent storage. These
      * tasks may or may not currently be in memory.
      */
-    final SparseArray<SparseBooleanArray> mPersistedTaskIds = new SparseArray<>(
+    private final SparseArray<SparseBooleanArray> mPersistedTaskIds = new SparseArray<>(
             DEFAULT_INITIAL_CAPACITY);
 
+    // List of recent tasks
+    private final ArrayList<TaskRecord> mTasks = new ArrayList<>();
+    private final ArrayList<Callbacks> mCallbacks = new ArrayList<>();
+
     // Mainly to avoid object recreation on multiple calls.
-    private final ArrayList<TaskRecord> mTmpRecents = new ArrayList<TaskRecord>();
+    private final ArrayList<TaskRecord> mTmpRecents = new ArrayList<>();
     private final HashMap<ComponentName, ActivityInfo> mTmpAvailActCache = new HashMap<>();
     private final HashMap<String, ApplicationInfo> mTmpAvailAppCache = new HashMap<>();
-    private final ActivityInfo mTmpActivityInfo = new ActivityInfo();
-    private final ApplicationInfo mTmpAppInfo = new ApplicationInfo();
 
-    RecentTasks(ActivityManagerService service, ActivityStackSupervisor mStackSupervisor) {
+    @VisibleForTesting
+    RecentTasks(ActivityManagerService service, TaskPersister taskPersister) {
+        mService = service;
+        mTaskPersister = taskPersister;
+    }
+
+    RecentTasks(ActivityManagerService service, ActivityStackSupervisor stackSupervisor) {
         File systemDir = Environment.getDataSystemDirectory();
         mService = service;
-        mTaskPersister = new TaskPersister(systemDir, mStackSupervisor, service, this);
-        mStackSupervisor.setRecentTasks(this);
+        mTaskPersister = new TaskPersister(systemDir, stackSupervisor, service, this);
+        stackSupervisor.setRecentTasks(this);
+    }
+
+    void registerCallback(Callbacks callback) {
+        mCallbacks.add(callback);
+    }
+
+    void unregisterCallback(Callbacks callback) {
+        mCallbacks.remove(callback);
+    }
+
+    private void notifyTaskAdded(TaskRecord task) {
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            mCallbacks.get(i).onRecentTaskAdded(task);
+        }
+    }
+
+    private void notifyTaskRemoved(TaskRecord task, boolean wasTrimmed) {
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            mCallbacks.get(i).onRecentTaskRemoved(task);
+        }
     }
 
     /**
@@ -106,6 +183,7 @@
      */
     void loadUserRecentsLocked(int userId) {
         if (mUsersWithRecentsLoaded.get(userId)) {
+            // User already loaded, return early
             return;
         }
 
@@ -114,14 +192,14 @@
 
         // Check if any tasks are added before recents is loaded
         final SparseBooleanArray preaddedTasks = new SparseBooleanArray();
-        for (final TaskRecord task : this) {
+        for (final TaskRecord task : mTasks) {
             if (task.userId == userId && shouldPersistTaskLocked(task)) {
                 preaddedTasks.put(task.taskId, true);
             }
         }
 
         Slog.i(TAG, "Loading recents for user " + userId + " into memory.");
-        addAll(mTaskPersister.restoreTasksForUserLocked(userId, preaddedTasks));
+        mTasks.addAll(mTaskPersister.restoreTasksForUserLocked(userId, preaddedTasks));
         cleanupLocked(userId);
         mUsersWithRecentsLoaded.put(userId, true);
 
@@ -140,11 +218,25 @@
         }
     }
 
-    boolean taskIdTakenForUserLocked(int taskId, int userId) {
+    /**
+     * @return whether the {@param taskId} is currently in use for the given user.
+     */
+    boolean containsTaskId(int taskId, int userId) {
         loadPersistedTaskIdsForUserLocked(userId);
         return mPersistedTaskIds.get(userId).get(taskId);
     }
 
+    /**
+     * @return all the task ids for the user with the given {@param userId}.
+     */
+    SparseBooleanArray getTaskIdsForUser(int userId) {
+        loadPersistedTaskIdsForUserLocked(userId);
+        return mPersistedTaskIds.get(userId);
+    }
+
+    /**
+     * Kicks off the task persister to write any pending tasks to disk.
+     */
     void notifyTaskPersisterLocked(TaskRecord task, boolean flush) {
         final ActivityStack stack = task != null ? task.getStack() : null;
         if (stack != null && stack.isHomeOrRecentsStack()) {
@@ -164,8 +256,8 @@
                 mPersistedTaskIds.valueAt(i).clear();
             }
         }
-        for (int i = size() - 1; i >= 0; i--) {
-            final TaskRecord task = get(i);
+        for (int i = mTasks.size() - 1; i >= 0; i--) {
+            final TaskRecord task = mTasks.get(i);
             if (shouldPersistTaskLocked(task)) {
                 // Set of persisted taskIds for task.userId should not be null here
                 // TODO Investigate why it can happen. For now initialize with an empty set
@@ -180,12 +272,12 @@
     }
 
     private static boolean shouldPersistTaskLocked(TaskRecord task) {
-        final ActivityStack<?> stack = task.getStack();
+        final ActivityStack stack = task.getStack();
         return task.isPersistable && (stack == null || !stack.isHomeOrRecentsStack());
     }
 
     void onSystemReadyLocked() {
-        clear();
+        mTasks.clear();
         mTaskPersister.startPersisting();
     }
 
@@ -225,14 +317,6 @@
         return usersWithRecentsLoaded;
     }
 
-    private void unloadUserRecentsLocked(int userId) {
-        if (mUsersWithRecentsLoaded.get(userId)) {
-            Slog.i(TAG, "Unloading recents for user " + userId + " from memory.");
-            mUsersWithRecentsLoaded.delete(userId);
-            removeTasksForUserLocked(userId);
-        }
-    }
-
     /**
      * Removes recent tasks and any other state kept in memory for the passed in user. Does not
      * touch the information present on persistent storage.
@@ -240,44 +324,36 @@
      * @param userId the id of the user
      */
     void unloadUserDataFromMemoryLocked(int userId) {
-        unloadUserRecentsLocked(userId);
+        if (mUsersWithRecentsLoaded.get(userId)) {
+            Slog.i(TAG, "Unloading recents for user " + userId + " from memory.");
+            mUsersWithRecentsLoaded.delete(userId);
+            removeTasksForUserLocked(userId);
+        }
         mPersistedTaskIds.delete(userId);
         mTaskPersister.unloadUserDataFromMemory(userId);
     }
 
-    TaskRecord taskForIdLocked(int id) {
-        final int recentsCount = size();
-        for (int i = 0; i < recentsCount; i++) {
-            TaskRecord tr = get(i);
-            if (tr.taskId == id) {
-                return tr;
-            }
-        }
-        return null;
-    }
-
     /** Remove recent tasks for a user. */
-    void removeTasksForUserLocked(int userId) {
+    private void removeTasksForUserLocked(int userId) {
         if(userId <= 0) {
             Slog.i(TAG, "Can't remove recent task on user " + userId);
             return;
         }
 
-        for (int i = size() - 1; i >= 0; --i) {
-            TaskRecord tr = get(i);
+        for (int i = mTasks.size() - 1; i >= 0; --i) {
+            TaskRecord tr = mTasks.get(i);
             if (tr.userId == userId) {
                 if(DEBUG_TASKS) Slog.i(TAG_TASKS,
                         "remove RecentTask " + tr + " when finishing user" + userId);
-                remove(i);
-                tr.removedFromRecents();
+                remove(mTasks.get(i));
             }
         }
     }
 
     void onPackagesSuspendedChanged(String[] packages, boolean suspended, int userId) {
         final Set<String> packageNames = Sets.newHashSet(packages);
-        for (int i = size() - 1; i >= 0; --i) {
-            final TaskRecord tr = get(i);
+        for (int i = mTasks.size() - 1; i >= 0; --i) {
+            final TaskRecord tr = mTasks.get(i);
             if (tr.realActivity != null
                     && packageNames.contains(tr.realActivity.getPackageName())
                     && tr.userId == userId
@@ -286,7 +362,38 @@
                notifyTaskPersisterLocked(tr, false);
             }
         }
+    }
 
+    void removeTasksByPackageName(String packageName, int userId) {
+        final int size = mTasks.size();
+        for (int i = 0; i < size; i++) {
+            final TaskRecord tr = mTasks.get(i);
+            final String taskPackageName =
+                    tr.getBaseIntent().getComponent().getPackageName();
+            if (tr.userId != userId) return;
+            if (!taskPackageName.equals(packageName)) return;
+
+            mService.mStackSupervisor.removeTaskByIdLocked(tr.taskId, true, REMOVE_FROM_RECENTS);
+        }
+    }
+
+    void cleanupDisabledPackageTasksLocked(String packageName, Set<String> filterByClasses,
+            int userId) {
+        final int size = mTasks.size();
+        for (int i = 0; i < size; i++) {
+            final TaskRecord tr = mTasks.get(i);
+            if (userId != UserHandle.USER_ALL && tr.userId != userId) {
+                continue;
+            }
+
+            ComponentName cn = tr.intent.getComponent();
+            final boolean sameComponent = cn != null && cn.getPackageName().equals(packageName)
+                    && (filterByClasses == null || filterByClasses.contains(cn.getClassName()));
+            if (sameComponent) {
+                mService.mStackSupervisor.removeTaskByIdLocked(tr.taskId, false,
+                        REMOVE_FROM_RECENTS);
+            }
+        }
     }
 
     /**
@@ -295,24 +402,28 @@
      * of affiliations.
      */
     void cleanupLocked(int userId) {
-        int recentsCount = size();
+        int recentsCount = mTasks.size();
         if (recentsCount == 0) {
             // Happens when called from the packagemanager broadcast before boot,
             // or just any empty list.
             return;
         }
 
+        // Clear the temp lists
+        mTmpAvailActCache.clear();
+        mTmpAvailAppCache.clear();
+
         final IPackageManager pm = AppGlobals.getPackageManager();
         for (int i = recentsCount - 1; i >= 0; i--) {
-            final TaskRecord task = get(i);
+            final TaskRecord task = mTasks.get(i);
             if (userId != UserHandle.USER_ALL && task.userId != userId) {
                 // Only look at tasks for the user ID of interest.
                 continue;
             }
             if (task.autoRemoveRecents && task.getTopActivity() == null) {
                 // This situation is broken, and we should just get rid of it now.
-                remove(i);
-                task.removedFromRecents();
+                mTasks.remove(i);
+                notifyTaskRemoved(task, !TRIMMED);
                 Slog.w(TAG, "Removing auto-remove without activity: " + task);
                 continue;
             }
@@ -331,11 +442,11 @@
                         continue;
                     }
                     if (ai == null) {
-                        ai = mTmpActivityInfo;
+                        ai = NO_ACTIVITY_INFO_TOKEN;
                     }
                     mTmpAvailActCache.put(task.realActivity, ai);
                 }
-                if (ai == mTmpActivityInfo) {
+                if (ai == NO_ACTIVITY_INFO_TOKEN) {
                     // This could be either because the activity no longer exists, or the
                     // app is temporarily gone. For the former we want to remove the recents
                     // entry; for the latter we want to mark it as unavailable.
@@ -350,15 +461,15 @@
                             continue;
                         }
                         if (app == null) {
-                            app = mTmpAppInfo;
+                            app = NO_APPLICATION_INFO_TOKEN;
                         }
                         mTmpAvailAppCache.put(task.realActivity.getPackageName(), app);
                     }
-                    if (app == mTmpAppInfo
+                    if (app == NO_APPLICATION_INFO_TOKEN
                             || (app.flags & ApplicationInfo.FLAG_INSTALLED) == 0) {
                         // Doesn't exist any more! Good-bye.
-                        remove(i);
-                        task.removedFromRecents();
+                        mTasks.remove(i);
+                        notifyTaskRemoved(task, !TRIMMED);
                         Slog.w(TAG, "Removing no longer valid recent: " + task);
                         continue;
                     } else {
@@ -390,15 +501,519 @@
 
         // Verify the affiliate chain for each task.
         int i = 0;
-        recentsCount = size();
+        recentsCount = mTasks.size();
         while (i < recentsCount) {
             i = processNextAffiliateChainLocked(i);
         }
         // recent tasks are now in sorted, affiliated order.
     }
 
-    private final boolean moveAffiliatedTasksToFront(TaskRecord task, int taskIndex) {
-        int recentsCount = size();
+    /**
+     * @return whether the given {@param task} can be added to the list without causing another
+     * task to be trimmed as a result of that add.
+     */
+    private boolean canAddTaskWithoutTrim(TaskRecord task) {
+        return findTrimIndexForAddTask(task) == -1;
+    }
+
+    /**
+     * Returns the list of {@link ActivityManager.AppTask}s.
+     */
+    ArrayList<IBinder> getAppTasksList(int callingUid, String callingPackage) {
+        final ArrayList<IBinder> list = new ArrayList<>();
+        final int size = mTasks.size();
+        for (int i = 0; i < size; i++) {
+            final TaskRecord tr = mTasks.get(i);
+            // Skip tasks that do not match the caller.  We don't need to verify
+            // callingPackage, because we are also limiting to callingUid and know
+            // that will limit to the correct security sandbox.
+            if (tr.effectiveUid != callingUid) {
+                continue;
+            }
+            Intent intent = tr.getBaseIntent();
+            if (intent == null || !callingPackage.equals(intent.getComponent().getPackageName())) {
+                continue;
+            }
+            ActivityManager.RecentTaskInfo taskInfo = createRecentTaskInfo(tr);
+            AppTaskImpl taskImpl = new AppTaskImpl(mService, taskInfo.persistentId, callingUid);
+            list.add(taskImpl.asBinder());
+        }
+        return list;
+    }
+
+    /**
+     * @return the list of recent tasks for presentation.
+     */
+    ParceledListSlice<ActivityManager.RecentTaskInfo> getRecentTasks(int maxNum, int flags,
+            boolean getTasksAllowed, boolean getDetailedTasks, int userId, int callingUid) {
+        final boolean includeProfiles = (flags & RECENT_INCLUDE_PROFILES) != 0;
+        final boolean withExcluded = (flags & RECENT_WITH_EXCLUDED) != 0;
+
+        if (!mService.isUserRunning(userId, FLAG_AND_UNLOCKED)) {
+            Slog.i(TAG, "user " + userId + " is still locked. Cannot load recents");
+            return ParceledListSlice.emptyList();
+        }
+        loadUserRecentsLocked(userId);
+
+
+        final Set<Integer> includedUsers;
+        if (includeProfiles) {
+            includedUsers = mService.mUserController.getProfileIds(userId);
+        } else {
+            includedUsers = new HashSet<>();
+        }
+        includedUsers.add(Integer.valueOf(userId));
+
+        final ArrayList<ActivityManager.RecentTaskInfo> res = new ArrayList<>();
+        final int size = mTasks.size();
+        for (int i = 0; i < size; i++) {
+            final TaskRecord tr = mTasks.get(i);
+            // Skip remaining tasks once we reach the requested size
+            if (res.size() >= maxNum) {
+                continue;
+            }
+
+            // Only add calling user or related users recent tasks
+            if (!includedUsers.contains(Integer.valueOf(tr.userId))) {
+                if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "Skipping, not user: " + tr);
+                continue;
+            }
+
+            if (tr.realActivitySuspended) {
+                if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "Skipping, activity suspended: " + tr);
+                continue;
+            }
+
+            // Return the entry if desired by the caller.  We always return
+            // the first entry, because callers always expect this to be the
+            // foreground app.  We may filter others if the caller has
+            // not supplied RECENT_WITH_EXCLUDED and there is some reason
+            // we should exclude the entry.
+
+            if (i == 0
+                    || withExcluded
+                    || (tr.intent == null)
+                    || ((tr.intent.getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
+                    == 0)) {
+                if (!getTasksAllowed) {
+                    // If the caller doesn't have the GET_TASKS permission, then only
+                    // allow them to see a small subset of tasks -- their own and home.
+                    if (!tr.isActivityTypeHome() && tr.effectiveUid != callingUid) {
+                        if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "Skipping, not allowed: " + tr);
+                        continue;
+                    }
+                }
+                final ActivityStack stack = tr.getStack();
+                if ((flags & RECENT_IGNORE_HOME_AND_RECENTS_STACK_TASKS) != 0) {
+                    if (stack != null && stack.isHomeOrRecentsStack()) {
+                        if (DEBUG_RECENTS) Slog.d(TAG_RECENTS,
+                                "Skipping, home or recents stack task: " + tr);
+                        continue;
+                    }
+                }
+                if ((flags & RECENT_INGORE_DOCKED_STACK_TOP_TASK) != 0) {
+                    if (stack != null && stack.inSplitScreenPrimaryWindowingMode()
+                            && stack.topTask() == tr) {
+                        if (DEBUG_RECENTS) Slog.d(TAG_RECENTS,
+                                "Skipping, top task in docked stack: " + tr);
+                        continue;
+                    }
+                }
+                if ((flags & RECENT_INGORE_PINNED_STACK_TASKS) != 0) {
+                    if (stack != null && stack.inPinnedWindowingMode()) {
+                        if (DEBUG_RECENTS) Slog.d(TAG_RECENTS,
+                                "Skipping, pinned stack task: " + tr);
+                        continue;
+                    }
+                }
+                if (tr.autoRemoveRecents && tr.getTopActivity() == null) {
+                    // Don't include auto remove tasks that are finished or finishing.
+                    if (DEBUG_RECENTS) Slog.d(TAG_RECENTS,
+                            "Skipping, auto-remove without activity: " + tr);
+                    continue;
+                }
+                if ((flags & RECENT_IGNORE_UNAVAILABLE) != 0 && !tr.isAvailable) {
+                    if (DEBUG_RECENTS) Slog.d(TAG_RECENTS,
+                            "Skipping, unavail real act: " + tr);
+                    continue;
+                }
+
+                if (!tr.mUserSetupComplete) {
+                    // Don't include task launched while user is not done setting-up.
+                    if (DEBUG_RECENTS) Slog.d(TAG_RECENTS,
+                            "Skipping, user setup not complete: " + tr);
+                    continue;
+                }
+
+                ActivityManager.RecentTaskInfo rti = RecentTasks.createRecentTaskInfo(tr);
+                if (!getDetailedTasks) {
+                    rti.baseIntent.replaceExtras((Bundle)null);
+                }
+
+                res.add(rti);
+            }
+        }
+        return new ParceledListSlice<>(res);
+    }
+
+    /**
+     * @return the list of persistable task ids.
+     */
+    void getPersistableTaskIds(ArraySet<Integer> persistentTaskIds) {
+        final int size = mTasks.size();
+        for (int i = 0; i < size; i++) {
+            final TaskRecord task = mTasks.get(i);
+            if (TaskPersister.DEBUG) Slog.d(TAG, "LazyTaskWriter: task=" + task
+                    + " persistable=" + task.isPersistable);
+            final ActivityStack stack = task.getStack();
+            if ((task.isPersistable || task.inRecents)
+                    && (stack == null || !stack.isHomeOrRecentsStack())) {
+                if (TaskPersister.DEBUG) Slog.d(TAG, "adding to persistentTaskIds task=" + task);
+                persistentTaskIds.add(task.taskId);
+            } else {
+                if (TaskPersister.DEBUG) Slog.d(TAG, "omitting from persistentTaskIds task="
+                        + task);
+            }
+        }
+    }
+
+    /**
+     * @return the task in the task list with the given {@param id} if one exists.
+     */
+    TaskRecord getTask(int id) {
+        final int recentsCount = mTasks.size();
+        for (int i = 0; i < recentsCount; i++) {
+            TaskRecord tr = mTasks.get(i);
+            if (tr.taskId == id) {
+                return tr;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Add a new task to the recent tasks list.
+     */
+    void add(TaskRecord task) {
+        final boolean isAffiliated = task.mAffiliatedTaskId != task.taskId
+                || task.mNextAffiliateTaskId != INVALID_TASK_ID
+                || task.mPrevAffiliateTaskId != INVALID_TASK_ID;
+
+        int recentsCount = mTasks.size();
+        // Quick case: never add voice sessions.
+        // TODO: VI what about if it's just an activity?
+        // Probably nothing to do here
+        if (task.voiceSession != null) {
+            if (DEBUG_RECENTS) Slog.d(TAG_RECENTS,
+                    "addRecent: not adding voice interaction " + task);
+            return;
+        }
+        // Another quick case: check if the top-most recent task is the same.
+        if (!isAffiliated && recentsCount > 0 && mTasks.get(0) == task) {
+            if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "addRecent: already at top: " + task);
+            return;
+        }
+        // Another quick case: check if this is part of a set of affiliated
+        // tasks that are at the top.
+        if (isAffiliated && recentsCount > 0 && task.inRecents
+                && task.mAffiliatedTaskId == mTasks.get(0).mAffiliatedTaskId) {
+            if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "addRecent: affiliated " + mTasks.get(0)
+                    + " at top when adding " + task);
+            return;
+        }
+
+        boolean needAffiliationFix = false;
+
+        // Slightly less quick case: the task is already in recents, so all we need
+        // to do is move it.
+        if (task.inRecents) {
+            int taskIndex = mTasks.indexOf(task);
+            if (taskIndex >= 0) {
+                if (!isAffiliated || !MOVE_AFFILIATED_TASKS_TO_FRONT) {
+                    // Simple case: this is not an affiliated task, so we just move it to the front.
+                    mTasks.remove(taskIndex);
+                    mTasks.add(0, task);
+                    notifyTaskPersisterLocked(task, false);
+                    if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "addRecent: moving to top " + task
+                            + " from " + taskIndex);
+                    return;
+                } else {
+                    // More complicated: need to keep all affiliated tasks together.
+                    if (moveAffiliatedTasksToFront(task, taskIndex)) {
+                        // All went well.
+                        return;
+                    }
+
+                    // Uh oh...  something bad in the affiliation chain, try to rebuild
+                    // everything and then go through our general path of adding a new task.
+                    needAffiliationFix = true;
+                }
+            } else {
+                Slog.wtf(TAG, "Task with inRecent not in recents: " + task);
+                needAffiliationFix = true;
+            }
+        }
+
+        if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "addRecent: trimming tasks for " + task);
+        trimForAddTask(task);
+        trimToMaxNumRecents();
+
+        task.inRecents = true;
+        if (!isAffiliated || needAffiliationFix) {
+            // If this is a simple non-affiliated task, or we had some failure trying to
+            // handle it as part of an affilated task, then just place it at the top.
+            mTasks.add(0, task);
+            notifyTaskAdded(task);
+            if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "addRecent: adding " + task);
+        } else if (isAffiliated) {
+            // If this is a new affiliated task, then move all of the affiliated tasks
+            // to the front and insert this new one.
+            TaskRecord other = task.mNextAffiliate;
+            if (other == null) {
+                other = task.mPrevAffiliate;
+            }
+            if (other != null) {
+                int otherIndex = mTasks.indexOf(other);
+                if (otherIndex >= 0) {
+                    // Insert new task at appropriate location.
+                    int taskIndex;
+                    if (other == task.mNextAffiliate) {
+                        // We found the index of our next affiliation, which is who is
+                        // before us in the list, so add after that point.
+                        taskIndex = otherIndex+1;
+                    } else {
+                        // We found the index of our previous affiliation, which is who is
+                        // after us in the list, so add at their position.
+                        taskIndex = otherIndex;
+                    }
+                    if (DEBUG_RECENTS) Slog.d(TAG_RECENTS,
+                            "addRecent: new affiliated task added at " + taskIndex + ": " + task);
+                    mTasks.add(taskIndex, task);
+                    notifyTaskAdded(task);
+
+                    // Now move everything to the front.
+                    if (moveAffiliatedTasksToFront(task, taskIndex)) {
+                        // All went well.
+                        return;
+                    }
+
+                    // Uh oh...  something bad in the affiliation chain, try to rebuild
+                    // everything and then go through our general path of adding a new task.
+                    needAffiliationFix = true;
+                } else {
+                    if (DEBUG_RECENTS) Slog.d(TAG_RECENTS,
+                            "addRecent: couldn't find other affiliation " + other);
+                    needAffiliationFix = true;
+                }
+            } else {
+                if (DEBUG_RECENTS) Slog.d(TAG_RECENTS,
+                        "addRecent: adding affiliated task without next/prev:" + task);
+                needAffiliationFix = true;
+            }
+        }
+
+        if (needAffiliationFix) {
+            if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "addRecent: regrouping affiliations");
+            cleanupLocked(task.userId);
+        }
+    }
+
+    /**
+     * Add the task to the bottom if possible.
+     */
+    boolean addToBottom(TaskRecord task) {
+        if (!canAddTaskWithoutTrim(task)) {
+            // Adding this task would cause the task to be removed (since it's appended at
+            // the bottom and would be trimmed) so just return now
+            return false;
+        }
+
+        add(task);
+        return true;
+    }
+
+    /**
+     * Remove a task from the recent tasks list.
+     */
+    void remove(TaskRecord task) {
+        mTasks.remove(task);
+        notifyTaskRemoved(task, !TRIMMED);
+    }
+
+    /**
+     * Trims the recents task list to the global max number of recents.
+     */
+    private void trimToMaxNumRecents() {
+        int recentsCount = mTasks.size();
+        final int maxNumRecents = ActivityManager.getMaxRecentTasksStatic();
+        while (recentsCount >= maxNumRecents) {
+            final TaskRecord tr = mTasks.remove(recentsCount - 1);
+            notifyTaskRemoved(tr, !TRIMMED);
+            recentsCount--;
+        }
+    }
+
+    /**
+     * If needed, remove oldest existing entries in recents that are for the same kind
+     * of task as the given one.
+     */
+    private void trimForAddTask(TaskRecord task) {
+        final int removeIndex = findTrimIndexForAddTask(task);
+        if (removeIndex == -1) {
+            // Nothing to trim
+            return;
+        }
+
+        final TaskRecord removedTask = mTasks.remove(removeIndex);
+        if (removedTask != task) {
+            notifyTaskRemoved(removedTask, TRIMMED);
+        }
+        notifyTaskPersisterLocked(removedTask, false);
+    }
+
+    /**
+     * Find the task that would be removed if the given {@param task} is added to the recent tasks
+     * list (if any).
+     */
+    private int findTrimIndexForAddTask(TaskRecord task) {
+        int recentsCount = mTasks.size();
+        final Intent intent = task.intent;
+        final boolean document = intent != null && intent.isDocument();
+        int maxRecents = task.maxRecents - 1;
+        final ActivityStack stack = task.getStack();
+        for (int i = 0; i < recentsCount; i++) {
+            final TaskRecord tr = mTasks.get(i);
+            final ActivityStack trStack = tr.getStack();
+
+            if (task != tr) {
+                if (stack != null && trStack != null && stack != trStack) {
+                    continue;
+                }
+                if (task.userId != tr.userId) {
+                    continue;
+                }
+                final Intent trIntent = tr.intent;
+                final boolean sameAffinity =
+                        task.affinity != null && task.affinity.equals(tr.affinity);
+                final boolean sameIntent = intent != null && intent.filterEquals(trIntent);
+                boolean multiTasksAllowed = false;
+                final int flags = intent.getFlags();
+                if ((flags & (FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_NEW_DOCUMENT)) != 0
+                        && (flags & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
+                    multiTasksAllowed = true;
+                }
+                final boolean trIsDocument = trIntent != null && trIntent.isDocument();
+                final boolean bothDocuments = document && trIsDocument;
+                if (!sameAffinity && !sameIntent && !bothDocuments) {
+                    continue;
+                }
+
+                if (bothDocuments) {
+                    // Do these documents belong to the same activity?
+                    final boolean sameActivity = task.realActivity != null
+                            && tr.realActivity != null
+                            && task.realActivity.equals(tr.realActivity);
+                    if (!sameActivity) {
+                        // If the document is open in another app or is not the same document, we
+                        // don't need to trim it.
+                        continue;
+                    } else if (maxRecents > 0) {
+                        // Otherwise only trim if we are over our max recents for this task
+                        --maxRecents;
+                        if (!sameIntent || multiTasksAllowed) {
+                            // We don't want to trim if we are not over the max allowed entries and
+                            // the tasks are not of the same intent filter, or multiple entries for
+                            // the task is allowed.
+                            continue;
+                        }
+                    }
+                    // Hit the maximum number of documents for this task. Fall through
+                    // and remove this document from recents.
+                } else if (document || trIsDocument) {
+                    // Only one of these is a document. Not the droid we're looking for.
+                    continue;
+                }
+            }
+            return i;
+        }
+        return -1;
+    }
+
+    // Extract the affiliates of the chain containing recent at index start.
+    private int processNextAffiliateChainLocked(int start) {
+        final TaskRecord startTask = mTasks.get(start);
+        final int affiliateId = startTask.mAffiliatedTaskId;
+
+        // Quick identification of isolated tasks. I.e. those not launched behind.
+        if (startTask.taskId == affiliateId && startTask.mPrevAffiliate == null &&
+                startTask.mNextAffiliate == null) {
+            // There is still a slim chance that there are other tasks that point to this task
+            // and that the chain is so messed up that this task no longer points to them but
+            // the gain of this optimization outweighs the risk.
+            startTask.inRecents = true;
+            return start + 1;
+        }
+
+        // Remove all tasks that are affiliated to affiliateId and put them in mTmpRecents.
+        mTmpRecents.clear();
+        for (int i = mTasks.size() - 1; i >= start; --i) {
+            final TaskRecord task = mTasks.get(i);
+            if (task.mAffiliatedTaskId == affiliateId) {
+                mTasks.remove(i);
+                mTmpRecents.add(task);
+            }
+        }
+
+        // Sort them all by taskId. That is the order they were create in and that order will
+        // always be correct.
+        Collections.sort(mTmpRecents, TASK_ID_COMPARATOR);
+
+        // Go through and fix up the linked list.
+        // The first one is the end of the chain and has no next.
+        final TaskRecord first = mTmpRecents.get(0);
+        first.inRecents = true;
+        if (first.mNextAffiliate != null) {
+            Slog.w(TAG, "Link error 1 first.next=" + first.mNextAffiliate);
+            first.setNextAffiliate(null);
+            notifyTaskPersisterLocked(first, false);
+        }
+        // Everything in the middle is doubly linked from next to prev.
+        final int tmpSize = mTmpRecents.size();
+        for (int i = 0; i < tmpSize - 1; ++i) {
+            final TaskRecord next = mTmpRecents.get(i);
+            final TaskRecord prev = mTmpRecents.get(i + 1);
+            if (next.mPrevAffiliate != prev) {
+                Slog.w(TAG, "Link error 2 next=" + next + " prev=" + next.mPrevAffiliate +
+                        " setting prev=" + prev);
+                next.setPrevAffiliate(prev);
+                notifyTaskPersisterLocked(next, false);
+            }
+            if (prev.mNextAffiliate != next) {
+                Slog.w(TAG, "Link error 3 prev=" + prev + " next=" + prev.mNextAffiliate +
+                        " setting next=" + next);
+                prev.setNextAffiliate(next);
+                notifyTaskPersisterLocked(prev, false);
+            }
+            prev.inRecents = true;
+        }
+        // The last one is the beginning of the list and has no prev.
+        final TaskRecord last = mTmpRecents.get(tmpSize - 1);
+        if (last.mPrevAffiliate != null) {
+            Slog.w(TAG, "Link error 4 last.prev=" + last.mPrevAffiliate);
+            last.setPrevAffiliate(null);
+            notifyTaskPersisterLocked(last, false);
+        }
+
+        // Insert the group back into mRecentTasks at start.
+        mTasks.addAll(start, mTmpRecents);
+        mTmpRecents.clear();
+
+        // Let the caller know where we left off.
+        return start + tmpSize;
+    }
+
+    private boolean moveAffiliatedTasksToFront(TaskRecord task, int taskIndex) {
+        int recentsCount = mTasks.size();
         TaskRecord top = task;
         int topIndex = taskIndex;
         while (top.mNextAffiliate != null && topIndex > 0) {
@@ -412,7 +1027,7 @@
         int endIndex = topIndex;
         TaskRecord prev = top;
         while (endIndex < recentsCount) {
-            TaskRecord cur = get(endIndex);
+            TaskRecord cur = mTasks.get(endIndex);
             if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "addRecent: looking at next chain @"
                     + endIndex + " " + cur);
             if (cur == top) {
@@ -487,8 +1102,8 @@
             for (int i=topIndex; i<=endIndex; i++) {
                 if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "addRecent: moving affiliated " + task
                         + " from " + i + " to " + (i-topIndex));
-                TaskRecord cur = remove(i);
-                add(i - topIndex, cur);
+                TaskRecord cur = mTasks.remove(i);
+                mTasks.add(i - topIndex, cur);
             }
             if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "addRecent: done moving tasks  " +  topIndex
                     + " to " + endIndex);
@@ -499,301 +1114,88 @@
         return false;
     }
 
-    final void addLocked(TaskRecord task) {
-        final boolean isAffiliated = task.mAffiliatedTaskId != task.taskId
-                || task.mNextAffiliateTaskId != INVALID_TASK_ID
-                || task.mPrevAffiliateTaskId != INVALID_TASK_ID;
-
-        int recentsCount = size();
-        // Quick case: never add voice sessions.
-        // TODO: VI what about if it's just an activity?
-        // Probably nothing to do here
-        if (task.voiceSession != null) {
-            if (DEBUG_RECENTS) Slog.d(TAG_RECENTS,
-                    "addRecent: not adding voice interaction " + task);
-            return;
-        }
-        // Another quick case: check if the top-most recent task is the same.
-        if (!isAffiliated && recentsCount > 0 && get(0) == task) {
-            if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "addRecent: already at top: " + task);
-            return;
-        }
-        // Another quick case: check if this is part of a set of affiliated
-        // tasks that are at the top.
-        if (isAffiliated && recentsCount > 0 && task.inRecents
-                && task.mAffiliatedTaskId == get(0).mAffiliatedTaskId) {
-            if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "addRecent: affiliated " + get(0)
-                    + " at top when adding " + task);
+    void dump(PrintWriter pw, boolean dumpAll, String dumpPackage) {
+        pw.println("ACTIVITY MANAGER RECENT TASKS (dumpsys activity recents)");
+        if (mTasks.isEmpty()) {
             return;
         }
 
-        boolean needAffiliationFix = false;
+        final MutableBoolean printedAnything = new MutableBoolean(false);
+        final MutableBoolean printedHeader = new MutableBoolean(false);
+        final int size = mTasks.size();
+        for (int i = 0; i < size; i++) {
+            final TaskRecord tr = mTasks.get(i);
+            if (dumpPackage != null && (tr.realActivity == null ||
+                    !dumpPackage.equals(tr.realActivity.getPackageName()))) {
+                continue;
+            }
 
-        // Slightly less quick case: the task is already in recents, so all we need
-        // to do is move it.
-        if (task.inRecents) {
-            int taskIndex = indexOf(task);
-            if (taskIndex >= 0) {
-                if (!isAffiliated || MOVE_AFFILIATED_TASKS_TO_FRONT) {
-                    // Simple case: this is not an affiliated task, so we just move it to the front.
-                    remove(taskIndex);
-                    add(0, task);
-                    notifyTaskPersisterLocked(task, false);
-                    if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "addRecent: moving to top " + task
-                            + " from " + taskIndex);
-                    return;
-                } else {
-                    // More complicated: need to keep all affiliated tasks together.
-                    if (moveAffiliatedTasksToFront(task, taskIndex)) {
-                        // All went well.
-                        return;
-                    }
-
-                    // Uh oh...  something bad in the affiliation chain, try to rebuild
-                    // everything and then go through our general path of adding a new task.
-                    needAffiliationFix = true;
-                }
-            } else {
-                Slog.wtf(TAG, "Task with inRecent not in recents: " + task);
-                needAffiliationFix = true;
+            if (!printedHeader.value) {
+                pw.println("  Recent tasks:");
+                printedHeader.value = true;
+                printedAnything.value = true;
+            }
+            pw.print("  * Recent #"); pw.print(i); pw.print(": ");
+            pw.println(tr);
+            if (dumpAll) {
+                tr.dump(pw, "    ");
             }
         }
 
-        if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "addRecent: trimming tasks for " + task);
-        trimForTaskLocked(task, true);
-
-        recentsCount = size();
-        final int maxRecents = ActivityManager.getMaxRecentTasksStatic();
-        while (recentsCount >= maxRecents) {
-            final TaskRecord tr = remove(recentsCount - 1);
-            tr.removedFromRecents();
-            recentsCount--;
-        }
-        task.inRecents = true;
-        if (!isAffiliated || needAffiliationFix) {
-            // If this is a simple non-affiliated task, or we had some failure trying to
-            // handle it as part of an affilated task, then just place it at the top.
-            add(0, task);
-            if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "addRecent: adding " + task);
-        } else if (isAffiliated) {
-            // If this is a new affiliated task, then move all of the affiliated tasks
-            // to the front and insert this new one.
-            TaskRecord other = task.mNextAffiliate;
-            if (other == null) {
-                other = task.mPrevAffiliate;
-            }
-            if (other != null) {
-                int otherIndex = indexOf(other);
-                if (otherIndex >= 0) {
-                    // Insert new task at appropriate location.
-                    int taskIndex;
-                    if (other == task.mNextAffiliate) {
-                        // We found the index of our next affiliation, which is who is
-                        // before us in the list, so add after that point.
-                        taskIndex = otherIndex+1;
-                    } else {
-                        // We found the index of our previous affiliation, which is who is
-                        // after us in the list, so add at their position.
-                        taskIndex = otherIndex;
-                    }
-                    if (DEBUG_RECENTS) Slog.d(TAG_RECENTS,
-                            "addRecent: new affiliated task added at " + taskIndex + ": " + task);
-                    add(taskIndex, task);
-
-                    // Now move everything to the front.
-                    if (moveAffiliatedTasksToFront(task, taskIndex)) {
-                        // All went well.
-                        return;
-                    }
-
-                    // Uh oh...  something bad in the affiliation chain, try to rebuild
-                    // everything and then go through our general path of adding a new task.
-                    needAffiliationFix = true;
-                } else {
-                    if (DEBUG_RECENTS) Slog.d(TAG_RECENTS,
-                            "addRecent: couldn't find other affiliation " + other);
-                    needAffiliationFix = true;
-                }
-            } else {
-                if (DEBUG_RECENTS) Slog.d(TAG_RECENTS,
-                        "addRecent: adding affiliated task without next/prev:" + task);
-                needAffiliationFix = true;
-            }
-        }
-
-        if (needAffiliationFix) {
-            if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "addRecent: regrouping affiliations");
-            cleanupLocked(task.userId);
+        if (!printedAnything.value) {
+            pw.println("  (nothing)");
         }
     }
 
     /**
-     * If needed, remove oldest existing entries in recents that are for the same kind
-     * of task as the given one.
+     * Creates a new RecentTaskInfo from a TaskRecord.
      */
-    int trimForTaskLocked(TaskRecord task, boolean doTrim) {
-        int recentsCount = size();
-        final Intent intent = task.intent;
-        final boolean document = intent != null && intent.isDocument();
-        int maxRecents = task.maxRecents - 1;
-        final ActivityStack stack = task.getStack();
-        for (int i = 0; i < recentsCount; i++) {
-            final TaskRecord tr = get(i);
-            final ActivityStack trStack = tr.getStack();
-            if (task != tr) {
-                if (stack != null && trStack != null && stack != trStack) {
-                    continue;
-                }
-                if (task.userId != tr.userId) {
-                    continue;
-                }
-                final Intent trIntent = tr.intent;
-                final boolean sameAffinity =
-                        task.affinity != null && task.affinity.equals(tr.affinity);
-                final boolean sameIntentFilter = intent != null && intent.filterEquals(trIntent);
-                boolean multiTasksAllowed = false;
-                final int flags = intent.getFlags();
-                if ((flags & (FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_NEW_DOCUMENT)) != 0
-                        && (flags & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) {
-                    multiTasksAllowed = true;
-                }
-                final boolean trIsDocument = trIntent != null && trIntent.isDocument();
-                final boolean bothDocuments = document && trIsDocument;
-                if (!sameAffinity && !sameIntentFilter && !bothDocuments) {
-                    continue;
-                }
+    static ActivityManager.RecentTaskInfo createRecentTaskInfo(TaskRecord tr) {
+        // Update the task description to reflect any changes in the task stack
+        tr.updateTaskDescription();
 
-                if (bothDocuments) {
-                    // Do these documents belong to the same activity?
-                    final boolean sameActivity = task.realActivity != null
-                            && tr.realActivity != null
-                            && task.realActivity.equals(tr.realActivity);
-                    // If the document is open in another app or is not the same
-                    // document, we don't need to trim it.
-                    if (!sameActivity) {
-                        continue;
-                    // Otherwise only trim if we are over our max recents for this task
-                    } else if (maxRecents > 0) {
-                        --maxRecents;
-                        if (!doTrim || !sameIntentFilter || multiTasksAllowed) {
-                            // We don't want to trim if we are not over the max allowed entries and
-                            // the caller doesn't want us to trim, the tasks are not of the same
-                            // intent filter, or multiple entries fot the task is allowed.
-                            continue;
-                        }
-                    }
-                    // Hit the maximum number of documents for this task. Fall through
-                    // and remove this document from recents.
-                } else if (document || trIsDocument) {
-                    // Only one of these is a document. Not the droid we're looking for.
-                    continue;
-                }
-            }
+        // Compose the recent task info
+        ActivityManager.RecentTaskInfo rti = new ActivityManager.RecentTaskInfo();
+        rti.id = tr.getTopActivity() == null ? INVALID_TASK_ID : tr.taskId;
+        rti.persistentId = tr.taskId;
+        rti.baseIntent = new Intent(tr.getBaseIntent());
+        rti.origActivity = tr.origActivity;
+        rti.realActivity = tr.realActivity;
+        rti.description = tr.lastDescription;
+        rti.stackId = tr.getStackId();
+        rti.userId = tr.userId;
+        rti.taskDescription = new ActivityManager.TaskDescription(tr.lastTaskDescription);
+        rti.firstActiveTime = tr.firstActiveTime;
+        rti.lastActiveTime = tr.lastActiveTime;
+        rti.affiliatedTaskId = tr.mAffiliatedTaskId;
+        rti.affiliatedTaskColor = tr.mAffiliatedTaskColor;
+        rti.numActivities = 0;
+        if (tr.mBounds != null) {
+            rti.bounds = new Rect(tr.mBounds);
+        }
+        rti.supportsSplitScreenMultiWindow = tr.supportsSplitScreenWindowingMode();
+        rti.resizeMode = tr.mResizeMode;
+        rti.configuration.setTo(tr.getConfiguration());
 
-            if (!doTrim) {
-                // If the caller is not actually asking for a trim, just tell them we reached
-                // a point where the trim would happen.
-                return i;
-            }
+        ActivityRecord base = null;
+        ActivityRecord top = null;
+        ActivityRecord tmp;
 
-            // Either task and tr are the same or, their affinities match or their intents match
-            // and neither of them is a document, or they are documents using the same activity
-            // and their maxRecents has been reached.
-            remove(i);
-            if (task != tr) {
-                tr.removedFromRecents();
+        for (int i = tr.mActivities.size() - 1; i >= 0; --i) {
+            tmp = tr.mActivities.get(i);
+            if (tmp.finishing) {
+                continue;
             }
-            i--;
-            recentsCount--;
-            if (task.intent == null) {
-                // If the new recent task we are adding is not fully
-                // specified, then replace it with the existing recent task.
-                task = tr;
+            base = tmp;
+            if (top == null || (top.state == ActivityState.INITIALIZING)) {
+                top = base;
             }
-            notifyTaskPersisterLocked(tr, false);
+            rti.numActivities++;
         }
 
-        return -1;
-    }
+        rti.baseActivity = (base != null) ? base.intent.getComponent() : null;
+        rti.topActivity = (top != null) ? top.intent.getComponent() : null;
 
-    // Sort by taskId
-    private static Comparator<TaskRecord> sTaskRecordComparator = new Comparator<TaskRecord>() {
-        @Override
-        public int compare(TaskRecord lhs, TaskRecord rhs) {
-            return rhs.taskId - lhs.taskId;
-        }
-    };
-
-    // Extract the affiliates of the chain containing recent at index start.
-    private int processNextAffiliateChainLocked(int start) {
-        final TaskRecord startTask = get(start);
-        final int affiliateId = startTask.mAffiliatedTaskId;
-
-        // Quick identification of isolated tasks. I.e. those not launched behind.
-        if (startTask.taskId == affiliateId && startTask.mPrevAffiliate == null &&
-                startTask.mNextAffiliate == null) {
-            // There is still a slim chance that there are other tasks that point to this task
-            // and that the chain is so messed up that this task no longer points to them but
-            // the gain of this optimization outweighs the risk.
-            startTask.inRecents = true;
-            return start + 1;
-        }
-
-        // Remove all tasks that are affiliated to affiliateId and put them in mTmpRecents.
-        mTmpRecents.clear();
-        for (int i = size() - 1; i >= start; --i) {
-            final TaskRecord task = get(i);
-            if (task.mAffiliatedTaskId == affiliateId) {
-                remove(i);
-                mTmpRecents.add(task);
-            }
-        }
-
-        // Sort them all by taskId. That is the order they were create in and that order will
-        // always be correct.
-        Collections.sort(mTmpRecents, sTaskRecordComparator);
-
-        // Go through and fix up the linked list.
-        // The first one is the end of the chain and has no next.
-        final TaskRecord first = mTmpRecents.get(0);
-        first.inRecents = true;
-        if (first.mNextAffiliate != null) {
-            Slog.w(TAG, "Link error 1 first.next=" + first.mNextAffiliate);
-            first.setNextAffiliate(null);
-            notifyTaskPersisterLocked(first, false);
-        }
-        // Everything in the middle is doubly linked from next to prev.
-        final int tmpSize = mTmpRecents.size();
-        for (int i = 0; i < tmpSize - 1; ++i) {
-            final TaskRecord next = mTmpRecents.get(i);
-            final TaskRecord prev = mTmpRecents.get(i + 1);
-            if (next.mPrevAffiliate != prev) {
-                Slog.w(TAG, "Link error 2 next=" + next + " prev=" + next.mPrevAffiliate +
-                        " setting prev=" + prev);
-                next.setPrevAffiliate(prev);
-                notifyTaskPersisterLocked(next, false);
-            }
-            if (prev.mNextAffiliate != next) {
-                Slog.w(TAG, "Link error 3 prev=" + prev + " next=" + prev.mNextAffiliate +
-                        " setting next=" + next);
-                prev.setNextAffiliate(next);
-                notifyTaskPersisterLocked(prev, false);
-            }
-            prev.inRecents = true;
-        }
-        // The last one is the beginning of the list and has no prev.
-        final TaskRecord last = mTmpRecents.get(tmpSize - 1);
-        if (last.mPrevAffiliate != null) {
-            Slog.w(TAG, "Link error 4 last.prev=" + last.mPrevAffiliate);
-            last.setPrevAffiliate(null);
-            notifyTaskPersisterLocked(last, false);
-        }
-
-        // Insert the group back into mRecentTasks at start.
-        addAll(start, mTmpRecents);
-        mTmpRecents.clear();
-
-        // Let the caller know where we left off.
-        return start + tmpSize;
+        return rti;
     }
 }
diff --git a/services/core/java/com/android/server/am/TaskPersister.java b/services/core/java/com/android/server/am/TaskPersister.java
index 61994b5..2689d6a 100644
--- a/services/core/java/com/android/server/am/TaskPersister.java
+++ b/services/core/java/com/android/server/am/TaskPersister.java
@@ -567,7 +567,7 @@
         SparseArray<SparseBooleanArray> changedTaskIdsPerUser = new SparseArray<>();
         synchronized (mService) {
             for (int userId : mRecentTasks.usersWithRecentsLoadedLocked()) {
-                SparseBooleanArray taskIdsToSave = mRecentTasks.mPersistedTaskIds.get(userId);
+                SparseBooleanArray taskIdsToSave = mRecentTasks.getTaskIdsForUser(userId);
                 SparseBooleanArray persistedIdsInFile = mTaskIdsInFile.get(userId);
                 if (persistedIdsInFile != null && persistedIdsInFile.equals(taskIdsToSave)) {
                     continue;
@@ -640,7 +640,7 @@
         @Override
         public void run() {
             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
-            ArraySet<Integer> persistentTaskIds = new ArraySet<Integer>();
+            ArraySet<Integer> persistentTaskIds = new ArraySet<>();
             while (true) {
                 // We can't lock mService while holding TaskPersister.this, but we don't want to
                 // call removeObsoleteFiles every time through the loop, only the last time before
@@ -654,20 +654,7 @@
                     persistentTaskIds.clear();
                     synchronized (mService) {
                         if (DEBUG) Slog.d(TAG, "mRecents=" + mRecentTasks);
-                        for (int taskNdx = mRecentTasks.size() - 1; taskNdx >= 0; --taskNdx) {
-                            final TaskRecord task = mRecentTasks.get(taskNdx);
-                            if (DEBUG) Slog.d(TAG, "LazyTaskWriter: task=" + task +
-                                    " persistable=" + task.isPersistable);
-                            final ActivityStack stack = task.getStack();
-                            if ((task.isPersistable || task.inRecents)
-                                    && (stack == null || !stack.isHomeOrRecentsStack())) {
-                                if (DEBUG) Slog.d(TAG, "adding to persistentTaskIds task=" + task);
-                                persistentTaskIds.add(task.taskId);
-                            } else {
-                                if (DEBUG) Slog.d(TAG,
-                                        "omitting from persistentTaskIds task=" + task);
-                            }
-                        }
+                        mRecentTasks.getPersistableTaskIds(persistentTaskIds);
                         mService.mWindowManager.removeObsoleteTaskFiles(persistentTaskIds,
                                 mRecentTasks.usersWithRecentsLoadedLocked());
                     }
diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityTestsBase.java b/services/tests/servicestests/src/com/android/server/am/ActivityTestsBase.java
index cc8bd69..b4db4a3 100644
--- a/services/tests/servicestests/src/com/android/server/am/ActivityTestsBase.java
+++ b/services/tests/servicestests/src/com/android/server/am/ActivityTestsBase.java
@@ -103,12 +103,18 @@
 
     protected static TaskRecord createTask(ActivityStackSupervisor supervisor,
             ComponentName component, ActivityStack stack) {
+        return createTask(supervisor, component, 0 /* flags */, stack);
+    }
+
+    protected static TaskRecord createTask(ActivityStackSupervisor supervisor,
+            ComponentName component, int flags, ActivityStack stack) {
         final ActivityInfo aInfo = new ActivityInfo();
         aInfo.applicationInfo = new ApplicationInfo();
         aInfo.applicationInfo.packageName = component.getPackageName();
 
         Intent intent = new Intent();
         intent.setComponent(component);
+        intent.setFlags(flags);
 
         final TaskRecord task = new TaskRecord(supervisor.mService, 0, aInfo, intent /*intent*/,
                 null /*_taskDescription*/);
diff --git a/services/tests/servicestests/src/com/android/server/am/RecentTasksTest.java b/services/tests/servicestests/src/com/android/server/am/RecentTasksTest.java
new file mode 100644
index 0000000..f40b646
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/RecentTasksTest.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.am;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.SparseBooleanArray;
+
+import com.android.server.am.RecentTasks.Callbacks;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * runtest --path frameworks/base/services/tests/servicestests/src/com/android/server/am/RecentTasksTest.java
+ */
+@MediumTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class RecentTasksTest extends ActivityTestsBase {
+    private static final int TEST_USER_0_ID = 0;
+    private static final int TEST_USER_1_ID = 10;
+
+    private Context mContext = InstrumentationRegistry.getContext();
+    private ActivityManagerService mService;
+    private ActivityStack mStack;
+    private TestTaskPersister mTaskPersister;
+    private RecentTasks mRecentTasks;
+
+    private static ArrayList<TaskRecord> mTasks = new ArrayList<>();
+    private static ArrayList<TaskRecord> mSameDocumentTasks = new ArrayList<>();
+
+    private CallbacksRecorder mCallbacksRecorder;
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mService = createActivityManagerService();
+        mStack = mService.mStackSupervisor.getDefaultDisplay().createStack(
+                WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
+        mTaskPersister = new TestTaskPersister(mContext.getFilesDir());
+        mRecentTasks = new RecentTasks(mService, mTaskPersister);
+        mCallbacksRecorder = new CallbacksRecorder();
+        mRecentTasks.registerCallback(mCallbacksRecorder);
+
+        mTasks.add(createTask(".Task1"));
+        mTasks.add(createTask(".Task2"));
+        mTasks.add(createTask(".Task3"));
+
+        mSameDocumentTasks.add(createDocumentTask(".DocumentTask1", null /* affinity */));
+        mSameDocumentTasks.add(createDocumentTask(".DocumentTask1", null /* affinity */));
+    }
+
+    @Test
+    public void testCallbacks() throws Exception {
+        // Add some tasks
+        mRecentTasks.add(mTasks.get(0));
+        mRecentTasks.add(mTasks.get(1));
+        assertTrue(mCallbacksRecorder.added.contains(mTasks.get(0))
+                && mCallbacksRecorder.added.contains(mTasks.get(1)));
+        assertTrue(mCallbacksRecorder.removed.isEmpty());
+        mCallbacksRecorder.clear();
+
+        // Remove some tasks
+        mRecentTasks.remove(mTasks.get(0));
+        mRecentTasks.remove(mTasks.get(1));
+        assertTrue(mCallbacksRecorder.added.isEmpty());
+        assertTrue(mCallbacksRecorder.removed.contains(mTasks.get(0)));
+        assertTrue(mCallbacksRecorder.removed.contains(mTasks.get(1)));
+        mCallbacksRecorder.clear();
+
+        // Add a task which will trigger the trimming of another
+        TaskRecord documentTask1 = createDocumentTask(".DocumentTask1", null /* affinity */);
+        documentTask1.maxRecents = 1;
+        TaskRecord documentTask2 = createDocumentTask(".DocumentTask1", null /* affinity */);
+        mRecentTasks.add(documentTask1);
+        mRecentTasks.add(documentTask2);
+        assertTrue(mCallbacksRecorder.added.contains(documentTask1));
+        assertTrue(mCallbacksRecorder.added.contains(documentTask2));
+        assertTrue(mCallbacksRecorder.removed.contains(documentTask1));
+        mCallbacksRecorder.clear();
+
+        // Remove the callback, ensure we don't get any calls
+        mRecentTasks.unregisterCallback(mCallbacksRecorder);
+        mRecentTasks.add(mTasks.get(0));
+        mRecentTasks.remove(mTasks.get(0));
+        assertTrue(mCallbacksRecorder.added.isEmpty());
+        assertTrue(mCallbacksRecorder.removed.isEmpty());
+    }
+
+    @Test
+    public void testUsersTasks() throws Exception {
+        // Setup some tasks for the users
+        mTaskPersister.userTaskIdsOverride = new SparseBooleanArray();
+        mTaskPersister.userTaskIdsOverride.put(1, true);
+        mTaskPersister.userTaskIdsOverride.put(2, true);
+        mTaskPersister.userTasksOverride = new ArrayList<>();
+        mTaskPersister.userTasksOverride.add(createTask(".UserTask1"));
+        mTaskPersister.userTasksOverride.add(createTask(".UserTask2"));
+
+        // Assert no user tasks are initially loaded
+        assertTrue(mRecentTasks.usersWithRecentsLoadedLocked().length == 0);
+
+        // Load user 0 tasks
+        mRecentTasks.loadUserRecentsLocked(TEST_USER_0_ID);
+        assertTrue(arrayContainsUser(mRecentTasks.usersWithRecentsLoadedLocked(), TEST_USER_0_ID));
+        assertTrue(mRecentTasks.containsTaskId(1, TEST_USER_0_ID));
+        assertTrue(mRecentTasks.containsTaskId(2, TEST_USER_0_ID));
+
+        // Load user 1 tasks
+        mRecentTasks.loadUserRecentsLocked(TEST_USER_1_ID);
+        assertTrue(arrayContainsUser(mRecentTasks.usersWithRecentsLoadedLocked(), TEST_USER_0_ID));
+        assertTrue(arrayContainsUser(mRecentTasks.usersWithRecentsLoadedLocked(), TEST_USER_1_ID));
+        assertTrue(mRecentTasks.containsTaskId(1, TEST_USER_0_ID));
+        assertTrue(mRecentTasks.containsTaskId(2, TEST_USER_0_ID));
+        assertTrue(mRecentTasks.containsTaskId(1, TEST_USER_1_ID));
+        assertTrue(mRecentTasks.containsTaskId(2, TEST_USER_1_ID));
+
+        // Unload user 1 tasks
+        mRecentTasks.unloadUserDataFromMemoryLocked(TEST_USER_1_ID);
+        assertTrue(arrayContainsUser(mRecentTasks.usersWithRecentsLoadedLocked(), TEST_USER_0_ID));
+        assertFalse(arrayContainsUser(mRecentTasks.usersWithRecentsLoadedLocked(), TEST_USER_1_ID));
+        assertTrue(mRecentTasks.containsTaskId(1, TEST_USER_0_ID));
+        assertTrue(mRecentTasks.containsTaskId(2, TEST_USER_0_ID));
+
+        // Unload user 0 tasks
+        mRecentTasks.unloadUserDataFromMemoryLocked(TEST_USER_0_ID);
+        assertFalse(arrayContainsUser(mRecentTasks.usersWithRecentsLoadedLocked(), TEST_USER_0_ID));
+        assertFalse(arrayContainsUser(mRecentTasks.usersWithRecentsLoadedLocked(), TEST_USER_1_ID));
+    }
+
+    private ComponentName createComponent(String className) {
+        return new ComponentName(mContext.getPackageName(), className);
+    }
+
+    private TaskRecord createTask(String className) {
+        return createTask(mService.mStackSupervisor, createComponent(className), mStack);
+    }
+
+    private TaskRecord createDocumentTask(String className, String affinity) {
+        TaskRecord task = createTask(mService.mStackSupervisor, createComponent(className),
+                FLAG_ACTIVITY_NEW_DOCUMENT, mStack);
+        task.affinity = affinity;
+        return task;
+    }
+
+    private boolean arrayContainsUser(int[] userIds, int targetUserId) {
+        Arrays.sort(userIds);
+        return Arrays.binarySearch(userIds, targetUserId) >= 0;
+    }
+
+    private static class CallbacksRecorder implements Callbacks {
+        ArrayList<TaskRecord> added = new ArrayList<>();
+        ArrayList<TaskRecord> removed = new ArrayList<>();
+
+        void clear() {
+            added.clear();
+            removed.clear();
+        }
+
+        @Override
+        public void onRecentTaskAdded(TaskRecord task) {
+            added.add(task);
+        }
+
+        @Override
+        public void onRecentTaskRemoved(TaskRecord task) {
+            removed.add(task);
+        }
+    }
+
+    private static class TestTaskPersister extends TaskPersister {
+
+        SparseBooleanArray userTaskIdsOverride;
+        ArrayList<TaskRecord> userTasksOverride;
+
+        TestTaskPersister(File workingDir) {
+            super(workingDir);
+        }
+
+        @Override
+        SparseBooleanArray loadPersistedTaskIdsForUser(int userId) {
+            if (userTaskIdsOverride != null) {
+                return userTaskIdsOverride;
+            }
+            return super.loadPersistedTaskIdsForUser(userId);
+        }
+
+        @Override
+        List<TaskRecord> restoreTasksForUserLocked(int userId, SparseBooleanArray preaddedTasks) {
+            if (userTasksOverride != null) {
+                return userTasksOverride;
+            }
+            return super.restoreTasksForUserLocked(userId, preaddedTasks);
+        }
+    }
+}
\ No newline at end of file