diff options
6 files changed, 331 insertions, 22 deletions
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index e2d868f2c45e..da9ea8359854 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -222,6 +222,13 @@ public class ActivityOptions { private static final String KEY_AVOID_MOVE_TO_FRONT = "android.activity.avoidMoveToFront"; /** + * See {@link #setFreezeRecentTasksReordering()}. + * @hide + */ + private static final String KEY_FREEZE_RECENT_TASKS_REORDERING = + "android.activity.freezeRecentTasksReordering"; + + /** * Where the split-screen-primary stack should be positioned. * @hide */ @@ -324,6 +331,7 @@ public class ActivityOptions { private boolean mTaskOverlay; private boolean mTaskOverlayCanResume; private boolean mAvoidMoveToFront; + private boolean mFreezeRecentTasksReordering; private AppTransitionAnimationSpec mAnimSpecs[]; private int mRotationAnimationHint = -1; private Bundle mAppVerificationBundle; @@ -946,6 +954,7 @@ public class ActivityOptions { mTaskOverlay = opts.getBoolean(KEY_TASK_OVERLAY, false); mTaskOverlayCanResume = opts.getBoolean(KEY_TASK_OVERLAY_CAN_RESUME, false); mAvoidMoveToFront = opts.getBoolean(KEY_AVOID_MOVE_TO_FRONT, false); + mFreezeRecentTasksReordering = opts.getBoolean(KEY_FREEZE_RECENT_TASKS_REORDERING, false); mSplitScreenCreateMode = opts.getInt(KEY_SPLIT_SCREEN_CREATE_MODE, SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT); mDisallowEnterPictureInPictureWhileLaunching = opts.getBoolean( @@ -1304,6 +1313,24 @@ public class ActivityOptions { return mAvoidMoveToFront; } + /** + * Sets whether the launch of this activity should freeze the recent task list reordering until + * the next user interaction or timeout. This flag is only applied when starting an activity + * in recents. + * @hide + */ + public void setFreezeRecentTasksReordering() { + mFreezeRecentTasksReordering = true; + } + + /** + * @return whether the launch of this activity should freeze the recent task list reordering + * @hide + */ + public boolean freezeRecentTasksReordering() { + return mFreezeRecentTasksReordering; + } + /** @hide */ public int getSplitScreenCreateMode() { return mSplitScreenCreateMode; @@ -1502,6 +1529,9 @@ public class ActivityOptions { if (mAvoidMoveToFront) { b.putBoolean(KEY_AVOID_MOVE_TO_FRONT, mAvoidMoveToFront); } + if (mFreezeRecentTasksReordering) { + b.putBoolean(KEY_FREEZE_RECENT_TASKS_REORDERING, mFreezeRecentTasksReordering); + } if (mSplitScreenCreateMode != SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT) { b.putInt(KEY_SPLIT_SCREEN_CREATE_MODE, mSplitScreenCreateMode); } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityOptionsCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityOptionsCompat.java index ea6fb48c67e7..7b39ba3d2759 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityOptionsCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityOptionsCompat.java @@ -78,4 +78,12 @@ public abstract class ActivityOptionsCompat { } }); } + + /** + * Sets the flag to freeze the recents task list reordering as a part of launching the activity. + */ + public static ActivityOptions setFreezeRecentTasksList(ActivityOptions opts) { + opts.setFreezeRecentTasksReordering(); + return opts; + } } diff --git a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java index 9a5ec2ab17dc..7cea4587bec5 100644 --- a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java @@ -2736,6 +2736,9 @@ public class ActivityStackSupervisor implements RecentTasks.Callbacks { if (activityOptions != null) { activityType = activityOptions.getLaunchActivityType(); windowingMode = activityOptions.getLaunchWindowingMode(); + if (activityOptions.freezeRecentTasksReordering()) { + mRecentTasks.setFreezeTaskListReordering(); + } } if (activityType == ACTIVITY_TYPE_HOME || activityType == ACTIVITY_TYPE_RECENTS) { throw new IllegalArgumentException("startActivityFromRecents: Task " diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 33573932199c..bcfbefe95479 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -41,12 +41,14 @@ import static android.view.View.GONE; import static android.view.WindowManager.DOCKED_BOTTOM; import static android.view.WindowManager.DOCKED_INVALID; import static android.view.WindowManager.DOCKED_TOP; +import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; import static android.view.WindowManager.LayoutParams.FLAG_SECURE; import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; import static android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH; +import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW; import static android.view.WindowManager.LayoutParams.NEEDS_MENU_SET_TRUE; import static android.view.WindowManager.LayoutParams.NEEDS_MENU_UNSET; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD; @@ -182,6 +184,8 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.ToBooleanFunction; import com.android.internal.util.function.TriConsumer; +import com.android.internal.util.function.pooled.PooledConsumer; +import com.android.internal.util.function.pooled.PooledLambda; import com.android.server.AnimationThread; import com.android.server.policy.WindowManagerPolicy; import com.android.server.wm.utils.DisplayRotationUtil; @@ -887,6 +891,10 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo mTapDetector = new TaskTapPointerEventListener(mWmService, this); registerPointerEventListener(mTapDetector); registerPointerEventListener(mWmService.mMousePositionTracker); + if (mWmService.mAtmService.getRecentTasks() != null) { + registerPointerEventListener( + mWmService.mAtmService.getRecentTasks().getInputListener()); + } mDisplayPolicy = new DisplayPolicy(service, this); mDisplayRotation = new DisplayRotation(service, this); @@ -2381,6 +2389,27 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo } /** + * Returns true if the input point is within an app window. + */ + boolean pointWithinAppWindow(int x, int y) { + final int[] targetWindowType = {-1}; + final Consumer fn = PooledLambda.obtainConsumer((w, nonArg) -> { + if (targetWindowType[0] != -1) { + return; + } + + if (w.isOnScreen() && w.isVisibleLw() && w.getFrameLw().contains(x, y)) { + targetWindowType[0] = w.mAttrs.type; + return; + } + }, PooledLambda.__(WindowState.class), mTmpRect); + forAllWindows(fn, true /* traverseTopToBottom */); + ((PooledConsumer) fn).recycle(); + return FIRST_APPLICATION_WINDOW <= targetWindowType[0] + && targetWindowType[0] <= LAST_APPLICATION_WINDOW; + } + + /** * Find the task whose outside touch area (for resizing) (x, y) falls within. * Returns null if the touch doesn't fall into a resizing area. */ diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java index 0480d438e3cd..d69ae3d5c3c0 100644 --- a/services/core/java/com/android/server/wm/RecentTasks.java +++ b/services/core/java/com/android/server/wm/RecentTasks.java @@ -60,6 +60,7 @@ import android.os.Bundle; import android.os.Environment; import android.os.IBinder; import android.os.RemoteException; +import android.os.SystemClock; import android.os.SystemProperties; import android.os.UserHandle; import android.text.TextUtils; @@ -67,8 +68,11 @@ import android.util.ArraySet; import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; +import android.view.MotionEvent; +import android.view.WindowManagerPolicyConstants.PointerEventListener; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.function.pooled.PooledLambda; import com.android.server.am.ActivityManagerService; import com.google.android.collect.Sets; @@ -109,8 +113,11 @@ class RecentTasks { 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; + // The duration of time after freezing the recent tasks list where getRecentTasks() will return + // a stable ordering of the tasks. Upon the next call to getRecentTasks() beyond this duration, + // the task list will be unfrozen and committed (the current top task will be moved to the + // front of the list) + private static final long FREEZE_TASK_LIST_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(5); // Comparator to sort by taskId private static final Comparator<TaskRecord> TASK_ID_COMPARATOR = @@ -174,12 +181,45 @@ class RecentTasks { private int mMaxNumVisibleTasks; private long mActiveTasksSessionDurationMs; + // When set, the task list will not be reordered as tasks within the list are moved to the + // front. Newly created tasks, or tasks that are removed from the list will continue to change + // the list. This does not affect affiliated tasks. + private boolean mFreezeTaskListReordering; + private long mFreezeTaskListReorderingTime; + private long mFreezeTaskListTimeoutMs = FREEZE_TASK_LIST_TIMEOUT_MS; + // 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(); + // TODO(b/127498985): This is currently a rough heuristic for interaction inside an app + private final PointerEventListener mListener = new PointerEventListener() { + @Override + public void onPointerEvent(MotionEvent ev) { + if (!mFreezeTaskListReordering || ev.getAction() != MotionEvent.ACTION_DOWN) { + // Skip if we aren't freezing or starting a gesture + return; + } + int displayId = ev.getDisplayId(); + int x = (int) ev.getX(); + int y = (int) ev.getY(); + mService.mH.post(PooledLambda.obtainRunnable((nonArg) -> { + synchronized (mService.mGlobalLock) { + // Unfreeze the task list once we touch down in a task + final RootActivityContainer rac = mService.mRootActivityContainer; + final DisplayContent dc = rac.getActivityDisplay(displayId).mDisplayContent; + if (dc.pointWithinAppWindow(x, y)) { + final ActivityStack stack = mService.getTopDisplayFocusedStack(); + final TaskRecord topTask = stack != null ? stack.topTask() : null; + resetFreezeTaskListReordering(topTask); + } + } + }, null).recycleOnUse()); + } + }; + @VisibleForTesting RecentTasks(ActivityTaskManagerService service, TaskPersister taskPersister) { mService = service; @@ -214,6 +254,73 @@ class RecentTasks { mGlobalMaxNumTasks = globalMaxNumTasks; } + @VisibleForTesting + void setFreezeTaskListTimeoutParams(long reorderingTime, long timeoutMs) { + mFreezeTaskListReorderingTime = reorderingTime; + mFreezeTaskListTimeoutMs = timeoutMs; + } + + PointerEventListener getInputListener() { + return mListener; + } + + /** + * Freezes the current recent task list order until either a user interaction with the current + * app, or a timeout occurs. + */ + void setFreezeTaskListReordering() { + // Always update the reordering time when this is called to ensure that the timeout + // is reset + mFreezeTaskListReordering = true; + mFreezeTaskListReorderingTime = SystemClock.elapsedRealtime(); + } + + /** + * Commits the frozen recent task list order, moving the provided {@param topTask} to the + * front of the list. + */ + void resetFreezeTaskListReordering(TaskRecord topTask) { + if (!mFreezeTaskListReordering) { + return; + } + + // Once we end freezing the task list, reset the existing task order to the stable state + mFreezeTaskListReordering = false; + + // If the top task is provided, then restore the top task to the front of the list + if (topTask != null) { + mTasks.remove(topTask); + mTasks.add(0, topTask); + } + + // Resume trimming tasks + trimInactiveRecentTasks(); + } + + /** + * Resets the frozen recent task list order if the timeout has passed. This should be called + * before we need to iterate the task list in order (either for purposes of returning the list + * to SystemUI or if we need to trim tasks in order) + */ + void resetFreezeTaskListReorderingOnTimeout() { + // Unfreeze the recent task list if the time heuristic has passed + if (mFreezeTaskListReorderingTime + > (SystemClock.elapsedRealtime() - mFreezeTaskListTimeoutMs)) { + return; + } + + final ActivityStack focusedStack = mService.getTopDisplayFocusedStack(); + final TaskRecord topTask = focusedStack != null + ? focusedStack.topTask() + : null; + resetFreezeTaskListReordering(topTask); + } + + @VisibleForTesting + boolean isFreezeTaskListReorderingSet() { + return mFreezeTaskListReordering; + } + /** * Loads the parameters from the system resources. */ @@ -351,7 +458,8 @@ class RecentTasks { } Slog.i(TAG, "Loading recents for user " + userId + " into memory."); - mTasks.addAll(mTaskPersister.restoreTasksForUserLocked(userId, preaddedTasks)); + List<TaskRecord> tasks = mTaskPersister.restoreTasksForUserLocked(userId, preaddedTasks); + mTasks.addAll(tasks); cleanupLocked(userId); mUsersWithRecentsLoaded.put(userId, true); @@ -746,11 +854,10 @@ class RecentTasks { getDetailedTasks, userId, callingUid)); } - /** * @return the list of recent tasks for presentation. */ - ArrayList<ActivityManager.RecentTaskInfo> getRecentTasksImpl(int maxNum, int flags, + private ArrayList<ActivityManager.RecentTaskInfo> getRecentTasksImpl(int maxNum, int flags, boolean getTasksAllowed, boolean getDetailedTasks, int userId, int callingUid) { final boolean withExcluded = (flags & RECENT_WITH_EXCLUDED) != 0; @@ -763,6 +870,9 @@ class RecentTasks { final Set<Integer> includedUsers = getProfileIds(userId); includedUsers.add(Integer.valueOf(userId)); + // Check if the frozen task list has timed out + resetFreezeTaskListReorderingOnTimeout(); + final ArrayList<ActivityManager.RecentTaskInfo> res = new ArrayList<>(); final int size = mTasks.size(); int numVisibleTasks = 0; @@ -946,24 +1056,20 @@ class RecentTasks { 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); + if (!isAffiliated) { + if (!mFreezeTaskListReordering) { + // Simple case: this is not an affiliated task, so we just move it to the + // front unless overridden by the provided activity options + mTasks.remove(taskIndex); + mTasks.add(0, task); + + if (DEBUG_RECENTS) { + Slog.d(TAG_RECENTS, "addRecent: moving to top " + task + + " from " + taskIndex); + } + } 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); @@ -1063,6 +1169,11 @@ class RecentTasks { * Trims the recents task list to the global max number of recents. */ private void trimInactiveRecentTasks() { + if (mFreezeTaskListReordering) { + // Defer trimming inactive recent tasks until we are unfrozen + return; + } + int recentsCount = mTasks.size(); // Remove from the end of the list until we reach the max number of recents @@ -1086,7 +1197,7 @@ class RecentTasks { + " quiet=" + mTmpQuietProfileUserIds.get(userId)); } - // Remove any inactive tasks, calculate the latest set of visible tasks + // 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); @@ -1300,6 +1411,11 @@ class RecentTasks { * list (if any). */ private int findRemoveIndexForAddTask(TaskRecord task) { + if (mFreezeTaskListReordering) { + // Defer removing tasks due to the addition of new tasks until the task list is unfrozen + return -1; + } + final int recentsCount = mTasks.size(); final Intent intent = task.intent; final boolean document = intent != null && intent.isDocument(); @@ -1536,6 +1652,9 @@ class RecentTasks { pw.println("ACTIVITY MANAGER RECENT TASKS (dumpsys activity recents)"); pw.println("mRecentsUid=" + mRecentsUid); pw.println("mRecentsComponent=" + mRecentsComponent); + pw.println("mFreezeTaskListReordering=" + mFreezeTaskListReordering); + pw.println("mFreezeTaskListReorderingTime (time since)=" + + (SystemClock.elapsedRealtime() - mFreezeTaskListReorderingTime) + "ms"); if (mTasks.isEmpty()) { return; } diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java index fc1eb1c57198..68e7470b8763 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java @@ -583,6 +583,114 @@ public class RecentTasksTest extends ActivityTestsBase { } @Test + public void testFreezeTaskListOrder_reorderExistingTask() { + // Add some tasks + mRecentTasks.add(mTasks.get(0)); + mRecentTasks.add(mTasks.get(1)); + mRecentTasks.add(mTasks.get(2)); + mRecentTasks.add(mTasks.get(3)); + mRecentTasks.add(mTasks.get(4)); + mCallbacksRecorder.clear(); + + // Freeze the list + mRecentTasks.setFreezeTaskListReordering(); + assertTrue(mRecentTasks.isFreezeTaskListReorderingSet()); + + // Relaunch a few tasks + mRecentTasks.add(mTasks.get(3)); + mRecentTasks.add(mTasks.get(2)); + + // Commit the task ordering with a specific task focused + mRecentTasks.resetFreezeTaskListReordering(mTasks.get(2)); + assertFalse(mRecentTasks.isFreezeTaskListReorderingSet()); + + // Ensure that the order of the task list is the same as before, but with the focused task + // at the front + assertRecentTasksOrder(mTasks.get(2), + mTasks.get(4), + mTasks.get(3), + mTasks.get(1), + mTasks.get(0)); + + assertThat(mCallbacksRecorder.mAdded).isEmpty(); + assertThat(mCallbacksRecorder.mTrimmed).isEmpty(); + assertThat(mCallbacksRecorder.mRemoved).isEmpty(); + } + + @Test + public void testFreezeTaskListOrder_addRemoveTasks() { + // Add some tasks + mRecentTasks.add(mTasks.get(0)); + mRecentTasks.add(mTasks.get(1)); + mRecentTasks.add(mTasks.get(2)); + mCallbacksRecorder.clear(); + + // Freeze the list + mRecentTasks.setFreezeTaskListReordering(); + assertTrue(mRecentTasks.isFreezeTaskListReorderingSet()); + + // Add and remove some tasks + mRecentTasks.add(mTasks.get(3)); + mRecentTasks.add(mTasks.get(4)); + mRecentTasks.remove(mTasks.get(0)); + mRecentTasks.remove(mTasks.get(1)); + + // Unfreeze the list + mRecentTasks.resetFreezeTaskListReordering(null); + assertFalse(mRecentTasks.isFreezeTaskListReorderingSet()); + + // Ensure that the order of the task list accounts for the added and removed tasks (added + // at the end) + assertRecentTasksOrder(mTasks.get(4), + mTasks.get(3), + mTasks.get(2)); + + assertThat(mCallbacksRecorder.mAdded).hasSize(2); + assertThat(mCallbacksRecorder.mAdded).contains(mTasks.get(3)); + assertThat(mCallbacksRecorder.mAdded).contains(mTasks.get(4)); + assertThat(mCallbacksRecorder.mRemoved).hasSize(2); + assertThat(mCallbacksRecorder.mRemoved).contains(mTasks.get(0)); + assertThat(mCallbacksRecorder.mRemoved).contains(mTasks.get(1)); + } + + @Test + public void testFreezeTaskListOrder_timeout() { + // Add some tasks + mRecentTasks.add(mTasks.get(0)); + mRecentTasks.add(mTasks.get(1)); + mRecentTasks.add(mTasks.get(2)); + mRecentTasks.add(mTasks.get(3)); + mRecentTasks.add(mTasks.get(4)); + + // Freeze the list + long freezeTime = SystemClock.elapsedRealtime(); + mRecentTasks.setFreezeTaskListReordering(); + assertTrue(mRecentTasks.isFreezeTaskListReorderingSet()); + + // Relaunch a few tasks + mRecentTasks.add(mTasks.get(2)); + mRecentTasks.add(mTasks.get(1)); + + // Override the freeze timeout params to simulate the timeout (simulate the freeze at 100ms + // ago with a timeout of 1ms) + mRecentTasks.setFreezeTaskListTimeoutParams(freezeTime - 100, 1); + + ActivityStack stack = mTasks.get(2).getStack(); + stack.moveToFront("", mTasks.get(2)); + doReturn(stack).when(mTestService.mRootActivityContainer).getTopDisplayFocusedStack(); + mRecentTasks.resetFreezeTaskListReorderingOnTimeout(); + assertFalse(mRecentTasks.isFreezeTaskListReorderingSet()); + + // Ensure that the order of the task list is the same as before, but with the focused task + // at the front + assertRecentTasksOrder(mTasks.get(2), + mTasks.get(4), + mTasks.get(3), + mTasks.get(1), + mTasks.get(0)); + } + + @Test public void testBackStackTasks_expectNoTrim() { mRecentTasks.setParameters(-1 /* min */, 1 /* max */, -1 /* ms */); @@ -721,6 +829,18 @@ public class RecentTasksTest extends ActivityTestsBase { true /* showRecents */)); } + /** + * Ensures that the recent tasks list is in the provided order. Note that the expected tasks + * should be ordered from least to most recent. + */ + private void assertRecentTasksOrder(TaskRecord... expectedTasks) { + ArrayList<TaskRecord> tasks = mRecentTasks.getRawTasks(); + assertTrue(expectedTasks.length == tasks.size()); + for (int i = 0; i < tasks.size(); i++) { + assertTrue(expectedTasks[i] == tasks.get(i)); + } + } + private void assertNotRestoreTask(Runnable action) { // Verify stack count doesn't change because task with fullscreen mode and standard type // would have its own stack. |