Add divider view support for app-pairs
Add divider bar for app-pairs. AppPairLayout and divide policy records
and handles the layout in pair.
Bug: 172704238
Bug: 172704672
Test: manul check the behavior of the splits and divider bar.
Test: AppPairTests, AppPairsPoolTests, AppPairsControllerTests
Change-Id: I688c7001d056fe8dd9a192885e1f9fd5f004dc11
diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java
index 5e5d14f..0ed7ca7 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -207,12 +207,19 @@
}
/** @hide */
+ @Nullable
protected SurfaceControl getSurfaceControl(View rootView) {
final ViewRootImpl root = rootView.getViewRootImpl();
if (root == null) {
return null;
}
- final State s = mStateForWindow.get(root.mWindow.asBinder());
+ return getSurfaceControl(root.mWindow);
+ }
+
+ /** @hide */
+ @Nullable
+ protected SurfaceControl getSurfaceControl(IWindow window) {
+ final State s = mStateForWindow.get(window.asBinder());
if (s == null) {
return null;
}
diff --git a/libs/WindowManager/Shell/res/layout/split_divider.xml b/libs/WindowManager/Shell/res/layout/split_divider.xml
new file mode 100644
index 0000000..b86f36a
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/split_divider.xml
@@ -0,0 +1,27 @@
+<!--
+ ~ 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.
+ -->
+
+<com.android.wm.shell.apppairs.DividerView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent">
+
+ <View
+ style="@style/DockedDividerBackground"
+ android:id="@+id/docked_divider_background"
+ android:background="@color/docked_divider_background"/>
+
+</com.android.wm.shell.apppairs.DividerView>
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
index d30acee..f199072 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java
@@ -32,6 +32,7 @@
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.SyncTransactionQueue;
import java.io.PrintWriter;
@@ -41,8 +42,6 @@
* {@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();
@@ -55,10 +54,13 @@
private final AppPairsController mController;
private final SyncTransactionQueue mSyncQueue;
+ private final DisplayController mDisplayController;
+ private AppPairLayout mAppPairLayout;
AppPair(AppPairsController controller) {
mController = controller;
mSyncQueue = controller.getSyncTransactionQueue();
+ mDisplayController = controller.getDisplayController();
}
int getRootTaskId() {
@@ -90,13 +92,12 @@
mTaskInfo1 = task1;
mTaskInfo2 = task2;
+ mAppPairLayout = new AppPairLayout(
+ mDisplayController.getDisplayContext(mRootTaskInfo.displayId),
+ mDisplayController.getDisplay(mRootTaskInfo.displayId),
+ mRootTaskInfo.configuration,
+ mRootTaskLeash);
- // 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();
@@ -106,8 +107,8 @@
.reparent(token2, mRootTaskInfo.token, true /* onTop */)
.setWindowingMode(token1, WINDOWING_MODE_MULTI_WINDOW)
.setWindowingMode(token2, WINDOWING_MODE_MULTI_WINDOW)
- .setBounds(token1, bounds1)
- .setBounds(token2, bounds2)
+ .setBounds(token1, mAppPairLayout.getBounds1())
+ .setBounds(token2, mAppPairLayout.getBounds2())
// 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);
@@ -131,6 +132,15 @@
mTaskInfo1 = null;
mTaskInfo2 = null;
+ mAppPairLayout.release();
+ mAppPairLayout = null;
+ }
+
+ void setVisible(boolean visible) {
+ if (mAppPairLayout == null) {
+ return;
+ }
+ mAppPairLayout.setDividerVisibility(visible);
}
@Override
@@ -150,13 +160,20 @@
if (mTaskLeash1 == null || mTaskLeash2 == null) return;
+ setVisible(true);
+ final SurfaceControl dividerLeash = mAppPairLayout.getDividerLeash();
+ final Rect dividerBounds = mAppPairLayout.getDividerBounds();
+
// 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)
+ .setLayer(dividerLeash, Integer.MAX_VALUE)
+ .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top)
.show(mRootTaskLeash)
+ .show(dividerLeash)
.show(mTaskLeash1)
.show(mTaskLeash2));
}
@@ -165,6 +182,24 @@
public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
if (taskInfo.taskId == getRootTaskId()) {
mRootTaskInfo = taskInfo;
+
+ if (mAppPairLayout != null
+ && mAppPairLayout.updateConfiguration(mRootTaskInfo.configuration)) {
+ // Update bounds when there is root bounds or orientation changed.
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ final SurfaceControl dividerLeash = mAppPairLayout.getDividerLeash();
+ final Rect dividerBounds = mAppPairLayout.getDividerBounds();
+ final Rect bounds1 = mAppPairLayout.getBounds1();
+ final Rect bounds2 = mAppPairLayout.getBounds2();
+
+ wct.setBounds(mTaskInfo1.token, bounds1)
+ .setBounds(mTaskInfo2.token, bounds2);
+ mController.getTaskOrganizer().applyTransaction(wct);
+ mSyncQueue.runInSync(t -> t
+ .setPosition(mTaskLeash1, bounds1.left, bounds1.top)
+ .setPosition(mTaskLeash2, bounds2.left, bounds2.top)
+ .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top));
+ }
} else if (taskInfo.taskId == getTaskId1()) {
mTaskInfo1 = taskInfo;
} else if (taskInfo.taskId == getTaskId2()) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairLayout.java
new file mode 100644
index 0000000..f8703f7
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairLayout.java
@@ -0,0 +1,224 @@
+/*
+ * 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.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
+import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY;
+import static android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
+import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
+import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.os.Binder;
+import android.os.IBinder;
+import android.view.Display;
+import android.view.IWindow;
+import android.view.LayoutInflater;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.WindowManager;
+import android.view.WindowlessWindowManager;
+
+import com.android.wm.shell.R;
+
+/**
+ * Records and handles layout of a pair of apps.
+ */
+// TODO(172704238): add tests
+final class AppPairLayout {
+ private static final String DIVIDER_WINDOW_TITLE = "AppPairDivider";
+ private final Context mContext;
+ private final AppPairWindowManager mAppPairWindowManager;
+ private final SurfaceControlViewHost mViewHost;
+
+ private final int mDividerWindowWidth;
+ private final int mDividerWindowInsets;
+
+ private boolean mIsLandscape;
+ private Rect mRootBounds;
+ private DIVIDE_POLICY mDividePolicy;
+
+ private DividerView mDividerView;
+ private SurfaceControl mDividerLeash;
+
+ AppPairLayout(
+ Context context,
+ Display display,
+ Configuration configuration,
+ SurfaceControl rootLeash) {
+ mContext = context.createConfigurationContext(configuration);
+ mIsLandscape = isLandscape(configuration);
+ mRootBounds = configuration.windowConfiguration.getBounds();
+ mDividerWindowWidth = mContext.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.docked_stack_divider_thickness);
+ mDividerWindowInsets = mContext.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.docked_stack_divider_insets);
+
+ mAppPairWindowManager = new AppPairWindowManager(configuration, rootLeash);
+ mViewHost = new SurfaceControlViewHost(mContext, display, mAppPairWindowManager);
+ mDividePolicy = DIVIDE_POLICY.MIDDLE;
+ mDividePolicy.update(mIsLandscape, mRootBounds, mDividerWindowWidth, mDividerWindowInsets);
+ }
+
+ boolean updateConfiguration(Configuration configuration) {
+ mAppPairWindowManager.setConfiguration(configuration);
+ final Rect rootBounds = configuration.windowConfiguration.getBounds();
+ final boolean isLandscape = isLandscape(configuration);
+ if (mIsLandscape == isLandscape && isIdenticalBounds(mRootBounds, rootBounds)) {
+ return false;
+ }
+
+ mIsLandscape = isLandscape;
+ mRootBounds = rootBounds;
+ mDividePolicy.update(mIsLandscape, mRootBounds, mDividerWindowWidth, mDividerWindowInsets);
+ mViewHost.relayout(
+ mDividePolicy.mDividerBounds.width(),
+ mDividePolicy.mDividerBounds.height());
+ // TODO(172704238): handle divider bar rotation.
+ return true;
+ }
+
+ Rect getBounds1() {
+ return mDividePolicy.mBounds1;
+ }
+
+ Rect getBounds2() {
+ return mDividePolicy.mBounds2;
+ }
+
+ Rect getDividerBounds() {
+ return mDividePolicy.mDividerBounds;
+ }
+
+ SurfaceControl getDividerLeash() {
+ return mDividerLeash;
+ }
+
+ void release() {
+ if (mViewHost == null) return;
+ mViewHost.release();
+ }
+
+ void setDividerVisibility(boolean visible) {
+ if (mDividerView == null) {
+ initDivider();
+ }
+ if (visible) {
+ mDividerView.show();
+ } else {
+ mDividerView.hide();
+ }
+ }
+
+ private void initDivider() {
+ final DividerView dividerView = (DividerView) LayoutInflater.from(mContext)
+ .inflate(R.layout.split_divider, null);
+
+ WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+ mDividePolicy.mDividerBounds.width(),
+ mDividePolicy.mDividerBounds.height(),
+ TYPE_DOCK_DIVIDER,
+ FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_WATCH_OUTSIDE_TOUCH
+ | FLAG_SPLIT_TOUCH | FLAG_SLIPPERY,
+ PixelFormat.TRANSLUCENT);
+ lp.token = new Binder();
+ lp.setTitle(DIVIDER_WINDOW_TITLE);
+ lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION;
+
+ mViewHost.setView(dividerView, lp);
+ mDividerView = dividerView;
+ mDividerLeash = mAppPairWindowManager.getSurfaceControl(mViewHost.getWindowToken());
+ }
+
+ private static boolean isLandscape(Configuration configuration) {
+ return configuration.orientation == ORIENTATION_LANDSCAPE;
+ }
+
+ private static boolean isIdenticalBounds(Rect bounds1, Rect bounds2) {
+ return bounds1.left == bounds2.left && bounds1.top == bounds2.top
+ && bounds1.right == bounds2.right && bounds1.bottom == bounds2.bottom;
+ }
+
+ /**
+ * Indicates the policy of placing divider bar and corresponding split-screens.
+ */
+ // TODO(172704238): add more divide policy and provide snap to resize feature for divider bar.
+ enum DIVIDE_POLICY {
+ MIDDLE;
+
+ void update(boolean isLandscape, Rect rootBounds, int dividerWindowWidth,
+ int dividerWindowInsets) {
+ final int dividerOffset = dividerWindowWidth / 2;
+ final int boundsOffset = dividerOffset - dividerWindowInsets;
+
+ mDividerBounds = new Rect(rootBounds);
+ mBounds1 = new Rect(rootBounds);
+ mBounds2 = new Rect(rootBounds);
+
+ switch (this) {
+ case MIDDLE:
+ default:
+ if (isLandscape) {
+ mDividerBounds.left = rootBounds.width() / 2 - dividerOffset;
+ mDividerBounds.right = rootBounds.width() / 2 + dividerOffset;
+ mBounds1.left = rootBounds.width() / 2 + boundsOffset;
+ mBounds2.right = rootBounds.width() / 2 - boundsOffset;
+ } else {
+ mDividerBounds.top = rootBounds.height() / 2 - dividerOffset;
+ mDividerBounds.bottom = rootBounds.height() / 2 + dividerOffset;
+ mBounds1.bottom = rootBounds.height() / 2 - boundsOffset;
+ mBounds2.top = rootBounds.height() / 2 + boundsOffset;
+ }
+ }
+ }
+
+ Rect mDividerBounds;
+ Rect mBounds1;
+ Rect mBounds2;
+ }
+
+ /**
+ * WindowManger for app pair. Holds view hierarchy for the root task.
+ */
+ private static final class AppPairWindowManager extends WindowlessWindowManager {
+ AppPairWindowManager(Configuration config, SurfaceControl rootSurface) {
+ super(config, rootSurface, null /* hostInputToken */);
+ }
+
+ @Override
+ public void setTouchRegion(IBinder window, Region region) {
+ super.setTouchRegion(window, region);
+ }
+
+ @Override
+ public SurfaceControl getSurfaceControl(IWindow window) {
+ return super.getSurfaceControl(window);
+ }
+
+ @Override
+ public void setConfiguration(Configuration configuration) {
+ super.setConfiguration(configuration);
+ }
+ }
+}
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
index ef3e3e0..af06764 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairs.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairs.java
@@ -36,4 +36,6 @@
void dump(@NonNull PrintWriter pw, String prefix);
/** Called when the shell organizer has been registered. */
void onOrganizerRegistered();
+ /** Called when the visibility of the keyguard changes. */
+ void onKeyguardVisibilityChanged(boolean showing);
}
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
index e0c7ba9..925a4f3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java
@@ -16,6 +16,8 @@
package com.android.wm.shell.apppairs;
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG;
import android.app.ActivityManager;
@@ -26,14 +28,17 @@
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.DisplayController;
import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.common.TaskStackListenerCallback;
+import com.android.wm.shell.common.TaskStackListenerImpl;
import java.io.PrintWriter;
/**
* Class manages app-pairs multitasking mode and implements the main interface {@link AppPairs}.
*/
-public class AppPairsController implements AppPairs {
+public class AppPairsController implements AppPairs, TaskStackListenerCallback {
private static final String TAG = AppPairsController.class.getSimpleName();
private final ShellTaskOrganizer mTaskOrganizer;
@@ -42,10 +47,15 @@
private AppPairsPool mPairsPool;
// Active app-pairs mapped by root task id key.
private final SparseArray<AppPair> mActiveAppPairs = new SparseArray<>();
+ private final DisplayController mDisplayController;
+ private int mForegroundTaskId = INVALID_TASK_ID;
- public AppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue) {
+ public AppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue,
+ DisplayController displayController, TaskStackListenerImpl taskStackListener) {
mTaskOrganizer = organizer;
mSyncQueue = syncQueue;
+ mDisplayController = displayController;
+ taskStackListener.addListener(this);
}
@Override
@@ -61,6 +71,27 @@
}
@Override
+ public void onTaskMovedToFront(int taskId) {
+ mForegroundTaskId = INVALID_TASK_ID;
+ for (int i = mActiveAppPairs.size() - 1; i >= 0; --i) {
+ final AppPair candidate = mActiveAppPairs.valueAt(i);
+ final boolean containForegroundTask = candidate.contains(taskId);
+ candidate.setVisible(containForegroundTask);
+ if (containForegroundTask) {
+ mForegroundTaskId = candidate.getRootTaskId();
+ }
+ }
+ }
+
+ @Override
+ public void onKeyguardVisibilityChanged(boolean showing) {
+ if (mForegroundTaskId == INVALID_TASK_ID) {
+ return;
+ }
+ mActiveAppPairs.get(mForegroundTaskId).setVisible(!showing);
+ }
+
+ @Override
public boolean pair(int taskId1, int taskId2) {
final ActivityManager.RunningTaskInfo task1 = mTaskOrganizer.getRunningTaskInfo(taskId1);
final ActivityManager.RunningTaskInfo task2 = mTaskOrganizer.getRunningTaskInfo(taskId2);
@@ -127,6 +158,10 @@
return mSyncQueue;
}
+ DisplayController getDisplayController() {
+ return mDisplayController;
+ }
+
@Override
public void dump(@NonNull PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/DividerView.java
new file mode 100644
index 0000000..41b5e47
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/DividerView.java
@@ -0,0 +1,56 @@
+/*
+ * 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.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Stack divider for app pair.
+ */
+public class DividerView extends FrameLayout {
+ public DividerView(@NonNull Context context) {
+ super(context);
+ }
+
+ public DividerView(@NonNull Context context,
+ @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public DividerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public DividerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ void show() {
+ post(() -> setVisibility(View.VISIBLE));
+ }
+
+ void hide() {
+ post(() -> setVisibility(View.INVISIBLE));
+ }
+}
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
index 9ab0f89..754f732 100644
--- 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
@@ -16,15 +16,25 @@
package com.android.wm.shell.apppairs;
+import static android.view.Display.DEFAULT_DISPLAY;
+
import static com.google.common.truth.Truth.assertThat;
-import android.app.ActivityManager;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.when;
+import android.app.ActivityManager;
+import android.hardware.display.DisplayManager;
+
+import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.common.TaskStackListenerImpl;
import org.junit.After;
import org.junit.Before;
@@ -36,22 +46,32 @@
/** Tests for {@link AppPair} */
@SmallTest
@RunWith(AndroidJUnit4.class)
-public class AppPairTests {
+public class AppPairTests extends ShellTestCase {
private AppPairsController mController;
@Mock private SyncTransactionQueue mSyncQueue;
@Mock private ShellTaskOrganizer mTaskOrganizer;
+ @Mock private DisplayController mDisplayController;
+ @Mock private TaskStackListenerImpl mTaskStackListener;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- mController = new TestAppPairsController(mTaskOrganizer, mSyncQueue);
+ mController = new TestAppPairsController(
+ mTaskOrganizer,
+ mSyncQueue,
+ mDisplayController,
+ mTaskStackListener);
+ when(mDisplayController.getDisplayContext(anyInt())).thenReturn(mContext);
+ when(mDisplayController.getDisplay(anyInt())).thenReturn(
+ mContext.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY));
}
@After
public void tearDown() {}
@Test
+ @UiThreadTest
public void testContains() {
final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build();
final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build();
@@ -66,6 +86,7 @@
}
@Test
+ @UiThreadTest
public void testVanishUnpairs() {
final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build();
final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build();
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
index ed85b67..6d441ab 100644
--- 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
@@ -16,15 +16,25 @@
package com.android.wm.shell.apppairs;
+import static android.view.Display.DEFAULT_DISPLAY;
+
import static com.google.common.truth.Truth.assertThat;
-import android.app.ActivityManager;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.when;
+import android.app.ActivityManager;
+import android.hardware.display.DisplayManager;
+
+import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.common.TaskStackListenerImpl;
import org.junit.After;
import org.junit.Before;
@@ -36,23 +46,33 @@
/** Tests for {@link AppPairsController} */
@SmallTest
@RunWith(AndroidJUnit4.class)
-public class AppPairsControllerTests {
+public class AppPairsControllerTests extends ShellTestCase {
private TestAppPairsController mController;
private TestAppPairsPool mPool;
@Mock private SyncTransactionQueue mSyncQueue;
@Mock private ShellTaskOrganizer mTaskOrganizer;
+ @Mock private DisplayController mDisplayController;
+ @Mock private TaskStackListenerImpl mTaskStackListener;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- mController = new TestAppPairsController(mTaskOrganizer, mSyncQueue);
+ mController = new TestAppPairsController(
+ mTaskOrganizer,
+ mSyncQueue,
+ mDisplayController,
+ mTaskStackListener);
mPool = mController.getPool();
+ when(mDisplayController.getDisplayContext(anyInt())).thenReturn(mContext);
+ when(mDisplayController.getDisplay(anyInt())).thenReturn(
+ mContext.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY));
}
@After
public void tearDown() {}
@Test
+ @UiThreadTest
public void testPairUnpair() {
final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build();
final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build();
@@ -69,6 +89,7 @@
}
@Test
+ @UiThreadTest
public void testUnpair_DontReleaseToPool() {
final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build();
final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build();
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
index 4a0fe0f..d3dbbfe 100644
--- 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
@@ -22,7 +22,9 @@
import androidx.test.filters.SmallTest;
import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.common.TaskStackListenerImpl;
import org.junit.After;
import org.junit.Before;
@@ -39,11 +41,17 @@
private TestAppPairsPool mPool;
@Mock private SyncTransactionQueue mSyncQueue;
@Mock private ShellTaskOrganizer mTaskOrganizer;
+ @Mock private DisplayController mDisplayController;
+ @Mock private TaskStackListenerImpl mTaskStackListener;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- mController = new TestAppPairsController(mTaskOrganizer, mSyncQueue);
+ mController = new TestAppPairsController(
+ mTaskOrganizer,
+ mSyncQueue,
+ mDisplayController,
+ mTaskStackListener);
mPool = mController.getPool();
}
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
index 7ea5a1b..e61cc91 100644
--- 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
@@ -17,13 +17,16 @@
package com.android.wm.shell.apppairs;
import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.common.TaskStackListenerImpl;
public class TestAppPairsController extends AppPairsController {
TestAppPairsPool mPool;
- public TestAppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue) {
- super(organizer, syncQueue);
+ public TestAppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue,
+ DisplayController displayController, TaskStackListenerImpl taskStackListener) {
+ super(organizer, syncQueue, displayController, taskStackListener);
mPool = new TestAppPairsPool(this);
setPairsPool(mPool);
}
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index c36acf5..d5ba2a4 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -105,6 +105,7 @@
private KeyguardUpdateMonitorCallback mSplitScreenKeyguardCallback;
private KeyguardUpdateMonitorCallback mPipKeyguardCallback;
private KeyguardUpdateMonitorCallback mOneHandedKeyguardCallback;
+ private KeyguardUpdateMonitorCallback mAppPairsKeyguardCallback;
@Inject
public WMShell(Context context, CommandQueue commandQueue,
@@ -144,6 +145,7 @@
mSplitScreenOptional.ifPresent(this::initSplitScreen);
mOneHandedOptional.ifPresent(this::initOneHanded);
mHideDisplayCutoutOptional.ifPresent(this::initHideDisplayCutout);
+ mAppPairsOptional.ifPresent(this::initAppPairs);
}
@VisibleForTesting
@@ -292,6 +294,16 @@
});
}
+ void initAppPairs(AppPairs appPairs) {
+ mAppPairsKeyguardCallback = new KeyguardUpdateMonitorCallback() {
+ @Override
+ public void onKeyguardVisibilityChanged(boolean showing) {
+ appPairs.onKeyguardVisibilityChanged(showing);
+ }
+ };
+ mKeyguardUpdateMonitor.registerCallback(mAppPairsKeyguardCallback);
+ }
+
@Override
public void writeToProto(SystemUiTraceProto proto) {
if (proto.wmShell == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java
index ef8a08c..4505b2a 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java
@@ -84,8 +84,10 @@
@WMSingleton
@Provides
static AppPairs provideAppPairs(ShellTaskOrganizer shellTaskOrganizer,
- SyncTransactionQueue syncQueue) {
- return new AppPairsController(shellTaskOrganizer, syncQueue);
+ SyncTransactionQueue syncQueue, DisplayController displayController,
+ TaskStackListenerImpl taskStackListener) {
+ return new AppPairsController(shellTaskOrganizer, syncQueue, displayController,
+ taskStackListener);
}
@WMSingleton
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index 802c8f9..73801c7 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -657,6 +657,13 @@
}
}
return SCREEN_ORIENTATION_UNSPECIFIED;
+ } else {
+ // Apps and their containers are not allowed to specify an orientation of full screen
+ // tasks created by organizer. The organizer handles the orientation instead.
+ final Task task = getTopStackInWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ if (task != null && task.isVisible() && task.mCreatedByOrganizer) {
+ return SCREEN_ORIENTATION_UNSPECIFIED;
+ }
}
final int orientation = super.getOrientation(candidate);