App-pairs baseline commit

- AppPairs: Main interface used to pair and unpair tasks.
- AppPairsController: Main controller for app-pairs and implements
AppPairs interface.
- AppPair: Manages a singal instance of an app-pair consisting of a
root task and 2 other tasks that are paired together.
- AppPairPool: Helper object pool for AppPair class so we don't have to
wait for a root task creation from system_server when we need an
app-pair.
- adb shell commands for pairing and unpairing tasks

Test: adb shell commands for pair/unpair
      adb shell dumpsys activity service SystemUIService WMShell \
        pair taskId1 taskId2
      adb shell dumpsys activity service SystemUIService WMShell \
        unpair taskId
Test: AppPairTests, AppPairsPoolTests, AppPairControllerTests
Bug: 172704672
Change-Id: Ie0a70c1aa764ad83b763931501234c1960251ca4
This commit is contained in:
Wale Ogunwale
2020-11-06 20:59:14 -08:00
committed by Jerry Chang
parent 15a494804c
commit fe8c73b18d
23 changed files with 971 additions and 14 deletions

View File

@@ -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",

View File

@@ -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<OneHanded> mOneHandedOptional;
private final Optional<HideDisplayCutout> mHideDisplayCutout;
private final ShellTaskOrganizer mShellTaskOrganizer;
private final Optional<AppPairs> mAppPairsOptional;
public ShellDump(ShellTaskOrganizer shellTaskOrganizer,
Optional<SplitScreen> splitScreenOptional,
Optional<Pip> pipOptional,
Optional<OneHanded> oneHandedOptional,
Optional<HideDisplayCutout> hideDisplayCutout) {
Optional<HideDisplayCutout> hideDisplayCutout,
Optional<AppPairs> 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, ""));
}
}

View File

@@ -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<SplitScreen> mSplitScreenOptional;
private final Optional<AppPairs> mAppPairsOptional;
public ShellInit(DisplayImeController displayImeController,
DragAndDropController dragAndDropController,
ShellTaskOrganizer shellTaskOrganizer,
Optional<SplitScreen> splitScreenOptional) {
Optional<SplitScreen> splitScreenOptional,
Optional<AppPairs> 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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) {

View File

@@ -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<SplitScreen> s);
@BindsInstance
Builder setAppPairs(Optional<AppPairs> s);
@BindsInstance
Builder setOneHanded(Optional<OneHanded> o);

View File

@@ -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<SplitScreen> getSplitScreen();
@WMSingleton
Optional<AppPairs> getAppPairs();
@WMSingleton
Optional<Bubbles> getBubbles();

View File

@@ -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<HideDisplayCutout> mHideDisplayCutoutOptional;
private final ProtoTracer mProtoTracer;
private final Optional<ShellDump> mShellDump;
private final Optional<AppPairs> mAppPairsOptional;
private boolean mIsSysUiStateValid;
private KeyguardUpdateMonitorCallback mSplitScreenKeyguardCallback;
@@ -116,7 +118,8 @@ public final class WMShell extends SystemUI
Optional<OneHanded> oneHandedOptional,
Optional<HideDisplayCutout> hideDisplayCutoutOptional,
ProtoTracer protoTracer,
Optional<ShellDump> shellDump) {
Optional<ShellDump> shellDump,
Optional<AppPairs> 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;

View File

@@ -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<SplitScreen> splitScreenOptional) {
Optional<SplitScreen> splitScreenOptional,
Optional<AppPairs> appPairsOptional) {
return new ShellInit(displayImeController,
dragAndDropController,
shellTaskOrganizer,
splitScreenOptional);
splitScreenOptional,
appPairsOptional);
}
/**
@@ -92,9 +95,10 @@ public abstract class WMShellBaseModule {
Optional<SplitScreen> splitScreenOptional,
Optional<Pip> pipOptional,
Optional<OneHanded> oneHandedOptional,
Optional<HideDisplayCutout> hideDisplayCutout) {
Optional<HideDisplayCutout> hideDisplayCutout,
Optional<AppPairs> 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<Bubbles> provideBubbles(Context context,

View File

@@ -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<Pip> providePip(Context context, DisplayController displayController,

View File

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

View File

@@ -5888,7 +5888,21 @@ class Task extends WindowContainer<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

View File

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