2/Trimming tasks based on active and visibility

- Add logic to trim tasks beyond the max-recents policy, including
  migrating the trimming of quiet profile tasks and visibile tasks
  to the system (to ensure callers always get a consistent list)
- Remove trimmed recent tasks from the active task list
- Add logic to actually handle config_hasRecents to determine whether to
  apply visibility filtering (otherwise only filters by max-recents as
  before)

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

Change-Id: If17586cd9e8ac1ae8f112239381adc96a0528123
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index dcb56a2..570f37c 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2352,9 +2352,37 @@
     <!-- Package name for default network scorer app; overridden by product overlays. -->
     <string name="config_defaultNetworkScorerPackageName"></string>
 
-    <!-- default device has recents property -->
+    <!-- Determines whether recent tasks are provided to the user. Default device has recents
+         property. If this is false, then the following recents config flags are ignored. -->
     <bool name="config_hasRecents">true</bool>
 
+    <!-- The minimum number of visible recent tasks to be presented to the user through the
+         SystemUI. Can be -1 if there is no minimum limit. -->
+    <integer name="config_minNumVisibleRecentTasks_grid">-1</integer>
+
+    <!-- The maximum number of visible recent tasks to be presented to the user through the
+         SystemUI. Can be -1 if there is no maximum limit. -->
+    <integer name="config_maxNumVisibleRecentTasks_grid">9</integer>
+
+    <!-- The minimum number of visible recent tasks to be presented to the user through the
+         SystemUI. Can be -1 if there is no minimum limit. -->
+    <integer name="config_minNumVisibleRecentTasks_lowRam">-1</integer>
+
+    <!-- The maximum number of visible recent tasks to be presented to the user through the
+         SystemUI. Can be -1 if there is no maximum limit. -->
+    <integer name="config_maxNumVisibleRecentTasks_lowRam">9</integer>
+
+    <!-- The minimum number of visible recent tasks to be presented to the user through the
+         SystemUI. Can be -1 if there is no minimum limit. -->
+    <integer name="config_minNumVisibleRecentTasks">5</integer>
+
+    <!-- The maximum number of visible recent tasks to be presented to the user through the
+         SystemUI. Can be -1 if there is no maximum limit. -->
+    <integer name="config_maxNumVisibleRecentTasks">-1</integer>
+
+    <!-- The duration in which a recent task is considered in session and should be visible. -->
+    <integer name="config_activeTaskDurationHours">6</integer>
+
     <!-- default window ShowCircularMask property -->
     <bool name="config_windowShowCircularMask">false</bool>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 44a10e8..5ac120b 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -311,6 +311,13 @@
   <java-symbol type="bool" name="config_enableMultiUserUI"/>
   <java-symbol type="bool" name="config_disableUsbPermissionDialogs"/>
   <java-symbol type="bool" name="config_hasRecents" />
+  <java-symbol type="integer" name="config_minNumVisibleRecentTasks_lowRam" />
+  <java-symbol type="integer" name="config_maxNumVisibleRecentTasks_lowRam" />
+  <java-symbol type="integer" name="config_minNumVisibleRecentTasks_grid" />
+  <java-symbol type="integer" name="config_maxNumVisibleRecentTasks_grid" />
+  <java-symbol type="integer" name="config_minNumVisibleRecentTasks" />
+  <java-symbol type="integer" name="config_maxNumVisibleRecentTasks" />
+  <java-symbol type="integer" name="config_activeTaskDurationHours" />
   <java-symbol type="bool" name="config_windowShowCircularMask" />
   <java-symbol type="bool" name="config_windowEnableCircularEmulatorDisplayOverlay" />
   <java-symbol type="bool" name="config_wifi_framework_enable_associated_network_selection" />
diff --git a/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java b/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java
index 3a9bf12..62c811c 100644
--- a/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java
+++ b/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java
@@ -74,6 +74,7 @@
     static final boolean DEBUG_PROVIDER = DEBUG_ALL || false;
     static final boolean DEBUG_PSS = DEBUG_ALL || false;
     static final boolean DEBUG_RECENTS = DEBUG_ALL || false;
+    static final boolean DEBUG_RECENTS_TRIM_TASKS = DEBUG_RECENTS || false;
     static final boolean DEBUG_RELEASE = DEBUG_ALL_ACTIVITIES || false;
     static final boolean DEBUG_RESULTS = DEBUG_ALL || false;
     static final boolean DEBUG_SAVED_STATE = DEBUG_ALL_ACTIVITIES || false;
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 90a5ca2..c80ef19 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -1730,9 +1730,6 @@
      */
     private boolean mUserIsMonkey;
 
