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
This commit is contained in:
Winson Chung
2021-10-16 21:44:25 -07:00
parent da86810290
commit 20e3e4e220
21 changed files with 864 additions and 17 deletions

View File

@@ -38,6 +38,7 @@ filegroup {
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 @@ filegroup {
path: "src",
}
// Aidls which can be used directly downstream of this library
filegroup {
name: "wm_shell-aidls",
srcs: [

View File

@@ -24,6 +24,7 @@ import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController;
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 @@ public final class ShellCommandHandlerImpl {
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 @@ public final class ShellCommandHandlerImpl {
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 @@ public final class ShellCommandHandlerImpl {
pw.println();
pw.println();
mSplitScreenOptional.ifPresent(splitScreen -> splitScreen.dump(pw, ""));
pw.println();
pw.println();
mRecentTasks.ifPresent(recentTasks -> recentTasks.dump(pw, ""));
}

View File

@@ -29,8 +29,8 @@ import com.android.wm.shell.draganddrop.DragAndDropController;
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 @@ public class ShellInitImpl {
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 @@ public class ShellInitImpl {
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 @@ public class ShellInitImpl {
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 @@ public class ShellInitImpl {
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 @@ public class ShellInitImpl {
f, ShellTaskOrganizer.TASK_LISTENER_TYPE_FREEFORM));
mFullscreenUnfoldController.ifPresent(FullscreenUnfoldController::init);
mRecentTasks.ifPresent(RecentTasksController::init);
}
@ExternalThread

View File

@@ -51,6 +51,7 @@ import com.android.internal.protolog.common.ProtoLog;
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.ArrayList;
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 @@ public class ShellTaskOrganizer extends TaskOrganizer implements
@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 @@ public class ShellTaskOrganizer extends TaskOrganizer implements
// 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 @@ public class ShellTaskOrganizer extends TaskOrganizer implements
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));
}
}

View File

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

View File

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

View File

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

View File

@@ -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];
}
}
}

View File

@@ -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;

View File

@@ -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];
}
};
}

View File

@@ -65,6 +65,7 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.Optional;
/**
* Tests for the shell task organizer.
@@ -131,7 +132,7 @@ public class ShellTaskOrganizerTests {
.when(mTaskOrganizerController).registerTaskOrganizer(any());
} catch (RemoteException e) {}
mOrganizer = spy(new ShellTaskOrganizer(mTaskOrganizerController, mTestExecutor, mContext,
mSizeCompatUI));
mSizeCompatUI, Optional.empty()));
}
@Test

View File

@@ -26,6 +26,7 @@ import androidx.test.InstrumentationRegistry;
import org.junit.After;
import org.junit.Before;
import org.mockito.MockitoAnnotations;
/**
* Base class that does shell test case setup.
@@ -36,6 +37,7 @@ public abstract class ShellTestCase {
@Before
public void shellSetup() {
MockitoAnnotations.initMocks(this);
final Context context =
InstrumentationRegistry.getInstrumentation().getTargetContext();
final DisplayManager dm = context.getSystemService(DisplayManager.class);

View File

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

View File

@@ -0,0 +1,45 @@
/*
* 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.systemui.shared.recents.model;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* A group task in the recent tasks list.
* TODO: Move this into Launcher
*/
public class GroupTask {
public @NonNull Task task1;
public @Nullable Task task2;
public GroupTask(@NonNull Task t1, @Nullable Task t2) {
task1 = t1;
task2 = t2;
}
public GroupTask(@NonNull GroupTask group) {
task1 = new Task(group.task1);
task2 = group.task2 != null
? new Task(group.task2)
: null;
}
public boolean containsTask(int taskId) {
return task1.key.id == taskId || (task2 != null && task2.key.id == taskId);
}
}

View File

