diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java index 5e5d14f552405..0ed7ca79113e1 100644 --- a/core/java/android/view/WindowlessWindowManager.java +++ b/core/java/android/view/WindowlessWindowManager.java @@ -207,12 +207,19 @@ public class WindowlessWindowManager implements IWindowSession { } /** @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 0000000000000..b86f36a14d17d --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/split_divider.xml @@ -0,0 +1,27 @@ + + + + + + + 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 d30acee4be6a9..f199072f7bcac 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 androidx.annotation.NonNull; 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 @@ import java.io.PrintWriter; * {@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 @@ class AppPair implements ShellTaskOrganizer.TaskListener { 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 @@ class AppPair implements ShellTaskOrganizer.TaskListener { 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 @@ class AppPair implements ShellTaskOrganizer.TaskListener { .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 @@ class AppPair implements ShellTaskOrganizer.TaskListener { mTaskInfo1 = null; mTaskInfo2 = null; + mAppPairLayout.release(); + mAppPairLayout = null; + } + + void setVisible(boolean visible) { + if (mAppPairLayout == null) { + return; + } + mAppPairLayout.setDividerVisibility(visible); } @Override @@ -150,13 +160,20 @@ class AppPair implements ShellTaskOrganizer.TaskListener { 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 @@ class AppPair implements ShellTaskOrganizer.TaskListener { 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 0000000000000..f8703f7ec0bc1 --- /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 ef3e3e0220e71..af06764145e35 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 @@ public interface AppPairs { 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 e0c7ba9234b24..925a4f36d5e65 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 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.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 @@ public class AppPairsController implements AppPairs { private AppPairsPool mPairsPool; // Active app-pairs mapped by root task id key. private final SparseArray 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 @@ -60,6 +70,27 @@ public class AppPairsController implements AppPairs { mPairsPool = pool; } + @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); @@ -127,6 +158,10 @@ public class AppPairsController implements AppPairs { 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 0000000000000..41b5e47e7fa94 --- /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 9ab0f89953e4c..754f73246c867 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 @@ import org.mockito.MockitoAnnotations; /** 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 @@ public class AppPairTests { } @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 ed85b67c3feac..6d441ab898ec6 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 @@ import org.mockito.MockitoAnnotations; /** 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 @@ public class AppPairsControllerTests { } @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 4a0fe0f28620f..d3dbbfe379856 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.ext.junit.runners.AndroidJUnit4; 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 @@ public class AppPairsPoolTests { 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 7ea5a1b96c07b..e61cc91c394b2 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 c36acf5b6def7..d5ba2a4b83da8 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 @@ public final class WMShell extends SystemUI private KeyguardUpdateMonitorCallback mSplitScreenKeyguardCallback; private KeyguardUpdateMonitorCallback mPipKeyguardCallback; private KeyguardUpdateMonitorCallback mOneHandedKeyguardCallback; + private KeyguardUpdateMonitorCallback mAppPairsKeyguardCallback; @Inject public WMShell(Context context, CommandQueue commandQueue, @@ -144,6 +145,7 @@ public final class WMShell extends SystemUI mSplitScreenOptional.ifPresent(this::initSplitScreen); mOneHandedOptional.ifPresent(this::initOneHanded); mHideDisplayCutoutOptional.ifPresent(this::initHideDisplayCutout); + mAppPairsOptional.ifPresent(this::initAppPairs); } @VisibleForTesting @@ -292,6 +294,16 @@ public final class WMShell extends SystemUI }); } + 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 ef8a08c565a04..4505b2a87c78a 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 @@ public class WMShellModule { @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 802c8f9f1ec9a..73801c7e55e9e 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 @@ final class TaskDisplayArea extends DisplayArea { } } 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);