-    /** Flag whether the device has a Recents UI */
-    boolean mHasRecents;
-
     /** The dimensions of the thumbnails in the Recents UI. */
     int mThumbnailWidth;
     int mThumbnailHeight;
@@ -2777,6 +2774,7 @@
                 new TaskChangeNotificationController(this, mStackSupervisor, mHandler);
         mActivityStarter = new ActivityStarter(this, mStackSupervisor);
         mRecentTasks = new RecentTasks(this, mStackSupervisor);
+        mStackSupervisor.setRecentTasks(mRecentTasks);
         mLockTaskController = new LockTaskController(mContext, mStackSupervisor, mHandler);
 
         mProcessCpuThread = new Thread("CpuTracker") {
@@ -13911,7 +13909,6 @@
 
             // Load resources only after the current configuration has been set.
             final Resources res = mContext.getResources();
-            mHasRecents = res.getBoolean(com.android.internal.R.bool.config_hasRecents);
             mThumbnailWidth = res.getDimensionPixelSize(
                     com.android.internal.R.dimen.thumbnail_width);
             mThumbnailHeight = res.getDimensionPixelSize(
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index 543d492..5221afd 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -3016,7 +3016,13 @@
     }
 
     @Override
-    public void onRecentTaskRemoved(TaskRecord task) {
+    public void onRecentTaskRemoved(TaskRecord task, boolean wasTrimmed) {
+        if (wasTrimmed) {
+            // Task was trimmed from the recent tasks list -- remove the active task record as well
+            // since the user won't really be able to go back to it
+            removeTaskByIdLocked(task.taskId, false /* killProcess */,
+                    false /* removeFromRecents */);
+        }
         task.removedFromRecents();
     }
 
diff --git a/services/core/java/com/android/server/am/RecentTasks.java b/services/core/java/com/android/server/am/RecentTasks.java
index 9091786..7e6c645 100644
--- a/services/core/java/com/android/server/am/RecentTasks.java
+++ b/services/core/java/com/android/server/am/RecentTasks.java
@@ -23,11 +23,16 @@
 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.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
 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_RECENTS_TRIM_TASKS;
 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;
@@ -47,16 +52,19 @@
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
 import android.content.pm.ParceledListSlice;
+import android.content.pm.UserInfo;
+import android.content.res.Resources;
 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.SystemProperties;
 import android.os.UserHandle;
 import android.util.ArraySet;
 import android.util.MutableBoolean;
+import android.util.MutableInt;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
@@ -73,7 +81,7 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Set;
-import java.util.function.BiConsumer;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Class for managing the recent tasks list. The list is ordered by most recent (index 0) to the
@@ -111,7 +119,7 @@
         /**
          * Called when a task is removed from the recent tasks list.
          */
-        void onRecentTaskRemoved(TaskRecord task);
+        void onRecentTaskRemoved(TaskRecord task, boolean wasTrimmed);
     }
 
     /**
@@ -119,6 +127,7 @@
      */
     private final TaskPersister mTaskPersister;
     private final ActivityManagerService mService;
+    private final UserController mUserController;
 
     /**
      * Mapping of user id -> whether recent tasks have been loaded for that user.
@@ -133,26 +142,83 @@
     private final SparseArray<SparseBooleanArray> mPersistedTaskIds = new SparseArray<>(
             DEFAULT_INITIAL_CAPACITY);
 
-    // List of recent tasks
+    // List of all active recent tasks
     private final ArrayList<TaskRecord> mTasks = new ArrayList<>();
     private final ArrayList<Callbacks> mCallbacks = new ArrayList<>();
 
+    // These values are generally loaded from resources, but can be set dynamically in the tests
+    private boolean mHasVisibleRecentTasks;
+    private int mGlobalMaxNumTasks;
+    private int mMinNumVisibleTasks;
+    private int mMaxNumVisibleTasks;
+    private long mActiveTasksSessionDurationMs;
+
     // Mainly to avoid object recreation on multiple calls.
     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 SparseBooleanArray mTmpQuietProfileUserIds = new SparseBooleanArray();
 
     @VisibleForTesting
-    RecentTasks(ActivityManagerService service, TaskPersister taskPersister) {
+    RecentTasks(ActivityManagerService service, TaskPersister taskPersister,
+            UserController userController) {
         mService = service;
+        mUserController = userController;
         mTaskPersister = taskPersister;
+        mGlobalMaxNumTasks = ActivityManager.getMaxRecentTasksStatic();
+        mHasVisibleRecentTasks = true;
     }
 
     RecentTasks(ActivityManagerService service, ActivityStackSupervisor stackSupervisor) {
-        File systemDir = Environment.getDataSystemDirectory();
+        final File systemDir = Environment.getDataSystemDirectory();
+        final Resources res = service.mContext.getResources();
         mService = service;
+        mUserController = service.mUserController;
         mTaskPersister = new TaskPersister(systemDir, stackSupervisor, service, this);
-        stackSupervisor.setRecentTasks(this);
+        mGlobalMaxNumTasks = ActivityManager.getMaxRecentTasksStatic();
+        mHasVisibleRecentTasks = res.getBoolean(com.android.internal.R.bool.config_hasRecents);
+        loadParametersFromResources(service.mContext.getResources());
+    }
+
+    @VisibleForTesting
+    void setParameters(int minNumVisibleTasks, int maxNumVisibleTasks,
+            long activeSessionDurationMs) {
+        mMinNumVisibleTasks = minNumVisibleTasks;
+        mMaxNumVisibleTasks = maxNumVisibleTasks;
+        mActiveTasksSessionDurationMs = activeSessionDurationMs;
+    }
+
+    @VisibleForTesting
+    void setGlobalMaxNumTasks(int globalMaxNumTasks) {
+        mGlobalMaxNumTasks = globalMaxNumTasks;
+    }
+
+    /**
+     * Loads the parameters from the system resources.
+     */
+    @VisibleForTesting
+    void loadParametersFromResources(Resources res) {
+        if (ActivityManager.isLowRamDeviceStatic()) {
+            mMinNumVisibleTasks = res.getInteger(
+                    com.android.internal.R.integer.config_minNumVisibleRecentTasks_lowRam);
+            mMaxNumVisibleTasks = res.getInteger(
+                    com.android.internal.R.integer.config_maxNumVisibleRecentTasks_lowRam);
+        } else if (SystemProperties.getBoolean("ro.recents.grid", false)) {
+            mMinNumVisibleTasks = res.getInteger(
+                    com.android.internal.R.integer.config_minNumVisibleRecentTasks_grid);
+            mMaxNumVisibleTasks = res.getInteger(
+                    com.android.internal.R.integer.config_maxNumVisibleRecentTasks_grid);
+        } else {
+            mMinNumVisibleTasks = res.getInteger(
+                    com.android.internal.R.integer.config_minNumVisibleRecentTasks);
+            mMaxNumVisibleTasks = res.getInteger(
+                    com.android.internal.R.integer.config_maxNumVisibleRecentTasks);
+        }
+        final int sessionDurationHrs = res.getInteger(
+                com.android.internal.R.integer.config_activeTaskDurationHours);
+        mActiveTasksSessionDurationMs = (sessionDurationHrs > 0)
+                ? TimeUnit.HOURS.toMillis(sessionDurationHrs)
+                : -1;
     }
 
     void registerCallback(Callbacks callback) {
@@ -171,7 +237,7 @@
 
     private void notifyTaskRemoved(TaskRecord task, boolean wasTrimmed) {
         for (int i = 0; i < mCallbacks.size(); i++) {
-            mCallbacks.get(i).onRecentTaskRemoved(task);
+            mCallbacks.get(i).onRecentTaskRemoved(task, wasTrimmed);
         }
     }
 
@@ -558,7 +624,7 @@
 
         final Set<Integer> includedUsers;
         if (includeProfiles) {
-            includedUsers = mService.mUserController.getProfileIds(userId);
+            includedUsers = mUserController.getProfileIds(userId);
         } else {
             includedUsers = new HashSet<>();
         }
@@ -677,6 +743,11 @@
         }
     }
 
