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);
}
}