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