+    @VisibleForTesting
+    ArrayList<TaskRecord> getRawTasks() {
+        return mTasks;
+    }
+
     /**
      * @return the task in the task list with the given {@param id} if one exists.
      */
@@ -695,6 +766,8 @@
      * Add a new task to the recent tasks list.
      */
     void add(TaskRecord task) {
+        if (DEBUG_RECENTS_TRIM_TASKS) Slog.d(TAG, "add: task=" + task);
+
         final boolean isAffiliated = task.mAffiliatedTaskId != task.taskId
                 || task.mNextAffiliateTaskId != INVALID_TASK_ID
                 || task.mPrevAffiliateTaskId != INVALID_TASK_ID;
@@ -756,7 +829,6 @@
 
         if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "addRecent: trimming tasks for " + task);
         trimForAddTask(task);
-        trimToMaxNumRecents();
 
         task.inRecents = true;
         if (!isAffiliated || needAffiliationFix) {
@@ -816,6 +888,9 @@
             if (DEBUG_RECENTS) Slog.d(TAG_RECENTS, "addRecent: regrouping affiliations");
             cleanupLocked(task.userId);
         }
+
+        // Trim the set of tasks to the active set
+        trimInactiveRecentTasks();
     }
 
     /**
@@ -843,14 +918,166 @@
     /**
      * Trims the recents task list to the global max number of recents.
      */