@@ -38,6 +38,7 @@ import java.util.Objects;
/**
* A task in the recent tasks list.
* TODO: Move this into Launcher or see if we can remove now
*/
public class Task {

View File

@@ -57,6 +57,8 @@ public class QuickStepContract {
"extra_shell_starting_window";
// See ISmartspaceTransitionController.aidl
public static final String KEY_EXTRA_SMARTSPACE_TRANSITION_CONTROLLER = "smartspace_transition";
// See IRecentTasks.aidl
public static final String KEY_EXTRA_RECENT_TASKS = "recent_tasks";
public static final String NAV_BAR_MODE_2BUTTON_OVERLAY =
WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON_OVERLAY;

View File

@@ -31,6 +31,7 @@ import com.android.systemui.dagger.WMComponent;
import com.android.systemui.navigationbar.gestural.BackGestureTfClassifierProvider;
import com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.recents.RecentTasks;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
@@ -119,7 +120,8 @@ public class SystemUIFactory {
.setTransitions(mWMComponent.getTransitions())
.setStartingSurface(mWMComponent.getStartingSurface())
.setDisplayAreaHelper(mWMComponent.getDisplayAreaHelper())
.setTaskSurfaceHelper(mWMComponent.getTaskSurfaceHelper());
.setTaskSurfaceHelper(mWMComponent.getTaskSurfaceHelper())
.setRecentTasks(mWMComponent.getRecentTasks());
} else {
// TODO: Call on prepareSysUIComponentBuilder but not with real components. Other option
// is separating this logic into newly creating SystemUITestsFactory.
@@ -136,7 +138,8 @@ public class SystemUIFactory {
.setTransitions(Transitions.createEmptyForTesting())
.setDisplayAreaHelper(Optional.ofNullable(null))
.setStartingSurface(Optional.ofNullable(null))
.setTaskSurfaceHelper(Optional.ofNullable(null));
.setTaskSurfaceHelper(Optional.ofNullable(null))
.setRecentTasks(Optional.ofNullable(null));
}
mSysUIComponent = builder.build();
if (mInitializeComponents) {

View File

@@ -34,6 +34,7 @@ import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.recents.RecentTasks;
import com.android.wm.shell.splitscreen.SplitScreen;
import com.android.wm.shell.startingsurface.StartingSurface;
import com.android.wm.shell.tasksurfacehelper.TaskSurfaceHelper;
@@ -101,6 +102,9 @@ public interface SysUIComponent {
@BindsInstance
Builder setTaskSurfaceHelper(Optional<TaskSurfaceHelper> t);
@BindsInstance
Builder setRecentTasks(Optional<RecentTasks> r);
SysUIComponent build();
}

View File

@@ -32,6 +32,7 @@ import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.recents.RecentTasks;
import com.android.wm.shell.splitscreen.SplitScreen;
import com.android.wm.shell.startingsurface.StartingSurface;
import com.android.wm.shell.tasksurfacehelper.TaskSurfaceHelper;
@@ -110,4 +111,7 @@ public interface WMComponent {
@WMSingleton
Optional<TaskSurfaceHelper> getTaskSurfaceHelper();
@WMSingleton
Optional<RecentTasks> getRecentTasks();
}

View File

@@ -24,6 +24,7 @@ import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON;
import static com.android.internal.accessibility.common.ShortcutConstants.CHOOSER_PACKAGE_NAME;
import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_RECENT_TASKS;
import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_ONE_HANDED;
import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_PIP;
import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_SHELL_TRANSITIONS;
@@ -103,6 +104,7 @@ import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.pip.PipAnimationController;
import com.android.wm.shell.recents.RecentTasks;
import com.android.wm.shell.splitscreen.SplitScreen;
import com.android.wm.shell.startingsurface.StartingSurface;
import com.android.wm.shell.transition.ShellTransitions;
@@ -156,6 +158,7 @@ public class OverviewProxyService extends CurrentUserTracker implements
private final ShellTransitions mShellTransitions;
private final Optional<StartingSurface> mStartingSurface;
private final SmartspaceTransitionController mSmartspaceTransitionController;
private final Optional<RecentTasks> mRecentTasks;
private Region mActiveNavBarRegion;
@@ -480,6 +483,9 @@ public class OverviewProxyService extends CurrentUserTracker implements
params.putBinder(
KEY_EXTRA_SMARTSPACE_TRANSITION_CONTROLLER,
mSmartspaceTransitionController.createExternalInterface().asBinder());
mRecentTasks.ifPresent(recentTasks -> params.putBinder(
KEY_EXTRA_RECENT_TASKS,
recentTasks.createExternalInterface().asBinder()));
try {
mOverviewProxy.onInitialize(params);
@@ -530,17 +536,18 @@ public class OverviewProxyService extends CurrentUserTracker implements
@Inject
public OverviewProxyService(Context context, CommandQueue commandQueue,
Lazy<NavigationBarController> navBarControllerLazy,
Lazy<Optional<StatusBar>> statusBarOptionalLazy,
NavigationModeController navModeController,
NotificationShadeWindowController statusBarWinController, SysUiState sysUiState,
Optional<Pip> pipOptional,
Optional<LegacySplitScreen> legacySplitScreenOptional,
Optional<SplitScreen> splitScreenOptional,
Lazy<Optional<StatusBar>> statusBarOptionalLazy,
Optional<OneHanded> oneHandedOptional,
Optional<RecentTasks> recentTasks,
Optional<StartingSurface> startingSurface,
BroadcastDispatcher broadcastDispatcher,
ShellTransitions shellTransitions,
ScreenLifecycle screenLifecycle,
Optional<StartingSurface> startingSurface,
SmartspaceTransitionController smartspaceTransitionController,
DumpManager dumpManager) {
super(broadcastDispatcher);
@@ -562,6 +569,7 @@ public class OverviewProxyService extends CurrentUserTracker implements
mSysUiState.addCallback(this::notifySystemUiStateFlags);
mOneHandedOptional = oneHandedOptional;
mShellTransitions = shellTransitions;
mRecentTasks = recentTasks;
// Assumes device always starts with back button until launcher tells it that it does not
mNavBarButtonAlpha = 1.0f;

View File

@@ -74,6 +74,8 @@ import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
import com.android.wm.shell.pip.PipUiEventLogger;
import com.android.wm.shell.pip.phone.PipAppOpsListener;
import com.android.wm.shell.pip.phone.PipTouchHandler;
import com.android.wm.shell.recents.RecentTasks;
import com.android.wm.shell.recents.RecentTasksController;
import com.android.wm.shell.sizecompatui.SizeCompatUIController;
import com.android.wm.shell.splitscreen.SplitScreen;
import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -144,8 +146,11 @@ public abstract class WMShellBaseModule {
@WMSingleton
@Provides
static ShellTaskOrganizer provideShellTaskOrganizer(@ShellMainThread ShellExecutor mainExecutor,
Context context, SizeCompatUIController sizeCompatUI) {
return new ShellTaskOrganizer(mainExecutor, context, sizeCompatUI);
Context context,
SizeCompatUIController sizeCompatUI,
Optional<RecentTasksController> recentTasksOptional
) {
return new ShellTaskOrganizer(mainExecutor, context, sizeCompatUI, recentTasksOptional);
}
@WMSingleton
@@ -409,6 +414,28 @@ public abstract class WMShellBaseModule {
@BindsOptionalOf
abstract PipTouchHandler optionalPipTouchHandler();
//
// Recent tasks
//
@WMSingleton
@Provides
static Optional<RecentTasks> provideRecentTasks(
Optional<RecentTasksController> recentTasksController) {
return recentTasksController.map((controller) -> controller.asRecentTasks());
}
@WMSingleton
@Provides
static Optional<RecentTasksController> provideRecentTasksController(
Context context,
TaskStackListenerImpl taskStackListener,
@ShellMainThread ShellExecutor mainExecutor
) {
return Optional.ofNullable(
RecentTasksController.create(context, taskStackListener, mainExecutor));
}
//
// Shell transitions
//
@@ -555,13 +582,13 @@ public abstract class WMShellBaseModule {
DragAndDropController dragAndDropController,
ShellTaskOrganizer shellTaskOrganizer,
Optional<BubbleController> bubblesOptional,
Optional<LegacySplitScreenController> legacySplitScreenOptional,
Optional<SplitScreenController> splitScreenOptional,
Optional<AppPairsController> appPairsOptional,
Optional<PipTouchHandler> pipTouchHandlerOptional,
FullscreenTaskListener fullscreenTaskListener,
Optional<FullscreenUnfoldController> appUnfoldTransitionController,
Optional<Optional<FreeformTaskListener>> freeformTaskListener,
Optional<RecentTasksController> recentTasksOptional,
Transitions transitions,
StartingWindowController startingWindow,
@ShellMainThread ShellExecutor mainExecutor) {
@@ -571,13 +598,13 @@ public abstract class WMShellBaseModule {
dragAndDropController,
shellTaskOrganizer,
bubblesOptional,
legacySplitScreenOptional,
splitScreenOptional,
appPairsOptional,
pipTouchHandlerOptional,
fullscreenTaskListener,
appUnfoldTransitionController,
freeformTaskListener,
recentTasksOptional,
transitions,
startingWindow,
mainExecutor);
@@ -603,9 +630,10 @@ public abstract class WMShellBaseModule {
Optional<OneHandedController> oneHandedOptional,
Optional<HideDisplayCutoutController> hideDisplayCutout,
Optional<AppPairsController> appPairsOptional,
Optional<RecentTasksController> recentTasksOptional,
@ShellMainThread ShellExecutor mainExecutor) {
return new ShellCommandHandlerImpl(shellTaskOrganizer,
legacySplitScreenOptional, splitScreenOptional, pipOptional, oneHandedOptional,
hideDisplayCutout, appPairsOptional, mainExecutor);
hideDisplayCutout, appPairsOptional, recentTasksOptional, mainExecutor);
}
}