Add shell recents interface
- Add a shell component and interface for Launcher to use to get recent
tasks including paired splits (pending integration w/ split controller)
Bug: 202740477
Test: atest WMShellUnitTests:com.android.wm.shell.recents.RecentTasksControllerTest
Signed-off-by: Winson Chung <winsonc@google.com>
Change-Id: I34d834061ed2391d89cf56d6a2fd262868707674
Merged-In: I34d834061ed2391d89cf56d6a2fd262868707674
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index 3ba1a34..73c2e8b 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -38,6 +38,7 @@
path: "src",
}
+// Sources that have no dependencies that can be used directly downstream of this library
filegroup {
name: "wm_shell_util-sources",
srcs: [
@@ -46,6 +47,7 @@
path: "src",
}
+// Aidls which can be used directly downstream of this library
filegroup {
name: "wm_shell-aidls",
srcs: [
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java
index e87b150..358553d7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java
@@ -24,6 +24,7 @@
import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController;
import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.pip.Pip;
+import com.android.wm.shell.recents.RecentTasksController;
import com.android.wm.shell.splitscreen.SplitScreenController;
import java.io.PrintWriter;
@@ -43,6 +44,7 @@
private final Optional<OneHandedController> mOneHandedOptional;
private final Optional<HideDisplayCutoutController> mHideDisplayCutout;
private final Optional<AppPairsController> mAppPairsOptional;
+ private final Optional<RecentTasksController> mRecentTasks;
private final ShellTaskOrganizer mShellTaskOrganizer;
private final ShellExecutor mMainExecutor;
private final HandlerImpl mImpl = new HandlerImpl();
@@ -55,8 +57,10 @@
Optional<OneHandedController> oneHandedOptional,
Optional<HideDisplayCutoutController> hideDisplayCutout,
Optional<AppPairsController> appPairsOptional,
+ Optional<RecentTasksController> recentTasks,
ShellExecutor mainExecutor) {
mShellTaskOrganizer = shellTaskOrganizer;
+ mRecentTasks = recentTasks;
mLegacySplitScreenOptional = legacySplitScreenOptional;
mSplitScreenOptional = splitScreenOptional;
mPipOptional = pipOptional;
@@ -85,6 +89,9 @@
pw.println();
pw.println();
mSplitScreenOptional.ifPresent(splitScreen -> splitScreen.dump(pw, ""));
+ pw.println();
+ pw.println();
+ mRecentTasks.ifPresent(recentTasks -> recentTasks.dump(pw, ""));
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java
index fa58fcd..f567877 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java
@@ -29,8 +29,8 @@
import com.android.wm.shell.freeform.FreeformTaskListener;
import com.android.wm.shell.fullscreen.FullscreenTaskListener;
import com.android.wm.shell.fullscreen.FullscreenUnfoldController;
-import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController;
import com.android.wm.shell.pip.phone.PipTouchHandler;
+import com.android.wm.shell.recents.RecentTasksController;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.startingsurface.StartingWindowController;
import com.android.wm.shell.transition.Transitions;
@@ -49,7 +49,6 @@
private final DragAndDropController mDragAndDropController;
private final ShellTaskOrganizer mShellTaskOrganizer;
private final Optional<BubbleController> mBubblesOptional;
- private final Optional<LegacySplitScreenController> mLegacySplitScreenOptional;
private final Optional<SplitScreenController> mSplitScreenOptional;
private final Optional<AppPairsController> mAppPairsOptional;
private final Optional<PipTouchHandler> mPipTouchHandlerOptional;
@@ -59,6 +58,7 @@
private final ShellExecutor mMainExecutor;
private final Transitions mTransitions;
private final StartingWindowController mStartingWindow;
+ private final Optional<RecentTasksController> mRecentTasks;
private final InitImpl mImpl = new InitImpl();
@@ -69,13 +69,13 @@
DragAndDropController dragAndDropController,
ShellTaskOrganizer shellTaskOrganizer,
Optional<BubbleController> bubblesOptional,
- Optional<LegacySplitScreenController> legacySplitScreenOptional,
Optional<SplitScreenController> splitScreenOptional,
Optional<AppPairsController> appPairsOptional,
Optional<PipTouchHandler> pipTouchHandlerOptional,
FullscreenTaskListener fullscreenTaskListener,
Optional<FullscreenUnfoldController> fullscreenUnfoldTransitionController,
Optional<Optional<FreeformTaskListener>> freeformTaskListenerOptional,
+ Optional<RecentTasksController> recentTasks,
Transitions transitions,
StartingWindowController startingWindow,
ShellExecutor mainExecutor) {
@@ -85,13 +85,13 @@
mDragAndDropController = dragAndDropController;
mShellTaskOrganizer = shellTaskOrganizer;
mBubblesOptional = bubblesOptional;
- mLegacySplitScreenOptional = legacySplitScreenOptional;
mSplitScreenOptional = splitScreenOptional;
mAppPairsOptional = appPairsOptional;
mFullscreenTaskListener = fullscreenTaskListener;
mPipTouchHandlerOptional = pipTouchHandlerOptional;
mFullscreenUnfoldController = fullscreenUnfoldTransitionController;
mFreeformTaskListenerOptional = freeformTaskListenerOptional.flatMap(f -> f);
+ mRecentTasks = recentTasks;
mTransitions = transitions;
mMainExecutor = mainExecutor;
mStartingWindow = startingWindow;
@@ -135,6 +135,7 @@
f, ShellTaskOrganizer.TASK_LISTENER_TYPE_FREEFORM));
mFullscreenUnfoldController.ifPresent(FullscreenUnfoldController::init);
+ mRecentTasks.ifPresent(RecentTasksController::init);
}
@ExternalThread
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
index 020ecb7..75bc461 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -51,6 +51,7 @@
import com.android.internal.util.FrameworkStatsLog;
import com.android.wm.shell.common.ScreenshotUtils;
import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.recents.RecentTasksController;
import com.android.wm.shell.sizecompatui.SizeCompatUIController;
import com.android.wm.shell.startingsurface.StartingWindowController;
@@ -59,6 +60,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
+import java.util.Optional;
import java.util.function.Consumer;
/**
@@ -150,20 +152,34 @@
@Nullable
private final SizeCompatUIController mSizeCompatUI;
+ @Nullable
+ private final Optional<RecentTasksController> mRecentTasks;
+
public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context) {
- this(null /* taskOrganizerController */, mainExecutor, context, null /* sizeCompatUI */);
+ this(null /* taskOrganizerController */, mainExecutor, context, null /* sizeCompatUI */,
+ Optional.empty() /* recentTasksController */);
}
public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context, @Nullable
SizeCompatUIController sizeCompatUI) {
- this(null /* taskOrganizerController */, mainExecutor, context, sizeCompatUI);
+ this(null /* taskOrganizerController */, mainExecutor, context, sizeCompatUI,
+ Optional.empty() /* recentTasksController */);
+ }
+
+ public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context, @Nullable
+ SizeCompatUIController sizeCompatUI,
+ Optional<RecentTasksController> recentTasks) {
+ this(null /* taskOrganizerController */, mainExecutor, context, sizeCompatUI,
+ recentTasks);
}
@VisibleForTesting
ShellTaskOrganizer(ITaskOrganizerController taskOrganizerController, ShellExecutor mainExecutor,
- Context context, @Nullable SizeCompatUIController sizeCompatUI) {
+ Context context, @Nullable SizeCompatUIController sizeCompatUI,
+ Optional<RecentTasksController> recentTasks) {
super(taskOrganizerController, mainExecutor);
mSizeCompatUI = sizeCompatUI;
+ mRecentTasks = recentTasks;
if (sizeCompatUI != null) {
sizeCompatUI.setSizeCompatUICallback(this);
}
@@ -401,6 +417,11 @@
// Notify the size compat UI if the listener or task info changed.
notifySizeCompatUI(taskInfo, newListener);
}
+ if (data.getTaskInfo().getWindowingMode() != taskInfo.getWindowingMode()) {
+ // Notify the recent tasks when a task changes windowing modes
+ mRecentTasks.ifPresent(recentTasks ->
+ recentTasks.onTaskWindowingModeChanged(taskInfo));
+ }
}
}
@@ -428,6 +449,8 @@
notifyLocusVisibilityIfNeeded(taskInfo);
// Pass null for listener to remove the size compat UI on this task if there is any.
notifySizeCompatUI(taskInfo, null /* taskListener */);
+ // Notify the recent tasks that a task has been removed
+ mRecentTasks.ifPresent(recentTasks -> recentTasks.onTaskRemoved(taskInfo));
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl
new file mode 100644
index 0000000..6e78fcb
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.recents;
+
+import com.android.wm.shell.recents.IRecentTasksListener;
+import com.android.wm.shell.util.GroupedRecentTaskInfo;
+
+/**
+ * Interface that is exposed to remote callers to fetch recent tasks.
+ */
+interface IRecentTasks {
+
+ /**
+ * Registers a recent tasks listener.
+ */
+ oneway void registerRecentTasksListener(in IRecentTasksListener listener) = 1;
+
+ /**
+ * Unregisters a recent tasks listener.
+ */
+ oneway void unregisterRecentTasksListener(in IRecentTasksListener listener) = 2;
+
+ /**
+ * Gets the set of recent tasks.
+ */
+ GroupedRecentTaskInfo[] getRecentTasks(int maxNum, int flags, int userId) = 3;
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
new file mode 100644
index 0000000..8efa428
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distshellributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.recents;
+
+/**
+ * Listener interface that Launcher attaches to SystemUI to get split-screen callbacks.
+ */
+oneway interface IRecentTasksListener {
+
+ /**
+ * Called when the set of recent tasks change.
+ */
+ void onRecentTasksChanged();
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java
new file mode 100644
index 0000000..a5748f6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.recents;
+
+import com.android.wm.shell.common.annotations.ExternalThread;
+
+/**
+ * Interface for interacting with the recent tasks.
+ */
+@ExternalThread
+public interface RecentTasks {
+ /**
+ * Returns a binder that can be passed to an external process to fetch recent tasks.
+ */
+ default IRecentTasks createExternalInterface() {
+ return null;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
new file mode 100644
index 0000000..836a6f6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.recents;
+
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+
+import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
+
+import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
+import android.app.TaskInfo;
+import android.content.Context;
+import android.os.RemoteException;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+
+import androidx.annotation.BinderThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.wm.shell.common.RemoteCallable;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SingleInstanceRemoteListener;
+import com.android.wm.shell.common.TaskStackListenerCallback;
+import com.android.wm.shell.common.TaskStackListenerImpl;
+import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.util.GroupedRecentTaskInfo;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Manages the recent task list from the system, caching it as necessary.
+ */
+public class RecentTasksController implements TaskStackListenerCallback,
+ RemoteCallable<RecentTasksController> {
+ private static final String TAG = RecentTasksController.class.getSimpleName();
+
+ private final Context mContext;
+ private final ShellExecutor mMainExecutor;
+ private final TaskStackListenerImpl mTaskStackListener;
+ private final RecentTasks mImpl = new RecentTasksImpl();
+
+ private final ArrayList<Runnable> mCallbacks = new ArrayList<>();
+ // Mapping of split task ids, mappings are symmetrical (ie. if t1 is the taskid of a task in a
+ // pair, then mSplitTasks[t1] = t2, and mSplitTasks[t2] = t1)
+ private final SparseIntArray mSplitTasks = new SparseIntArray();
+
+ /**
+ * Creates {@link RecentTasksController}, returns {@code null} if the feature is not
+ * supported.
+ */
+ @Nullable
+ public static RecentTasksController create(
+ Context context,
+ TaskStackListenerImpl taskStackListener,
+ @ShellMainThread ShellExecutor mainExecutor
+ ) {
+ if (!context.getResources().getBoolean(com.android.internal.R.bool.config_hasRecents)) {
+ return null;
+ }
+ return new RecentTasksController(context, taskStackListener, mainExecutor);
+ }
+
+ RecentTasksController(Context context, TaskStackListenerImpl taskStackListener,
+ ShellExecutor mainExecutor) {
+ mContext = context;
+ mTaskStackListener = taskStackListener;
+ mMainExecutor = mainExecutor;
+ }
+
+ public RecentTasks asRecentTasks() {
+ return mImpl;
+ }
+
+ public void init() {
+ mTaskStackListener.addListener(this);
+ }
+
+ /**
+ * Adds a split pair. This call does not validate the taskIds, only that they are not the same.
+ */
+ public void addSplitPair(int taskId1, int taskId2) {
+ if (taskId1 == taskId2) {
+ return;
+ }
+ // Remove any previous pairs
+ removeSplitPair(taskId1);
+ removeSplitPair(taskId2);
+ mSplitTasks.put(taskId1, taskId2);
+ mSplitTasks.put(taskId2, taskId1);
+ }
+
+ /**
+ * Removes a split pair.
+ */
+ public void removeSplitPair(int taskId) {
+ int pairedTaskId = mSplitTasks.get(taskId, INVALID_TASK_ID);
+ if (pairedTaskId != INVALID_TASK_ID) {
+ mSplitTasks.delete(taskId);
+ mSplitTasks.delete(pairedTaskId);
+ }
+ }
+
+ @Override
+ public Context getContext() {
+ return mContext;
+ }
+
+ @Override
+ public ShellExecutor getRemoteCallExecutor() {
+ return mMainExecutor;
+ }
+
+ @Override
+ public void onTaskStackChanged() {
+ notifyRecentTasksChanged();
+ }
+
+ @Override
+ public void onRecentTaskListUpdated() {
+ // In some cases immediately after booting, the tasks in the system recent task list may be
+ // loaded, but not in the active task hierarchy in the system. These tasks are displayed in
+ // overview, but removing them don't result in a onTaskStackChanged() nor a onTaskRemoved()
+ // callback (those are for changes to the active tasks), but the task list is still updated,
+ // so we should also invalidate the change id to ensure we load a new list instead of
+ // reusing a stale list.
+ notifyRecentTasksChanged();
+ }
+
+ public void onTaskRemoved(TaskInfo taskInfo) {
+ // Remove any split pairs associated with this task
+ removeSplitPair(taskInfo.taskId);
+ notifyRecentTasksChanged();
+ }
+
+ public void onTaskWindowingModeChanged(TaskInfo taskInfo) {
+ notifyRecentTasksChanged();
+ }
+
+ @VisibleForTesting
+ void notifyRecentTasksChanged() {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).run();
+ }
+ }
+
+ private void registerRecentTasksListener(Runnable listener) {
+ if (!mCallbacks.contains(listener)) {
+ mCallbacks.add(listener);
+ }
+ }
+
+ private void unregisterRecentTasksListener(Runnable listener) {
+ mCallbacks.remove(listener);
+ }
+
+ @VisibleForTesting
+ List<ActivityManager.RecentTaskInfo> getRawRecentTasks(int maxNum, int flags, int userId) {
+ return ActivityTaskManager.getInstance().getRecentTasks(maxNum, flags, userId);
+ }
+
+ @VisibleForTesting
+ ArrayList<GroupedRecentTaskInfo> getRecentTasks(int maxNum, int flags, int userId) {
+ // Note: the returned task list is from the most-recent to least-recent order
+ final List<ActivityManager.RecentTaskInfo> rawList = getRawRecentTasks(maxNum, flags,
+ userId);
+
+ // Make a mapping of task id -> task info
+ final SparseArray<ActivityManager.RecentTaskInfo> rawMapping = new SparseArray<>();
+ for (int i = 0; i < rawList.size(); i++) {
+ final ActivityManager.RecentTaskInfo taskInfo = rawList.get(i);
+ rawMapping.put(taskInfo.taskId, taskInfo);
+ }
+
+ // Pull out the pairs as we iterate back in the list
+ ArrayList<GroupedRecentTaskInfo> recentTasks = new ArrayList<>();
+ for (int i = 0; i < rawList.size(); i++) {
+ final ActivityManager.RecentTaskInfo taskInfo = rawList.get(i);
+ if (!rawMapping.contains(taskInfo.taskId)) {
+ // If it's not in the mapping, then it was already paired with another task
+ continue;
+ }
+
+ final int pairedTaskId = mSplitTasks.get(taskInfo.taskId);
+ if (pairedTaskId != INVALID_TASK_ID) {
+ final ActivityManager.RecentTaskInfo pairedTaskInfo = rawMapping.get(pairedTaskId);
+ rawMapping.remove(pairedTaskId);
+ recentTasks.add(new GroupedRecentTaskInfo(taskInfo, pairedTaskInfo));
+ } else {
+ recentTasks.add(new GroupedRecentTaskInfo(taskInfo));
+ }
+ }
+ return recentTasks;
+ }
+
+ public void dump(@NonNull PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + TAG);
+ ArrayList<GroupedRecentTaskInfo> recentTasks = getRecentTasks(Integer.MAX_VALUE,
+ ActivityManager.RECENT_IGNORE_UNAVAILABLE, ActivityManager.getCurrentUser());
+ for (int i = 0; i < recentTasks.size(); i++) {
+ pw.println(innerPrefix + recentTasks.get(i));
+ }
+ }
+
+ /**
+ * The interface for calls from outside the Shell, within the host process.
+ */
+ @ExternalThread
+ private class RecentTasksImpl implements RecentTasks {
+ private IRecentTasksImpl mIRecentTasks;
+
+ @Override
+ public IRecentTasks createExternalInterface() {
+ if (mIRecentTasks != null) {
+ mIRecentTasks.invalidate();
+ }
+ mIRecentTasks = new IRecentTasksImpl(RecentTasksController.this);
+ return mIRecentTasks;
+ }
+ }
+
+
+ /**
+ * The interface for calls from outside the host process.
+ */
+ @BinderThread
+ private static class IRecentTasksImpl extends IRecentTasks.Stub {
+ private RecentTasksController mController;
+ private final SingleInstanceRemoteListener<RecentTasksController,
+ IRecentTasksListener> mListener;
+ private final Runnable mRecentTasksListener =
+ new Runnable() {
+ @Override
+ public void run() {
+ mListener.call(l -> l.onRecentTasksChanged());
+ }
+ };
+
+ public IRecentTasksImpl(RecentTasksController controller) {
+ mController = controller;
+ mListener = new SingleInstanceRemoteListener<>(controller,
+ c -> c.registerRecentTasksListener(mRecentTasksListener),
+ c -> c.unregisterRecentTasksListener(mRecentTasksListener));
+ }
+
+ /**
+ * Invalidates this instance, preventing future calls from updating the controller.
+ */
+ void invalidate() {
+ mController = null;
+ }
+
+ @Override
+ public void registerRecentTasksListener(IRecentTasksListener listener)
+ throws RemoteException {
+ executeRemoteCallWithTaskPermission(mController, "registerRecentTasksListener",
+ (controller) -> mListener.register(listener));
+ }
+
+ @Override
+ public void unregisterRecentTasksListener(IRecentTasksListener listener)
+ throws RemoteException {
+ executeRemoteCallWithTaskPermission(mController, "unregisterRecentTasksListener",
+ (controller) -> mListener.unregister());
+ }
+
+ @Override
+ public GroupedRecentTaskInfo[] getRecentTasks(int maxNum, int flags, int userId)
+ throws RemoteException {
+ final GroupedRecentTaskInfo[][] out = new GroupedRecentTaskInfo[][]{null};
+ executeRemoteCallWithTaskPermission(mController, "getRecentTasks",
+ (controller) -> out[0] = controller.getRecentTasks(maxNum, flags, userId)
+ .toArray(new GroupedRecentTaskInfo[0]),
+ true /* blocking */);
+ return out[0];
+ }
+ }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.aidl
new file mode 100644
index 0000000..15797cd
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.util;
+
+parcelable GroupedRecentTaskInfo;
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java
new file mode 100644
index 0000000..0331ba1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.util;
+
+import android.app.ActivityManager;
+import android.app.WindowConfiguration;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Simple container for recent tasks. May contain either a single or pair of tasks.
+ */
+public class GroupedRecentTaskInfo implements Parcelable {
+ public @NonNull ActivityManager.RecentTaskInfo mTaskInfo1;
+ public @Nullable ActivityManager.RecentTaskInfo mTaskInfo2;
+
+ public GroupedRecentTaskInfo(@NonNull ActivityManager.RecentTaskInfo task1) {
+ this(task1, null);
+ }
+
+ public GroupedRecentTaskInfo(@NonNull ActivityManager.RecentTaskInfo task1,
+ @Nullable ActivityManager.RecentTaskInfo task2) {
+ mTaskInfo1 = task1;
+ mTaskInfo2 = task2;
+ }
+
+ GroupedRecentTaskInfo(Parcel parcel) {
+ mTaskInfo1 = parcel.readTypedObject(ActivityManager.RecentTaskInfo.CREATOR);
+ mTaskInfo2 = parcel.readTypedObject(ActivityManager.RecentTaskInfo.CREATOR);
+ }
+
+ @Override
+ public String toString() {
+ return "Task1: " + getTaskInfo(mTaskInfo1) + ", Task2: " + getTaskInfo(mTaskInfo2);
+ }
+
+ private String getTaskInfo(ActivityManager.RecentTaskInfo taskInfo) {
+ if (taskInfo == null) {
+ return null;
+ }
+ return "id=" + taskInfo.taskId
+ + " baseIntent=" + (taskInfo.baseIntent != null
+ ? taskInfo.baseIntent.getComponent()
+ : "null")
+ + " winMode=" + WindowConfiguration.windowingModeToString(
+ taskInfo.getWindowingMode());
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeTypedObject(mTaskInfo1, flags);
+ parcel.writeTypedObject(mTaskInfo2, flags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @android.annotation.NonNull Creator<GroupedRecentTaskInfo> CREATOR =
+ new Creator<GroupedRecentTaskInfo>() {
+ public GroupedRecentTaskInfo createFromParcel(Parcel source) {
+ return new GroupedRecentTaskInfo(source);
+ }
+ public GroupedRecentTaskInfo[] newArray(int size) {
+ return new GroupedRecentTaskInfo[size];
+ }
+ };
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
index d5acbbcf..1fcbf14 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
@@ -65,6 +65,7 @@
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
+import java.util.Optional;
/**
* Tests for the shell task organizer.
@@ -131,7 +132,7 @@
.when(mTaskOrganizerController).registerTaskOrganizer(any());
} catch (RemoteException e) {}
mOrganizer = spy(new ShellTaskOrganizer(mTaskOrganizerController, mTestExecutor, mContext,
- mSizeCompatUI));
+ mSizeCompatUI, Optional.empty()));
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java
index 5bdf831..6080f3a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java
@@ -26,6 +26,7 @@
import org.junit.After;
import org.junit.Before;
+import org.mockito.MockitoAnnotations;
/**
* Base class that does shell test case setup.
@@ -36,6 +37,7 @@
@Before
public void shellSetup() {
+ MockitoAnnotations.initMocks(this);
final Context context =
InstrumentationRegistry.getInstrumentation().getTargetContext();
final DisplayManager dm = context.getSystemService(DisplayManager.class);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
new file mode 100644
index 0000000..a1e1231
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.recents;
+
+import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import static java.lang.Integer.MAX_VALUE;
+
+import android.app.ActivityManager;
+import android.app.WindowConfiguration;
+import android.content.Context;
+import android.view.SurfaceControl;
+import android.window.TaskAppearedInfo;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.TaskStackListenerImpl;
+import com.android.wm.shell.util.GroupedRecentTaskInfo;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Optional;
+
+/**
+ * Tests for {@link RecentTasksController}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RecentTasksControllerTest extends ShellTestCase {
+
+ @Mock
+ private Context mContext;
+ @Mock
+ private TaskStackListenerImpl mTaskStackListener;
+
+ private ShellTaskOrganizer mShellTaskOrganizer;
+ private RecentTasksController mRecentTasksController;
+ private ShellExecutor mMainExecutor;
+
+ @Before
+ public void setUp() {
+ mMainExecutor = new TestShellExecutor();
+ mRecentTasksController = spy(new RecentTasksController(mContext, mTaskStackListener,
+ mMainExecutor));
+ mShellTaskOrganizer = new ShellTaskOrganizer(mMainExecutor, mContext,
+ null /* sizeCompatUI */, Optional.of(mRecentTasksController));
+ }
+
+ @Test
+ public void testGetRecentTasks() {
+ ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
+ ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
+ ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3);
+ setRawList(t1, t2, t3);
+
+ ArrayList<GroupedRecentTaskInfo> recentTasks = mRecentTasksController.getRecentTasks(
+ MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0);
+ assertGroupedTasksListEquals(recentTasks,
+ t1.taskId, -1,
+ t2.taskId, -1,
+ t3.taskId, -1);
+ }
+
+ @Test
+ public void testGetRecentTasks_withPairs() {
+ ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
+ ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
+ ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3);
+ ActivityManager.RecentTaskInfo t4 = makeTaskInfo(4);
+ ActivityManager.RecentTaskInfo t5 = makeTaskInfo(5);
+ ActivityManager.RecentTaskInfo t6 = makeTaskInfo(6);
+ setRawList(t1, t2, t3, t4, t5, t6);
+
+ // Mark a couple pairs [t2, t4], [t3, t5]
+ mRecentTasksController.addSplitPair(t2.taskId, t4.taskId);
+ mRecentTasksController.addSplitPair(t3.taskId, t5.taskId);
+
+ ArrayList<GroupedRecentTaskInfo> recentTasks = mRecentTasksController.getRecentTasks(
+ MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0);
+ assertGroupedTasksListEquals(recentTasks,
+ t1.taskId, -1,
+ t2.taskId, t4.taskId,
+ t3.taskId, t5.taskId,
+ t6.taskId, -1);
+ }
+
+ @Test
+ public void testRemovedTaskRemovesSplit() {
+ ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
+ ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
+ ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3);
+ setRawList(t1, t2, t3);
+
+ // Add a pair
+ mRecentTasksController.addSplitPair(t2.taskId, t3.taskId);
+ reset(mRecentTasksController);
+
+ // Remove one of the tasks and ensure the pair is removed
+ SurfaceControl mockLeash = mock(SurfaceControl.class);
+ ActivityManager.RunningTaskInfo rt2 = makeRunningTaskInfo(2);
+ mShellTaskOrganizer.onTaskAppeared(rt2, mockLeash);
+ mShellTaskOrganizer.onTaskVanished(rt2);
+
+ verify(mRecentTasksController).removeSplitPair(t2.taskId);
+ }
+
+ @Test
+ public void testTaskWindowingModeChangedNotifiesChange() {
+ ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
+ setRawList(t1);
+
+ // Remove one of the tasks and ensure the pair is removed
+ SurfaceControl mockLeash = mock(SurfaceControl.class);
+ ActivityManager.RunningTaskInfo rt2Fullscreen = makeRunningTaskInfo(2);
+ rt2Fullscreen.configuration.windowConfiguration.setWindowingMode(
+ WINDOWING_MODE_FULLSCREEN);
+ mShellTaskOrganizer.onTaskAppeared(rt2Fullscreen, mockLeash);
+
+ // Change the windowing mode and ensure the recent tasks change is notified
+ ActivityManager.RunningTaskInfo rt2MultiWIndow = makeRunningTaskInfo(2);
+ rt2MultiWIndow.configuration.windowConfiguration.setWindowingMode(
+ WINDOWING_MODE_MULTI_WINDOW);
+ mShellTaskOrganizer.onTaskInfoChanged(rt2MultiWIndow);
+
+ verify(mRecentTasksController).notifyRecentTasksChanged();
+ }
+
+ /**
+ * Helper to create a task with a given task id.
+ */
+ private ActivityManager.RecentTaskInfo makeTaskInfo(int taskId) {
+ ActivityManager.RecentTaskInfo info = new ActivityManager.RecentTaskInfo();
+ info.taskId = taskId;
+ return info;
+ }
+
+ /**
+ * Helper to create a running task with a given task id.
+ */
+ private ActivityManager.RunningTaskInfo makeRunningTaskInfo(int taskId) {
+ ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo();
+ info.taskId = taskId;
+ return info;
+ }
+
+ /**
+ * Helper to set the raw task list on the controller.
+ */
+ private ArrayList<ActivityManager.RecentTaskInfo> setRawList(
+ ActivityManager.RecentTaskInfo... tasks) {
+ ArrayList<ActivityManager.RecentTaskInfo> rawList = new ArrayList<>();
+ for (ActivityManager.RecentTaskInfo task : tasks) {
+ rawList.add(task);
+ }
+ doReturn(rawList).when(mRecentTasksController).getRawRecentTasks(anyInt(), anyInt(),
+ anyInt());
+ return rawList;
+ }
+
+ /**
+ * Asserts that the recent tasks matches the given task ids.
+ * @param expectedTaskIds list of task ids that map to the flattened task ids of the tasks in
+ * the grouped task list
+ */
+ private void assertGroupedTasksListEquals(ArrayList<GroupedRecentTaskInfo> recentTasks,
+ int... expectedTaskIds) {
+ int[] flattenedTaskIds = new int[recentTasks.size() * 2];
+ for (int i = 0; i < recentTasks.size(); i++) {
+ GroupedRecentTaskInfo pair = recentTasks.get(i);
+ flattenedTaskIds[2 * i] = pair.mTaskInfo1.taskId;
+ flattenedTaskIds[2 * i + 1] = pair.mTaskInfo2 != null
+ ? pair.mTaskInfo2.taskId
+ : -1;
+ }
+ assertTrue("Expected: " + Arrays.toString(expectedTaskIds)
+ + " Received: " + Arrays.toString(flattenedTaskIds),
+ Arrays.equals(flattenedTaskIds, expectedTaskIds));
+ }
+}