-    private void trimToMaxNumRecents() {
+    private void trimInactiveRecentTasks() {
         int recentsCount = mTasks.size();
-        final int maxNumRecents = ActivityManager.getMaxRecentTasksStatic();
-        while (recentsCount >= maxNumRecents) {
+
+        // Remove from the end of the list until we reach the max number of recents
+        while (recentsCount > mGlobalMaxNumTasks) {
             final TaskRecord tr = mTasks.remove(recentsCount - 1);
-            notifyTaskRemoved(tr, !TRIMMED);
+            notifyTaskRemoved(tr, TRIMMED);
             recentsCount--;
+            if (DEBUG_RECENTS_TRIM_TASKS) Slog.d(TAG, "Trimming over max-recents task=" + tr
+                    + " max=" + mGlobalMaxNumTasks);
         }
+
+        // Remove any tasks that belong to currently quiet profiles
+        final int[] profileUserIds = mUserController.getCurrentProfileIds();
+        mTmpQuietProfileUserIds.clear();
+        for (int userId : profileUserIds) {
+            final UserInfo userInfo = mUserController.getUserInfo(userId);
+            if (userInfo.isManagedProfile() && userInfo.isQuietModeEnabled()) {
+                mTmpQuietProfileUserIds.put(userId, true);
+            }
+            if (DEBUG_RECENTS_TRIM_TASKS) Slog.d(TAG, "User: " + userInfo
+                    + " quiet=" + mTmpQuietProfileUserIds.get(userId));
+        }
+
+        // Remove any inactive tasks, calculate the latest set of visible tasks
+        int numVisibleTasks = 0;
+        for (int i = 0; i < mTasks.size();) {
+            final TaskRecord task = mTasks.get(i);
+
+            if (isActiveRecentTask(task, mTmpQuietProfileUserIds)) {
+                if (!mHasVisibleRecentTasks) {
+                    // Keep all active tasks if visible recent tasks is not supported
+                    i++;
+                    continue;
+                }
+
+                if (!isVisibleRecentTask(task)) {
+                    // Keep all active-but-invisible tasks
+                    i++;
+                    continue;
+                } else {
+                    numVisibleTasks++;
+                    if (isInVisibleRange(task, numVisibleTasks)) {
+                        // Keep visible tasks in range
+                        i++;
+                        continue;
+                    } else {
+                        // Fall through to trim visible tasks that are no longer in range
+                        if (DEBUG_RECENTS_TRIM_TASKS) Slog.d(TAG,
+                                "Trimming out-of-range visible task=" + task);
+                    }
+                }
+            } else {
+                // Fall through to trim inactive tasks
+                if (DEBUG_RECENTS_TRIM_TASKS) Slog.d(TAG, "Trimming inactive task=" + task);
+            }
+
+            // Task is no longer active, trim it from the list
+            mTasks.remove(task);
+            notifyTaskRemoved(task, TRIMMED);
+            notifyTaskPersisterLocked(task, false /* flush */);
+        }
+    }
+
+    /**
+     * @return whether the given task should be considered active.
+     */
+    private boolean isActiveRecentTask(TaskRecord task, SparseBooleanArray quietProfileUserIds) {
+        if (DEBUG_RECENTS_TRIM_TASKS) Slog.d(TAG, "isActiveRecentTask: task=" + task
+                + " globalMax=" + mGlobalMaxNumTasks);
+
+        if (quietProfileUserIds.get(task.userId)) {
+            // Quiet profile user's tasks are never active
+            if (DEBUG_RECENTS_TRIM_TASKS) Slog.d(TAG, "\tisQuietProfileTask=true");
+            return false;
+        }
+
+        if (task.mAffiliatedTaskId != INVALID_TASK_ID && task.mAffiliatedTaskId != task.taskId) {
+            // Keep the task active if its affiliated task is also active
+            final TaskRecord affiliatedTask = getTask(task.mAffiliatedTaskId);
+            if (affiliatedTask != null) {
+                if (!isActiveRecentTask(affiliatedTask, quietProfileUserIds)) {
+                    if (DEBUG_RECENTS_TRIM_TASKS) Slog.d(TAG,
+                            "\taffiliatedWithTask=" + affiliatedTask + " is not active");
+                    return false;
+                }
+            }
+        }
+
+        // All other tasks are considered active
+        return true;
+    }
+
+    /**
+     * @return whether the given active task should be presented to the user through SystemUI.
+     */
+    private boolean isVisibleRecentTask(TaskRecord task) {
+        if (DEBUG_RECENTS_TRIM_TASKS) Slog.d(TAG, "isVisibleRecentTask: task=" + task
+                + " minVis=" + mMinNumVisibleTasks + " maxVis=" + mMaxNumVisibleTasks
+                + " sessionDuration=" + mActiveTasksSessionDurationMs
+                + " inactiveDuration=" + task.getInactiveDuration()
+                + " activityType=" + task.getActivityType()
+                + " windowingMode=" + task.getWindowingMode());
+
+        // Ignore certain activity types completely
+        switch (task.getActivityType()) {
+            case ACTIVITY_TYPE_HOME:
+            case ACTIVITY_TYPE_RECENTS:
+                return false;
+        }
+
+        // Ignore certain windowing modes
+        switch (task.getWindowingMode()) {
+            case WINDOWING_MODE_PINNED:
+                return false;
+            case WINDOWING_MODE_SPLIT_SCREEN_PRIMARY:
+                if (DEBUG_RECENTS_TRIM_TASKS) Slog.d(TAG, "\ttop=" + task.getStack().topTask());
+                final ActivityStack stack = task.getStack();
+                if (stack != null && stack.topTask() == task) {
+                    // Only the non-top task of the primary split screen mode is visible
+                    return false;
+                }
+        }
+
+        return true;
+    }
+
+    /**
+     * @return whether the given visible task is within the policy range.
+     */
+    private boolean isInVisibleRange(TaskRecord task, int numVisibleTasks) {
+        // Keep the last most task even if it is excluded from recents
+        final boolean isExcludeFromRecents =
+                (task.getBaseIntent().getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
+                        == Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
+        if (isExcludeFromRecents) {
+            if (DEBUG_RECENTS_TRIM_TASKS) Slog.d(TAG, "\texcludeFromRecents=true");
+            return numVisibleTasks == 1;
+        }
+
+        if (mMinNumVisibleTasks >= 0 && numVisibleTasks <= mMinNumVisibleTasks) {
+            // Always keep up to the min number of recent tasks, after that fall through to the
+            // checks below
+            return true;
+        }
+
+        if (mMaxNumVisibleTasks >= 0) {
+            // Always keep up to the max number of recent tasks, but return false afterwards
+            return numVisibleTasks <= mMaxNumVisibleTasks;
+        }
+
+        if (mActiveTasksSessionDurationMs > 0) {
+            // Keep the task if the inactive time is within the session window, this check must come
+            // after the checks for the min/max visible task range
+            if (task.getInactiveDuration() <= mActiveTasksSessionDurationMs) {
+                return true;
+            }
+        }
+
+        return false;
     }
 
     /**
@@ -864,11 +1091,16 @@
             return;
         }
 
+        // There is a similar task that will be removed for the addition of {@param task}, but it
+        // can be the same task, and if so, the task will be re-added in add(), so skip the
+        // callbacks here.
         final TaskRecord removedTask = mTasks.remove(removeIndex);
         if (removedTask != task) {
             notifyTaskRemoved(removedTask, TRIMMED);
+            if (DEBUG_RECENTS_TRIM_TASKS) Slog.d(TAG, "Trimming task=" + removedTask
+                    + " for addition of task=" + task);
         }
-        notifyTaskPersisterLocked(removedTask, false);
+        notifyTaskPersisterLocked(removedTask, false /* flush */);
     }
 
     /**
@@ -1004,7 +1236,7 @@
             notifyTaskPersisterLocked(last, false);
         }
 
-        // Insert the group back into mRecentTasks at start.
+        // Insert the group back into mTmpTasks at start.
         mTasks.addAll(start, mTmpRecents);
         mTmpRecents.clear();
 
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 b4db4a3..20077f3 100644
--- a/services/tests/servicestests/src/com/android/server/am/ActivityTestsBase.java
+++ b/services/tests/servicestests/src/com/android/server/am/ActivityTestsBase.java
@@ -103,11 +103,11 @@
 
     protected static TaskRecord createTask(ActivityStackSupervisor supervisor,
             ComponentName component, ActivityStack stack) {
-        return createTask(supervisor, component, 0 /* flags */, stack);
+        return createTask(supervisor, component, 0 /* flags */, 0 /* taskId */, stack);
     }
 
     protected static TaskRecord createTask(ActivityStackSupervisor supervisor,
-            ComponentName component, int flags, ActivityStack stack) {
+            ComponentName component, int flags, int taskId, ActivityStack stack) {
         final ActivityInfo aInfo = new ActivityInfo();
         aInfo.applicationInfo = new ApplicationInfo();
         aInfo.applicationInfo.packageName = component.getPackageName();
@@ -116,8 +116,8 @@
         intent.setComponent(component);
         intent.setFlags(flags);
 
-        final TaskRecord task = new TaskRecord(supervisor.mService, 0, aInfo, intent /*intent*/,
-                null /*_taskDescription*/);
+        final TaskRecord task = new TaskRecord(supervisor.mService, taskId, aInfo,
+                intent /*intent*/, null /*_taskDescription*/);
         supervisor.setFocusStackUnchecked("test", stack);
         stack.addTask(task, true, "creating test task");
         task.setStack(stack);
diff --git a/services/tests/servicestests/src/com/android/server/am/RecentTasksTest.java b/services/tests/servicestests/src/com/android/server/am/RecentTasksTest.java
index f40b646..e607228 100644
--- a/services/tests/servicestests/src/com/android/server/am/RecentTasksTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/RecentTasksTest.java
@@ -18,18 +18,27 @@
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
 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 static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
 
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.UserInfo;
+import android.os.Debug;
+import android.os.SystemClock;
 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.MutableLong;
 import android.util.SparseBooleanArray;
 
 import com.android.server.am.RecentTasks.Callbacks;
@@ -38,6 +47,8 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -53,6 +64,10 @@
 public class RecentTasksTest extends ActivityTestsBase {
     private static final int TEST_USER_0_ID = 0;
     private static final int TEST_USER_1_ID = 10;
+    private static final int TEST_QUIET_USER_ID = 20;
+    private static final UserInfo DEFAULT_USER_INFO = new UserInfo();
+    private static final UserInfo QUIET_USER_INFO = new UserInfo();
+    private static int LAST_TASK_ID = 1;
 
     private Context mContext = InstrumentationRegistry.getContext();
     private ActivityManagerService mService;
@@ -65,6 +80,29 @@
 
     private CallbacksRecorder mCallbacksRecorder;
 
+    class TestUserController extends UserController {
+        TestUserController(ActivityManagerService service) {
+            super(service);
+        }
+
+        @Override
+        int[] getCurrentProfileIds() {
+            return new int[] { TEST_USER_0_ID, TEST_QUIET_USER_ID };
+        }
+
+        @Override
+        UserInfo getUserInfo(int userId) {
+            switch (userId) {
+                case TEST_USER_0_ID:
+                case TEST_USER_1_ID:
+                    return DEFAULT_USER_INFO;
+                case TEST_QUIET_USER_ID:
+                    return QUIET_USER_INFO;
+            }
+            return null;
+        }
+    }
+
     @Before
     @Override
     public void setUp() throws Exception {
@@ -74,13 +112,17 @@
         mStack = mService.mStackSupervisor.getDefaultDisplay().createStack(
                 WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */);
         mTaskPersister = new TestTaskPersister(mContext.getFilesDir());
-        mRecentTasks = new RecentTasks(mService, mTaskPersister);
+        mRecentTasks = new RecentTasks(mService, mTaskPersister, new TestUserController(mService));
+        mRecentTasks.loadParametersFromResources(mContext.getResources());
         mCallbacksRecorder = new CallbacksRecorder();
         mRecentTasks.registerCallback(mCallbacksRecorder);
+        QUIET_USER_INFO.flags = UserInfo.FLAG_MANAGED_PROFILE | UserInfo.FLAG_QUIET_MODE;
 
         mTasks.add(createTask(".Task1"));
         mTasks.add(createTask(".Task2"));
         mTasks.add(createTask(".Task3"));
+        mTasks.add(createTask(".Task4"));
+        mTasks.add(createTask(".Task5"));
 
         mSameDocumentTasks.add(createDocumentTask(".DocumentTask1", null /* affinity */));
         mSameDocumentTasks.add(createDocumentTask(".DocumentTask1", null /* affinity */));
@@ -93,6 +135,7 @@
         mRecentTasks.add(mTasks.get(1));
         assertTrue(mCallbacksRecorder.added.contains(mTasks.get(0))
                 && mCallbacksRecorder.added.contains(mTasks.get(1)));
+        assertTrue(mCallbacksRecorder.trimmed.isEmpty());
         assertTrue(mCallbacksRecorder.removed.isEmpty());
         mCallbacksRecorder.clear();
 
@@ -100,6 +143,7 @@
         mRecentTasks.remove(mTasks.get(0));
         mRecentTasks.remove(mTasks.get(1));
         assertTrue(mCallbacksRecorder.added.isEmpty());
+        assertTrue(mCallbacksRecorder.trimmed.isEmpty());
         assertTrue(mCallbacksRecorder.removed.contains(mTasks.get(0)));
         assertTrue(mCallbacksRecorder.removed.contains(mTasks.get(1)));
         mCallbacksRecorder.clear();
@@ -112,6 +156,7 @@
         mRecentTasks.add(documentTask2);
         assertTrue(mCallbacksRecorder.added.contains(documentTask1));
         assertTrue(mCallbacksRecorder.added.contains(documentTask2));
+        assertTrue(mCallbacksRecorder.trimmed.contains(documentTask1));
         assertTrue(mCallbacksRecorder.removed.contains(documentTask1));
         mCallbacksRecorder.clear();
 
@@ -120,6 +165,7 @@
         mRecentTasks.add(mTasks.get(0));
         mRecentTasks.remove(mTasks.get(0));
         assertTrue(mCallbacksRecorder.added.isEmpty());
+        assertTrue(mCallbacksRecorder.trimmed.isEmpty());
         assertTrue(mCallbacksRecorder.removed.isEmpty());
     }
 
@@ -164,17 +210,140 @@
         assertFalse(arrayContainsUser(mRecentTasks.usersWithRecentsLoadedLocked(), TEST_USER_1_ID));
     }
 
+    @Test
+    public void testOrderedIteration() throws Exception {
+        MutableLong prevLastActiveTime = new MutableLong(0);
+        final ArrayList<TaskRecord> tasks = mRecentTasks.getRawTasks();
+        for (int i = 0; i < tasks.size(); i++) {
+            final TaskRecord task = tasks.get(i);
+            assertTrue(task.lastActiveTime >= prevLastActiveTime.value);
+            prevLastActiveTime.value = task.lastActiveTime;
+        }
+    }
+
+    @Test
+    public void testTrimToGlobalMaxNumRecents() throws Exception {
+        // Limit the global maximum number of recent tasks to a fixed size
+        mRecentTasks.setGlobalMaxNumTasks(2 /* globalMaxNumTasks */);
+
+        // Add N+1 tasks
+        mRecentTasks.add(mTasks.get(0));
+        mRecentTasks.add(mTasks.get(1));
+        mRecentTasks.add(mTasks.get(2));
+
+        // Ensure that the last task was trimmed as an inactive task
+        assertTrimmed(mTasks.get(0));
+    }
+
+    @Test
+    public void testTrimQuietProfileTasks() throws Exception {
+        TaskRecord qt1 = createTask(".QuietTask1", TEST_QUIET_USER_ID);
+        TaskRecord qt2 = createTask(".QuietTask2", TEST_QUIET_USER_ID);
+        mRecentTasks.add(qt1);
+        mRecentTasks.add(qt2);
+
+        mRecentTasks.add(mTasks.get(0));
+        mRecentTasks.add(mTasks.get(1));
+
+        // Ensure that the quiet user's tasks was trimmed once the new tasks were added
+        assertTrimmed(qt1, qt2);
+    }
+
+    @Test
+    public void testSessionDuration() throws Exception {
+        mRecentTasks.setParameters(-1 /* min */, -1 /* max */, 50 /* ms */);
+
+        TaskRecord t1 = createTask(".Task1");
+        t1.touchActiveTime();
+        mRecentTasks.add(t1);
+
+        // Force a small sleep just beyond the session duration
+        SystemClock.sleep(75);
+
+        TaskRecord t2 = createTask(".Task2");
+        t2.touchActiveTime();
+        mRecentTasks.add(t2);
+
+        // Assert that the old task has been removed due to being out of the active session
+        assertTrimmed(t1);
+    }
+
+    @Test
+    public void testVisibleTasks_excludedFromRecents() throws Exception {
+        mRecentTasks.setParameters(-1 /* min */, 4 /* max */, -1 /* ms */);
+
+        TaskRecord excludedTask1 = createTask(".ExcludedTask1", FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS,
+                TEST_USER_0_ID);
+        TaskRecord excludedTask2 = createTask(".ExcludedTask2", FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS,
+                TEST_USER_0_ID);
+
+        mRecentTasks.add(excludedTask1);
+        mRecentTasks.add(mTasks.get(0));
+        mRecentTasks.add(mTasks.get(1));
+        mRecentTasks.add(mTasks.get(2));
+        mRecentTasks.add(excludedTask2);
+
+        // The last excluded task should be trimmed, while the first-most excluded task should not
+        assertTrimmed(excludedTask1);
+    }
+
+    @Test
+    public void testVisibleTasks_minNum() throws Exception {
+        mRecentTasks.setParameters(5 /* min */, -1 /* max */, 25 /* ms */);
+
+        for (int i = 0; i < 4; i++) {
+            final TaskRecord task = mTasks.get(i);
+            task.touchActiveTime();
+            mRecentTasks.add(task);
+        }
+
+        // Force a small sleep just beyond the session duration
+        SystemClock.sleep(50);
+
+        // Add a new task to trigger tasks to be trimmed
+        mRecentTasks.add(mTasks.get(4));
+
+        // Ensure that there are a minimum number of tasks regardless of session length
+        assertTrue(mCallbacksRecorder.trimmed.isEmpty());
+        assertTrue(mCallbacksRecorder.removed.isEmpty());
+    }
+
+    @Test
+    public void testVisibleTasks_maxNum() throws Exception {
+        mRecentTasks.setParameters(-1 /* min */, 3 /* max */, -1 /* ms */);
+
+        for (int i = 0; i < 5; i++) {
+            final TaskRecord task = mTasks.get(i);
+            task.touchActiveTime();
+            mRecentTasks.add(task);
+        }
+
+        // Ensure that only the last number of max tasks are kept
+        assertTrimmed(mTasks.get(0), mTasks.get(1));
+    }
+
     private ComponentName createComponent(String className) {
         return new ComponentName(mContext.getPackageName(), className);
     }
 
     private TaskRecord createTask(String className) {
-        return createTask(mService.mStackSupervisor, createComponent(className), mStack);
+        return createTask(className, TEST_USER_0_ID);
+    }
+
+    private TaskRecord createTask(String className, int userId) {
+        return createTask(className, 0 /* flags */, userId);
+    }
+
+    private TaskRecord createTask(String className, int flags, int userId) {
+        TaskRecord task = createTask(mService.mStackSupervisor, createComponent(className), flags,
+                LAST_TASK_ID++, mStack);
+        task.userId = userId;
+        task.touchActiveTime();
+        return task;
     }
 
     private TaskRecord createDocumentTask(String className, String affinity) {
-        TaskRecord task = createTask(mService.mStackSupervisor, createComponent(className),
-                FLAG_ACTIVITY_NEW_DOCUMENT, mStack);
+        TaskRecord task = createTask(className, FLAG_ACTIVITY_NEW_DOCUMENT, TEST_USER_0_ID);
         task.affinity = affinity;
         return task;
     }
@@ -184,12 +353,27 @@
         return Arrays.binarySearch(userIds, targetUserId) >= 0;
     }
 
