diff --git a/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json b/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json index 6342c00f80fa0..8213f2fec04d9 100644 --- a/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json +++ b/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json @@ -79,6 +79,12 @@ "group": "WM_SHELL_TASK_ORG", "at": "com\/android\/wm\/shell\/splitscreen\/SplitScreenTaskListener.java" }, + "-742394458": { + "message": "pair task1=%d task2=%d in AppPair=%s", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/apppairs\/AppPair.java" + }, "-710770147": { "message": "Add target: %s", "level": "VERBOSE", @@ -91,6 +97,12 @@ "group": "WM_SHELL_TASK_ORG", "at": "com\/android\/wm\/shell\/splitscreen\/SplitScreenTaskListener.java" }, + "-234284913": { + "message": "unpair taskId=%d pair=%s", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/apppairs\/AppPairsController.java" + }, "-191422040": { "message": "Transition animations finished, notifying core %s", "level": "VERBOSE", @@ -139,12 +151,30 @@ "group": "WM_SHELL_TASK_ORG", "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" }, + "900599280": { + "message": "Can't pair unresizeable tasks task1.isResizeable=%b task1.isResizeable=%b", + "level": "ERROR", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/apppairs\/AppPair.java" + }, + "950299522": { + "message": "taskId %d isn't isn't in an app-pair.", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/apppairs\/AppPairsController.java" + }, "980952660": { "message": "Task root back pressed taskId=%d", "level": "VERBOSE", "group": "WM_SHELL_TASK_ORG", "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" }, + "1079041527": { + "message": "incrementPool size=%d", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/apppairs\/AppPairsPool.java" + }, "1104702476": { "message": "Letterbox Task Changed: #%d", "level": "VERBOSE", @@ -175,12 +205,24 @@ "group": "WM_SHELL_DRAG_AND_DROP", "at": "com\/android\/wm\/shell\/draganddrop\/DragAndDropController.java" }, + "1891981945": { + "message": "release entry.taskId=%s listener=%s size=%d", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/apppairs\/AppPairsPool.java" + }, "1990759023": { "message": "addListenerForType types=%s listener=%s", "level": "VERBOSE", "group": "WM_SHELL_TASK_ORG", "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" }, + "2006473416": { + "message": "acquire entry.taskId=%s listener=%s size=%d", + "level": "VERBOSE", + "group": "WM_SHELL_TASK_ORG", + "at": "com\/android\/wm\/shell\/apppairs\/AppPairsPool.java" + }, "2057038970": { "message": "Display changed: %d", "level": "VERBOSE", diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellDump.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellDump.java index bb9accd78bab1..13089affd0586 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellDump.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellDump.java @@ -16,6 +16,7 @@ package com.android.wm.shell; +import com.android.wm.shell.apppairs.AppPairs; import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.pip.Pip; @@ -34,17 +35,20 @@ public class ShellDump { private final Optional mOneHandedOptional; private final Optional mHideDisplayCutout; private final ShellTaskOrganizer mShellTaskOrganizer; + private final Optional mAppPairsOptional; public ShellDump(ShellTaskOrganizer shellTaskOrganizer, Optional splitScreenOptional, Optional pipOptional, Optional oneHandedOptional, - Optional hideDisplayCutout) { + Optional hideDisplayCutout, + Optional appPairsOptional) { mShellTaskOrganizer = shellTaskOrganizer; mSplitScreenOptional = splitScreenOptional; mPipOptional = pipOptional; mOneHandedOptional = oneHandedOptional; mHideDisplayCutout = hideDisplayCutout; + mAppPairsOptional = appPairsOptional; } public void dump(PrintWriter pw) { @@ -55,5 +59,8 @@ public class ShellDump { mSplitScreenOptional.ifPresent(splitScreen -> splitScreen.dump(pw)); mOneHandedOptional.ifPresent(oneHanded -> oneHanded.dump(pw)); mHideDisplayCutout.ifPresent(hideDisplayCutout -> hideDisplayCutout.dump(pw)); + pw.println(); + pw.println(); + mAppPairsOptional.ifPresent(appPairs -> appPairs.dump(pw, "")); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInit.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInit.java index 4269a905a0fe2..d654f8adc153f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInit.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInit.java @@ -16,6 +16,7 @@ package com.android.wm.shell; +import com.android.wm.shell.apppairs.AppPairs; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.splitscreen.SplitScreen; @@ -31,15 +32,18 @@ public class ShellInit { private final DragAndDropController mDragAndDropController; private final ShellTaskOrganizer mShellTaskOrganizer; private final Optional mSplitScreenOptional; + private final Optional mAppPairsOptional; public ShellInit(DisplayImeController displayImeController, DragAndDropController dragAndDropController, ShellTaskOrganizer shellTaskOrganizer, - Optional splitScreenOptional) { + Optional splitScreenOptional, + Optional appPairsOptional) { mDisplayImeController = displayImeController; mDragAndDropController = dragAndDropController; mShellTaskOrganizer = shellTaskOrganizer; mSplitScreenOptional = splitScreenOptional; + mAppPairsOptional = appPairsOptional; } public void init() { @@ -47,6 +51,7 @@ public class ShellInit { mDisplayImeController.startMonitorDisplays(); // Register the shell organizer mShellTaskOrganizer.registerOrganizer(); + mAppPairsOptional.ifPresent(AppPairs::onOrganizerRegistered); // Bind the splitscreen impl to the drag drop controller mDragAndDropController.setSplitScreenController(mSplitScreenOptional); } 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 ece063cabf7cd..006fd9d35a28f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -26,7 +26,6 @@ import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG import android.annotation.IntDef; import android.app.ActivityManager.RunningTaskInfo; -import android.app.WindowConfiguration.WindowingMode; import android.content.Context; import android.os.Binder; import android.os.IBinder; @@ -39,6 +38,7 @@ import android.window.TaskAppearedInfo; import android.window.TaskOrganizer; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; @@ -309,6 +309,15 @@ public class ShellTaskOrganizer extends TaskOrganizer { } } + /** Gets running task by taskId. Returns {@code null} if no such task observed. */ + @Nullable + public RunningTaskInfo getRunningTaskInfo(int taskId) { + synchronized (mLock) { + final TaskAppearedInfo info = mTasks.get(taskId); + return info != null ? info.getTaskInfo() : null; + } + } + private boolean updateTaskListenerIfNeeded(RunningTaskInfo taskInfo, SurfaceControl leash, TaskListener oldListener, TaskListener newListener) { if (oldListener == newListener) return false; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java new file mode 100644 index 0000000000000..d30acee4be6a9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java @@ -0,0 +1,208 @@ +/* + * 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.apppairs; + +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; + +import android.app.ActivityManager; +import android.graphics.Rect; +import android.view.SurfaceControl; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.SyncTransactionQueue; + +import java.io.PrintWriter; + +/** + * An app-pairs consisting of {@link #mRootTaskInfo} that acts as the hierarchy parent of + * {@link #mTaskInfo1} and {@link #mTaskInfo2} in the pair. + * Also includes all UI for managing the pair like the divider. + */ +// TODO: Add divider +// TODO: Handle display rotation +class AppPair implements ShellTaskOrganizer.TaskListener { + private static final String TAG = AppPair.class.getSimpleName(); + + private ActivityManager.RunningTaskInfo mRootTaskInfo; + private SurfaceControl mRootTaskLeash; + private ActivityManager.RunningTaskInfo mTaskInfo1; + private SurfaceControl mTaskLeash1; + private ActivityManager.RunningTaskInfo mTaskInfo2; + private SurfaceControl mTaskLeash2; + + private final AppPairsController mController; + private final SyncTransactionQueue mSyncQueue; + + AppPair(AppPairsController controller) { + mController = controller; + mSyncQueue = controller.getSyncTransactionQueue(); + } + + int getRootTaskId() { + return mRootTaskInfo != null ? mRootTaskInfo.taskId : INVALID_TASK_ID; + } + + private int getTaskId1() { + return mTaskInfo1 != null ? mTaskInfo1.taskId : INVALID_TASK_ID; + } + + private int getTaskId2() { + return mTaskInfo2 != null ? mTaskInfo2.taskId : INVALID_TASK_ID; + } + + boolean contains(int taskId) { + return taskId == getRootTaskId() || taskId == getTaskId1() || taskId == getTaskId2(); + } + + boolean pair(ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) { + ProtoLog.v(WM_SHELL_TASK_ORG, "pair task1=%d task2=%d in AppPair=%s", + task1.taskId, task2.taskId, this); + + if (!task1.isResizeable || !task2.isResizeable) { + ProtoLog.e(WM_SHELL_TASK_ORG, + "Can't pair unresizeable tasks task1.isResizeable=%b task1.isResizeable=%b", + task1.isResizeable, task2.isResizeable); + return false; + } + + mTaskInfo1 = task1; + mTaskInfo2 = task2; + + // TODO: properly calculate bounds for pairs. + final Rect rootBounds = mRootTaskInfo.configuration.windowConfiguration.getBounds(); + final Rect bounds1 = new Rect( + rootBounds.left, rootBounds.top, rootBounds.right / 2, rootBounds.bottom / 2); + final Rect bounds2 = new Rect( + bounds1.right, bounds1.bottom, rootBounds.right, rootBounds.bottom); + final WindowContainerToken token1 = task1.token; + final WindowContainerToken token2 = task2.token; + final WindowContainerTransaction wct = new WindowContainerTransaction(); + + wct.setHidden(mRootTaskInfo.token, false) + .reparent(token1, mRootTaskInfo.token, true /* onTop */) + .reparent(token2, mRootTaskInfo.token, true /* onTop */) + .setWindowingMode(token1, WINDOWING_MODE_MULTI_WINDOW) + .setWindowingMode(token2, WINDOWING_MODE_MULTI_WINDOW) + .setBounds(token1, bounds1) + .setBounds(token2, bounds2) + // Moving the root task to top after the child tasks were repareted , or the root + // task cannot be visible and focused. + .reorder(mRootTaskInfo.token, true); + mController.getTaskOrganizer().applyTransaction(wct); + return true; + } + + void unpair() { + final WindowContainerToken token1 = mTaskInfo1.token; + final WindowContainerToken token2 = mTaskInfo2.token; + final WindowContainerTransaction wct = new WindowContainerTransaction(); + + // Reparent out of this container and reset windowing mode. + wct.setHidden(mRootTaskInfo.token, true) + .reorder(mRootTaskInfo.token, false) + .reparent(token1, null, false /* onTop */) + .reparent(token2, null, false /* onTop */) + .setWindowingMode(token1, WINDOWING_MODE_UNDEFINED) + .setWindowingMode(token2, WINDOWING_MODE_UNDEFINED); + mController.getTaskOrganizer().applyTransaction(wct); + + mTaskInfo1 = null; + mTaskInfo2 = null; + } + + @Override + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + if (mRootTaskInfo == null || taskInfo.taskId == mRootTaskInfo.taskId) { + mRootTaskInfo = taskInfo; + mRootTaskLeash = leash; + } else if (taskInfo.taskId == getTaskId1()) { + mTaskInfo1 = taskInfo; + mTaskLeash1 = leash; + } else if (taskInfo.taskId == getTaskId2()) { + mTaskInfo2 = taskInfo; + mTaskLeash2 = leash; + } else { + throw new IllegalStateException("Unknown task=" + taskInfo.taskId); + } + + if (mTaskLeash1 == null || mTaskLeash2 == null) return; + + // TODO: Is there more we need to do here? + mSyncQueue.runInSync(t -> t + .setPosition(mTaskLeash1, mTaskInfo1.positionInParent.x, + mTaskInfo1.positionInParent.y) + .setPosition(mTaskLeash2, mTaskInfo2.positionInParent.x, + mTaskInfo2.positionInParent.y) + .show(mRootTaskLeash) + .show(mTaskLeash1) + .show(mTaskLeash2)); + } + + @Override + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + if (taskInfo.taskId == getRootTaskId()) { + mRootTaskInfo = taskInfo; + } else if (taskInfo.taskId == getTaskId1()) { + mTaskInfo1 = taskInfo; + } else if (taskInfo.taskId == getTaskId2()) { + mTaskInfo2 = taskInfo; + } else { + throw new IllegalStateException("Unknown task=" + taskInfo.taskId); + } + } + + @Override + public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + if (taskInfo.taskId == getRootTaskId()) { + // We don't want to release this object back to the pool since the root task went away. + mController.unpair(mRootTaskInfo.taskId, false /* releaseToPool */); + } else if (taskInfo.taskId == getTaskId1() || taskInfo.taskId == getTaskId2()) { + mController.unpair(mRootTaskInfo.taskId); + } + } + + @Override + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + final String childPrefix = innerPrefix + " "; + pw.println(prefix + this); + pw.println(innerPrefix + "Root taskId=" + getRootTaskId() + + " winMode=" + mRootTaskInfo.getWindowingMode()); + if (mTaskInfo1 != null) { + pw.println(innerPrefix + "1 taskId=" + mTaskInfo1.taskId + + " winMode=" + mTaskInfo1.getWindowingMode()); + } + if (mTaskInfo2 != null) { + pw.println(innerPrefix + "2 taskId=" + mTaskInfo2.taskId + + " winMode=" + mTaskInfo2.getWindowingMode()); + } + } + + @Override + public String toString() { + return TAG + "#" + getRootTaskId(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairs.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairs.java new file mode 100644 index 0000000000000..ef3e3e0220e71 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairs.java @@ -0,0 +1,39 @@ +/* + * 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.apppairs; + +import android.app.ActivityManager; + +import androidx.annotation.NonNull; + +import java.io.PrintWriter; + +/** + * Interface to engage app pairs feature. + */ +public interface AppPairs { + /** Pairs indicated tasks. */ + boolean pair(int task1, int task2); + /** Pairs indicated tasks. */ + boolean pair(ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2); + /** Unpairs any app-pair containing this task id. */ + void unpair(int taskId); + /** Dumps current status of app pairs. */ + void dump(@NonNull PrintWriter pw, String prefix); + /** Called when the shell organizer has been registered. */ + void onOrganizerRegistered(); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java new file mode 100644 index 0000000000000..e0c7ba9234b24 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java @@ -0,0 +1,150 @@ +/* + * 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.apppairs; + +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; + +import android.app.ActivityManager; +import android.util.SparseArray; + +import androidx.annotation.NonNull; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.SyncTransactionQueue; + +import java.io.PrintWriter; + +/** + * Class manages app-pairs multitasking mode and implements the main interface {@link AppPairs}. + */ +public class AppPairsController implements AppPairs { + private static final String TAG = AppPairsController.class.getSimpleName(); + + private final ShellTaskOrganizer mTaskOrganizer; + private final SyncTransactionQueue mSyncQueue; + + private AppPairsPool mPairsPool; + // Active app-pairs mapped by root task id key. + private final SparseArray mActiveAppPairs = new SparseArray<>(); + + public AppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue) { + mTaskOrganizer = organizer; + mSyncQueue = syncQueue; + } + + @Override + public void onOrganizerRegistered() { + if (mPairsPool == null) { + setPairsPool(new AppPairsPool(this)); + } + } + + @VisibleForTesting + void setPairsPool(AppPairsPool pool) { + mPairsPool = pool; + } + + @Override + public boolean pair(int taskId1, int taskId2) { + final ActivityManager.RunningTaskInfo task1 = mTaskOrganizer.getRunningTaskInfo(taskId1); + final ActivityManager.RunningTaskInfo task2 = mTaskOrganizer.getRunningTaskInfo(taskId2); + if (task1 == null || task2 == null) { + return false; + } + return pair(task1, task2); + } + + @Override + public boolean pair(ActivityManager.RunningTaskInfo task1, + ActivityManager.RunningTaskInfo task2) { + return pairInner(task1, task2) != null; + } + + @VisibleForTesting + AppPair pairInner( + @NonNull ActivityManager.RunningTaskInfo task1, + @NonNull ActivityManager.RunningTaskInfo task2) { + final AppPair pair = mPairsPool.acquire(); + if (!pair.pair(task1, task2)) { + mPairsPool.release(pair); + return null; + } + + mActiveAppPairs.put(pair.getRootTaskId(), pair); + return pair; + } + + @Override + public void unpair(int taskId) { + unpair(taskId, true /* releaseToPool */); + } + + void unpair(int taskId, boolean releaseToPool) { + AppPair pair = mActiveAppPairs.get(taskId); + if (pair == null) { + for (int i = mActiveAppPairs.size() - 1; i >= 0; --i) { + final AppPair candidate = mActiveAppPairs.valueAt(i); + if (candidate.contains(taskId)) { + pair = candidate; + break; + } + } + } + if (pair == null) { + ProtoLog.v(WM_SHELL_TASK_ORG, "taskId %d isn't isn't in an app-pair.", taskId); + return; + } + + ProtoLog.v(WM_SHELL_TASK_ORG, "unpair taskId=%d pair=%s", taskId, pair); + mActiveAppPairs.remove(pair.getRootTaskId()); + pair.unpair(); + if (releaseToPool) { + mPairsPool.release(pair); + } + } + + ShellTaskOrganizer getTaskOrganizer() { + return mTaskOrganizer; + } + + SyncTransactionQueue getSyncTransactionQueue() { + return mSyncQueue; + } + + @Override + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + final String childPrefix = innerPrefix + " "; + pw.println(prefix + this); + + for (int i = mActiveAppPairs.size() - 1; i >= 0; --i) { + mActiveAppPairs.valueAt(i).dump(pw, childPrefix); + } + + if (mPairsPool != null) { + mPairsPool.dump(pw, prefix); + } + } + + @Override + public String toString() { + return TAG + "#" + mActiveAppPairs.size(); + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsPool.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsPool.java new file mode 100644 index 0000000000000..5c6037ea07027 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsPool.java @@ -0,0 +1,93 @@ +/* + * 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.apppairs; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; + +import androidx.annotation.NonNull; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; + +import java.io.PrintWriter; +import java.util.ArrayList; + +/** + * Class that manager pool of {@link AppPair} objects. Helps reduce the need to call system_server + * to create a root task for the app-pair when needed since we always have one ready to go. + */ +class AppPairsPool { + private static final String TAG = AppPairsPool.class.getSimpleName(); + + @VisibleForTesting + final AppPairsController mController; + // The pool + private final ArrayList mPool = new ArrayList(); + + AppPairsPool(AppPairsController controller) { + mController = controller; + incrementPool(); + } + + AppPair acquire() { + final AppPair entry = mPool.remove(mPool.size() - 1); + ProtoLog.v(WM_SHELL_TASK_ORG, "acquire entry.taskId=%s listener=%s size=%d", + entry.getRootTaskId(), entry, mPool.size()); + if (mPool.size() == 0) { + incrementPool(); + } + return entry; + } + + void release(AppPair entry) { + mPool.add(entry); + ProtoLog.v(WM_SHELL_TASK_ORG, "release entry.taskId=%s listener=%s size=%d", + entry.getRootTaskId(), entry, mPool.size()); + } + + @VisibleForTesting + void incrementPool() { + ProtoLog.v(WM_SHELL_TASK_ORG, "incrementPool size=%d", mPool.size()); + final AppPair entry = new AppPair(mController); + // TODO: multi-display... + mController.getTaskOrganizer().createRootTask( + DEFAULT_DISPLAY, WINDOWING_MODE_FULLSCREEN, entry); + mPool.add(entry); + } + + @VisibleForTesting + int poolSize() { + return mPool.size(); + } + + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + final String childPrefix = innerPrefix + " "; + pw.println(prefix + this); + for (int i = mPool.size() - 1; i >= 0; --i) { + mPool.get(i).dump(pw, childPrefix); + } + } + + @Override + public String toString() { + return TAG + "#" + mPool.size(); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairTests.java new file mode 100644 index 0000000000000..9ab0f89953e4c --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairTests.java @@ -0,0 +1,81 @@ +/* + * 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.apppairs; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.ActivityManager; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.SyncTransactionQueue; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Tests for {@link AppPair} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class AppPairTests { + + private AppPairsController mController; + @Mock private SyncTransactionQueue mSyncQueue; + @Mock private ShellTaskOrganizer mTaskOrganizer; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mController = new TestAppPairsController(mTaskOrganizer, mSyncQueue); + } + + @After + public void tearDown() {} + + @Test + public void testContains() { + final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build(); + final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build(); + + final AppPair pair = mController.pairInner(task1, task2); + assertThat(pair.contains(task1.taskId)).isTrue(); + assertThat(pair.contains(task2.taskId)).isTrue(); + + pair.unpair(); + assertThat(pair.contains(task1.taskId)).isFalse(); + assertThat(pair.contains(task2.taskId)).isFalse(); + } + + @Test + public void testVanishUnpairs() { + final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build(); + final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build(); + + final AppPair pair = mController.pairInner(task1, task2); + assertThat(pair.contains(task1.taskId)).isTrue(); + assertThat(pair.contains(task2.taskId)).isTrue(); + + pair.onTaskVanished(task1); + assertThat(pair.contains(task1.taskId)).isFalse(); + assertThat(pair.contains(task2.taskId)).isFalse(); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsControllerTests.java new file mode 100644 index 0000000000000..ed85b67c3feac --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsControllerTests.java @@ -0,0 +1,85 @@ +/* + * 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.apppairs; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.ActivityManager; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.SyncTransactionQueue; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Tests for {@link AppPairsController} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class AppPairsControllerTests { + private TestAppPairsController mController; + private TestAppPairsPool mPool; + @Mock private SyncTransactionQueue mSyncQueue; + @Mock private ShellTaskOrganizer mTaskOrganizer; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mController = new TestAppPairsController(mTaskOrganizer, mSyncQueue); + mPool = mController.getPool(); + } + + @After + public void tearDown() {} + + @Test + public void testPairUnpair() { + final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build(); + final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build(); + + final AppPair pair = mController.pairInner(task1, task2); + assertThat(pair.contains(task1.taskId)).isTrue(); + assertThat(pair.contains(task2.taskId)).isTrue(); + assertThat(mPool.poolSize()).isGreaterThan(0); + + mController.unpair(task2.taskId); + assertThat(pair.contains(task1.taskId)).isFalse(); + assertThat(pair.contains(task2.taskId)).isFalse(); + assertThat(mPool.poolSize()).isGreaterThan(1); + } + + @Test + public void testUnpair_DontReleaseToPool() { + final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build(); + final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build(); + + final AppPair pair = mController.pairInner(task1, task2); + assertThat(pair.contains(task1.taskId)).isTrue(); + assertThat(pair.contains(task2.taskId)).isTrue(); + + mController.unpair(task2.taskId, false /* releaseToPool */); + assertThat(pair.contains(task1.taskId)).isFalse(); + assertThat(pair.contains(task2.taskId)).isFalse(); + assertThat(mPool.poolSize()).isEqualTo(1); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsPoolTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsPoolTests.java new file mode 100644 index 0000000000000..4a0fe0f28620f --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsPoolTests.java @@ -0,0 +1,67 @@ +/* + * 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.apppairs; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.SyncTransactionQueue; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Tests for {@link AppPairsPool} */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class AppPairsPoolTests { + private TestAppPairsController mController; + private TestAppPairsPool mPool; + @Mock private SyncTransactionQueue mSyncQueue; + @Mock private ShellTaskOrganizer mTaskOrganizer; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mController = new TestAppPairsController(mTaskOrganizer, mSyncQueue); + mPool = mController.getPool(); + } + + @After + public void tearDown() {} + + @Test + public void testInitialState() { + // Pool should always start off with at least 1 entry. + assertThat(mPool.poolSize()).isGreaterThan(0); + } + + @Test + public void testAcquireRelease() { + assertThat(mPool.poolSize()).isGreaterThan(0); + final AppPair appPair = mPool.acquire(); + assertThat(mPool.poolSize()).isGreaterThan(0); + mPool.release(appPair); + assertThat(mPool.poolSize()).isGreaterThan(1); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java new file mode 100644 index 0000000000000..7ea5a1b96c07b --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java @@ -0,0 +1,34 @@ +/* + * 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.apppairs; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.SyncTransactionQueue; + +public class TestAppPairsController extends AppPairsController { + TestAppPairsPool mPool; + + public TestAppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue) { + super(organizer, syncQueue); + mPool = new TestAppPairsPool(this); + setPairsPool(mPool); + } + + TestAppPairsPool getPool() { + return mPool; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsPool.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsPool.java new file mode 100644 index 0000000000000..080f2075344af --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsPool.java @@ -0,0 +1,34 @@ +/* + * 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.apppairs; + +import android.app.ActivityManager; + +public class TestAppPairsPool extends AppPairsPool{ + TestAppPairsPool(AppPairsController controller) { + super(controller); + } + + @Override + void incrementPool() { + final AppPair entry = new AppPair(mController); + final ActivityManager.RunningTaskInfo info = + new TestRunningTaskInfoBuilder().build(); + entry.onTaskAppeared(info, null /* leash */); + release(entry); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestRunningTaskInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestRunningTaskInfoBuilder.java new file mode 100644 index 0000000000000..76d3a6aaee226 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestRunningTaskInfoBuilder.java @@ -0,0 +1,43 @@ +/* + * 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.apppairs; + +import android.app.ActivityManager; +import android.graphics.Rect; +import android.window.IWindowContainerToken; +import android.window.WindowContainerToken; + +public class TestRunningTaskInfoBuilder { + static int sNextTaskId = 500; + private Rect mBounds = new Rect(0, 0, 100, 100); + private WindowContainerToken mToken = + new WindowContainerToken(new IWindowContainerToken.Default()); + + TestRunningTaskInfoBuilder setBounds(Rect bounds) { + mBounds.set(bounds); + return this; + } + + ActivityManager.RunningTaskInfo build() { + final ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo(); + info.taskId = sNextTaskId++; + info.configuration.windowConfiguration.setBounds(mBounds); + info.token = mToken; + info.isResizeable = true; + return info; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java index 7e48edf3028ac..11180d131e700 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java @@ -110,7 +110,8 @@ public class SystemUIFactory { .setOneHanded(mWMComponent.getOneHanded()) .setBubbles(mWMComponent.getBubbles()) .setHideDisplayCutout(mWMComponent.getHideDisplayCutout()) - .setShellDump(mWMComponent.getShellDump()); + .setShellDump(mWMComponent.getShellDump()) + .setAppPairs(mWMComponent.getAppPairs()); } else { // TODO: Call on prepareSysUIComponentBuilder but not with real components. builder = builder.setPip(Optional.ofNullable(null)) @@ -118,7 +119,8 @@ public class SystemUIFactory { .setOneHanded(Optional.ofNullable(null)) .setBubbles(Optional.ofNullable(null)) .setHideDisplayCutout(Optional.ofNullable(null)) - .setShellDump(Optional.ofNullable(null)); + .setShellDump(Optional.ofNullable(null)) + .setAppPairs(Optional.ofNullable(null)); } mSysUIComponent = builder.build(); if (initializeComponents) { diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java index 02c3c2f27e077..68a28ba63d6fd 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java @@ -25,6 +25,7 @@ import com.android.systemui.keyguard.KeyguardSliceProvider; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.util.InjectionInflationController; import com.android.wm.shell.ShellDump; +import com.android.wm.shell.apppairs.AppPairs; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout; import com.android.wm.shell.onehanded.OneHanded; @@ -60,6 +61,9 @@ public interface SysUIComponent { @BindsInstance Builder setSplitScreen(Optional s); + @BindsInstance + Builder setAppPairs(Optional s); + @BindsInstance Builder setOneHanded(Optional o); diff --git a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java index b3e62e52e31b9..e634529dcb715 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java @@ -19,6 +19,7 @@ package com.android.systemui.dagger; import com.android.systemui.wmshell.WMShellModule; import com.android.wm.shell.ShellDump; import com.android.wm.shell.ShellInit; +import com.android.wm.shell.apppairs.AppPairs; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout; import com.android.wm.shell.onehanded.OneHanded; @@ -70,6 +71,9 @@ public interface WMComponent { @WMSingleton Optional getSplitScreen(); + @WMSingleton + Optional getAppPairs(); + @WMSingleton Optional getBubbles(); diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index 04f1f86c3ac7a..c36acf5b6def7 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -53,6 +53,7 @@ import com.android.systemui.statusbar.policy.UserInfoController; import com.android.systemui.tracing.ProtoTracer; import com.android.systemui.tracing.nano.SystemUiTraceProto; import com.android.wm.shell.ShellDump; +import com.android.wm.shell.apppairs.AppPairs; import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout; import com.android.wm.shell.nano.WmShellTraceProto; import com.android.wm.shell.onehanded.OneHanded; @@ -98,6 +99,7 @@ public final class WMShell extends SystemUI private final Optional mHideDisplayCutoutOptional; private final ProtoTracer mProtoTracer; private final Optional mShellDump; + private final Optional mAppPairsOptional; private boolean mIsSysUiStateValid; private KeyguardUpdateMonitorCallback mSplitScreenKeyguardCallback; @@ -116,7 +118,8 @@ public final class WMShell extends SystemUI Optional oneHandedOptional, Optional hideDisplayCutoutOptional, ProtoTracer protoTracer, - Optional shellDump) { + Optional shellDump, + Optional appPairsOptional) { super(context); mCommandQueue = commandQueue; mConfigurationController = configurationController; @@ -131,6 +134,7 @@ public final class WMShell extends SystemUI mProtoTracer = protoTracer; mProtoTracer.add(this); mShellDump = shellDump; + mAppPairsOptional = appPairsOptional; } @Override @@ -335,6 +339,21 @@ public final class WMShell extends SystemUI } return true; } + + case "pair": { + String[] groups = Arrays.copyOfRange(args, i + 1, args.length); + final int taskId1 = new Integer(groups[0]); + final int taskId2 = new Integer(groups[1]); + mAppPairsOptional.ifPresent(appPairs -> appPairs.pair(taskId1, taskId2)); + return true; + } + + case "unpair": { + String[] groups = Arrays.copyOfRange(args, i + 1, args.length); + final int taskId = new Integer(groups[0]); + mAppPairsOptional.ifPresent(appPairs -> appPairs.unpair(taskId)); + return true; + } } } return false; diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java index 8c2980fddf960..74aa1a7d0749b 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java @@ -32,6 +32,7 @@ import com.android.wm.shell.ShellDump; import com.android.wm.shell.ShellInit; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; +import com.android.wm.shell.apppairs.AppPairs; import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.common.AnimationThread; @@ -75,11 +76,13 @@ public abstract class WMShellBaseModule { static ShellInit provideShellInit(DisplayImeController displayImeController, DragAndDropController dragAndDropController, ShellTaskOrganizer shellTaskOrganizer, - Optional splitScreenOptional) { + Optional splitScreenOptional, + Optional appPairsOptional) { return new ShellInit(displayImeController, dragAndDropController, shellTaskOrganizer, - splitScreenOptional); + splitScreenOptional, + appPairsOptional); } /** @@ -92,9 +95,10 @@ public abstract class WMShellBaseModule { Optional splitScreenOptional, Optional pipOptional, Optional oneHandedOptional, - Optional hideDisplayCutout) { + Optional hideDisplayCutout, + Optional appPairsOptional) { return Optional.of(new ShellDump(shellTaskOrganizer, splitScreenOptional, pipOptional, - oneHandedOptional, hideDisplayCutout)); + oneHandedOptional, hideDisplayCutout, appPairsOptional)); } @WMSingleton @@ -187,6 +191,9 @@ public abstract class WMShellBaseModule { @BindsOptionalOf abstract SplitScreen optionalSplitScreen(); + @BindsOptionalOf + abstract AppPairs optionalAppPairs(); + @WMSingleton @Provides static Optional provideBubbles(Context context, diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java index e9c4b0b398c89..ef8a08c565a04 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java @@ -24,6 +24,8 @@ import com.android.systemui.dagger.WMSingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; +import com.android.wm.shell.apppairs.AppPairs; +import com.android.wm.shell.apppairs.AppPairsController; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.FloatingContentCoordinator; @@ -79,6 +81,13 @@ public class WMShellModule { taskStackListener); } + @WMSingleton + @Provides + static AppPairs provideAppPairs(ShellTaskOrganizer shellTaskOrganizer, + SyncTransactionQueue syncQueue) { + return new AppPairsController(shellTaskOrganizer, syncQueue); + } + @WMSingleton @Provides static Optional providePip(Context context, DisplayController displayController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java index 34889ffdf959b..a658469f9e2b2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java @@ -34,6 +34,7 @@ import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.tracing.ProtoTracer; import com.android.wm.shell.ShellDump; +import com.android.wm.shell.apppairs.AppPairs; import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.onehanded.OneHandedGestureHandler; @@ -68,6 +69,7 @@ public class WMShellTest extends SysuiTestCase { @Mock HideDisplayCutout mHideDisplayCutout; @Mock ProtoTracer mProtoTracer; @Mock ShellDump mShellDump; + @Mock AppPairs mAppPairs; @Before public void setUp() { @@ -77,7 +79,7 @@ public class WMShellTest extends SysuiTestCase { mKeyguardUpdateMonitor, mNavigationModeController, mScreenLifecycle, mSysUiState, Optional.of(mPip), Optional.of(mSplitScreen), Optional.of(mOneHanded), Optional.of(mHideDisplayCutout), mProtoTracer, - Optional.of(mShellDump)); + Optional.of(mShellDump), Optional.of(mAppPairs)); when(mPip.getPipTouchHandler()).thenReturn(mPipTouchHandler); } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index ac9c28935211a..90c3f9ec21ef0 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -5888,7 +5888,21 @@ class Task extends WindowContainer { try { // Protect against recursion. mInResumeTopActivity = true; - result = resumeTopActivityInnerLocked(prev, options); + + // TODO(b/172885410): Allow the top activities of all visible leaf tasks to be resumed + if (mCreatedByOrganizer && !isLeafTask() + && getConfiguration().windowConfiguration.getWindowingMode() + == WINDOWING_MODE_FULLSCREEN) { + for (int i = mChildren.size() - 1; i >= 0; i--) { + final Task child = (Task) getChildAt(i); + if (!child.shouldBeVisible(null /* starting */)) { + break; + } + result |= child.resumeTopActivityUncheckedLocked(prev, options); + } + } else { + result = resumeTopActivityInnerLocked(prev, options); + } // When resuming the top activity, it may be necessary to pause the top activity (for // example, returning to the lock screen. We suppress the normal pause logic in diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index f11cd933616b1..499fbf61a1c35 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -434,8 +434,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if (hop.isReparent()) { final boolean isNonOrganizedRootableTask = - (task.isRootTask() && !task.mCreatedByOrganizer) - || task.getParent().asTask().mCreatedByOrganizer; + task.isRootTask() || task.getParent().asTask().mCreatedByOrganizer; if (isNonOrganizedRootableTask) { WindowContainer newParent = hop.getNewParent() == null ? dc.getDefaultTaskDisplayArea()