+    private void assertTrimmed(TaskRecord... tasks) {
+        final ArrayList<TaskRecord> trimmed = mCallbacksRecorder.trimmed;
+        final ArrayList<TaskRecord> removed = mCallbacksRecorder.removed;
+        assertTrue("Expected " + tasks.length + " trimmed tasks, got " + trimmed.size(),
+                trimmed.size() == tasks.length);
+        assertTrue("Expected " + tasks.length + " removed tasks, got " + removed.size(),
+                removed.size() == tasks.length);
+        for (TaskRecord task : tasks) {
+            assertTrue("Expected trimmed task: " + task, trimmed.contains(task));
+            assertTrue("Expected removed task: " + task, removed.contains(task));
+        }
+    }
+
     private static class CallbacksRecorder implements Callbacks {
         ArrayList<TaskRecord> added = new ArrayList<>();
+        ArrayList<TaskRecord> trimmed = new ArrayList<>();
         ArrayList<TaskRecord> removed = new ArrayList<>();
 
         void clear() {
             added.clear();
+            trimmed.clear();
             removed.clear();
         }
 
@@ -199,7 +383,10 @@
         }
 
         @Override
-        public void onRecentTaskRemoved(TaskRecord task) {
+        public void onRecentTaskRemoved(TaskRecord task, boolean wasTrimmed) {
+            if (wasTrimmed) {
+                trimmed.add(task);
+            }
             removed.add(task);
         }
     }