summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author TreeHugger Robot <treehugger-gerrit@google.com> 2020-12-04 03:03:34 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2020-12-04 03:03:34 +0000
commit1fa042b64f4d45e8c502d424d78c925d23b134d8 (patch)
tree04043a14763847571f0afd22ecdadf05378494d0
parent089d0f680b48f527397c5a94fc69ffa7356c9e4c (diff)
parent0c9c2cfac1f63f7447b4a2da3843568d572c3dd1 (diff)
Merge "Support dragging divider bar of app-pair to resize the splits"
-rw-r--r--libs/WindowManager/Shell/res/layout/split_divider.xml4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java91
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairLayout.java211
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/DividerView.java56
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java171
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java205
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java115
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairLayoutTests.java89
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairTests.java6
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsControllerTests.java6
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsPoolTests.java7
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java106
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java66
13 files changed, 743 insertions, 390 deletions
diff --git a/libs/WindowManager/Shell/res/layout/split_divider.xml b/libs/WindowManager/Shell/res/layout/split_divider.xml
index b86f36a14d17..341fe617b2d0 100644
--- a/libs/WindowManager/Shell/res/layout/split_divider.xml
+++ b/libs/WindowManager/Shell/res/layout/split_divider.xml
@@ -14,7 +14,7 @@
~ limitations under the License.
-->
-<com.android.wm.shell.apppairs.DividerView
+<com.android.wm.shell.common.split.DividerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="match_parent">
@@ -24,4 +24,4 @@
android:id="@+id/docked_divider_background"
android:background="@color/docked_divider_background"/>
-</com.android.wm.shell.apppairs.DividerView>
+</com.android.wm.shell.common.split.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 d3032f83fc1c..cfbf8452ddae 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
@@ -29,11 +29,13 @@ import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
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.split.SplitLayout;
import java.io.PrintWriter;
@@ -42,7 +44,7 @@ import java.io.PrintWriter;
* {@link #mTaskInfo1} and {@link #mTaskInfo2} in the pair.
* Also includes all UI for managing the pair like the divider.
*/
-class AppPair implements ShellTaskOrganizer.TaskListener {
+class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.LayoutChangeListener {
private static final String TAG = AppPair.class.getSimpleName();
private ActivityManager.RunningTaskInfo mRootTaskInfo;
@@ -55,7 +57,7 @@ class AppPair implements ShellTaskOrganizer.TaskListener {
private final AppPairsController mController;
private final SyncTransactionQueue mSyncQueue;
private final DisplayController mDisplayController;
- private AppPairLayout mAppPairLayout;
+ private SplitLayout mSplitLayout;
AppPair(AppPairsController controller) {
mController = controller;
@@ -92,11 +94,9 @@ class AppPair implements ShellTaskOrganizer.TaskListener {
mTaskInfo1 = task1;
mTaskInfo2 = task2;
- mAppPairLayout = new AppPairLayout(
+ mSplitLayout = new SplitLayout(
mDisplayController.getDisplayContext(mRootTaskInfo.displayId),
- mDisplayController.getDisplay(mRootTaskInfo.displayId),
- mRootTaskInfo.configuration,
- mRootTaskLeash);
+ mRootTaskInfo.configuration, this, mRootTaskLeash);
final WindowContainerToken token1 = task1.token;
final WindowContainerToken token2 = task2.token;
@@ -107,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, mAppPairLayout.getBounds1())
- .setBounds(token2, mAppPairLayout.getBounds2())
+ .setBounds(token1, mSplitLayout.getBounds1())
+ .setBounds(token2, mSplitLayout.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);
@@ -117,6 +117,10 @@ class AppPair implements ShellTaskOrganizer.TaskListener {
}
void unpair() {
+ unpair(null /* toTopToken */);
+ }
+
+ private void unpair(@Nullable WindowContainerToken toTopToken) {
final WindowContainerToken token1 = mTaskInfo1.token;
final WindowContainerToken token2 = mTaskInfo2.token;
final WindowContainerTransaction wct = new WindowContainerTransaction();
@@ -124,16 +128,16 @@ class AppPair implements ShellTaskOrganizer.TaskListener {
// Reparent out of this container and reset windowing mode.
wct.setHidden(mRootTaskInfo.token, true)
.reorder(mRootTaskInfo.token, false)
- .reparent(token1, null, false /* onTop */)
- .reparent(token2, null, false /* onTop */)
+ .reparent(token1, null, token1 == toTopToken /* onTop */)
+ .reparent(token2, null, token2 == toTopToken /* onTop */)
.setWindowingMode(token1, WINDOWING_MODE_UNDEFINED)
.setWindowingMode(token2, WINDOWING_MODE_UNDEFINED);
mController.getTaskOrganizer().applyTransaction(wct);
mTaskInfo1 = null;
mTaskInfo2 = null;
- mAppPairLayout.release();
- mAppPairLayout = null;
+ mSplitLayout.release();
+ mSplitLayout = null;
}
@Override
@@ -153,17 +157,17 @@ class AppPair implements ShellTaskOrganizer.TaskListener {
if (mTaskLeash1 == null || mTaskLeash2 == null) return;
- mAppPairLayout.init();
- final SurfaceControl dividerLeash = mAppPairLayout.getDividerLeash();
- final Rect dividerBounds = mAppPairLayout.getDividerBounds();
+ mSplitLayout.init();
+ final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash();
+ final Rect dividerBounds = mSplitLayout.getDividerBounds();
// TODO: Is there more we need to do here?
mSyncQueue.runInSync(t -> {
- t.setPosition(mTaskLeash1, mTaskInfo1.positionInParent.x,
- mTaskInfo1.positionInParent.y)
+ t.setLayer(dividerLeash, Integer.MAX_VALUE)
+ .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(mTaskLeash1)
@@ -185,14 +189,14 @@ class AppPair implements ShellTaskOrganizer.TaskListener {
}
mRootTaskInfo = taskInfo;
- if (mAppPairLayout != null
- && mAppPairLayout.updateConfiguration(mRootTaskInfo.configuration)) {
+ if (mSplitLayout != null
+ && mSplitLayout.updateConfiguration(mRootTaskInfo.configuration)) {
// Update bounds when root bounds or its 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();
+ final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash();
+ final Rect dividerBounds = mSplitLayout.getDividerBounds();
+ final Rect bounds1 = mSplitLayout.getBounds1();
+ final Rect bounds2 = mSplitLayout.getBounds2();
wct.setBounds(mTaskInfo1.token, bounds1)
.setBounds(mTaskInfo2.token, bounds2);
@@ -200,7 +204,9 @@ class AppPair implements ShellTaskOrganizer.TaskListener {
mSyncQueue.runInSync(t -> t
.setPosition(mTaskLeash1, bounds1.left, bounds1.top)
.setPosition(mTaskLeash2, bounds2.left, bounds2.top)
- .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top));
+ .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top)
+ // Resets layer to divider bar to make sure it is always on top.
+ .setLayer(dividerLeash, Integer.MAX_VALUE));
}
} else if (taskInfo.taskId == getTaskId1()) {
mTaskInfo1 = taskInfo;
@@ -242,4 +248,39 @@ class AppPair implements ShellTaskOrganizer.TaskListener {
public String toString() {
return TAG + "#" + getRootTaskId();
}
+
+ @Override
+ public void onSnappedToDismiss(boolean snappedToEnd) {
+ unpair(snappedToEnd ? mTaskInfo1.token : mTaskInfo2.token /* toTopToken */);
+ }
+
+ @Override
+ public void onBoundsChanging(SplitLayout layout) {
+ final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash();
+ if (dividerLeash == null) return;
+ final Rect dividerBounds = layout.getDividerBounds();
+ final Rect bounds1 = layout.getBounds1();
+ final Rect bounds2 = layout.getBounds2();
+ mSyncQueue.runInSync(t -> t
+ .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top)
+ .setPosition(mTaskLeash1, bounds1.left, bounds1.top)
+ .setPosition(mTaskLeash2, bounds2.left, bounds2.top));
+ }
+
+ @Override
+ public void onBoundsChanged(SplitLayout layout) {
+ final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash();
+ if (dividerLeash == null) return;
+ final Rect dividerBounds = layout.getDividerBounds();
+ final Rect bounds1 = layout.getBounds1();
+ final Rect bounds2 = layout.getBounds2();
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.setBounds(mTaskInfo1.token, bounds1)
+ .setBounds(mTaskInfo2.token, bounds2);
+ mController.getTaskOrganizer().applyTransaction(wct);
+ mSyncQueue.runInSync(t -> t
+ .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top)
+ .setPosition(mTaskLeash1, bounds1.left, bounds1.top)
+ .setPosition(mTaskLeash2, bounds2.left, bounds2.top));
+ }
}
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
deleted file mode 100644
index 8c8655e1ff1f..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairLayout.java
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- * 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.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.
- */
-final class AppPairLayout {
- private static final String DIVIDER_WINDOW_TITLE = "AppPairDivider";
- private final Display mDisplay;
- private final int mDividerWindowWidth;
- private final int mDividerWindowInsets;
- private final AppPairWindowManager mAppPairWindowManager;
-
- private Context mContext;
- private Rect mRootBounds;
- private DIVIDE_POLICY mDividePolicy;
-
- private SurfaceControlViewHost mViewHost;
- private SurfaceControl mDividerLeash;
-
- AppPairLayout(
- Context context,
- Display display,
- Configuration configuration,
- SurfaceControl rootLeash) {
- mContext = context.createConfigurationContext(configuration);
- mDisplay = display;
- 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);
- mDividePolicy = DIVIDE_POLICY.MIDDLE;
- mDividePolicy.update(mRootBounds, mDividerWindowWidth, mDividerWindowInsets);
- }
-
- boolean updateConfiguration(Configuration configuration) {
- mAppPairWindowManager.setConfiguration(configuration);
- final Rect rootBounds = configuration.windowConfiguration.getBounds();
- if (isIdenticalBounds(mRootBounds, rootBounds)) {
- return false;
- }
-
- mContext = mContext.createConfigurationContext(configuration);
- mRootBounds = rootBounds;
- mDividePolicy.update(mRootBounds, mDividerWindowWidth, mDividerWindowInsets);
- release();
- init();
- 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();
- mDividerLeash = null;
- mViewHost = null;
- }
-
- void init() {
- if (mViewHost == null) {
- mViewHost = new SurfaceControlViewHost(mContext, mDisplay, mAppPairWindowManager);
- }
-
- 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);
- mDividerLeash = mAppPairWindowManager.getSurfaceControl(mViewHost.getWindowToken());
- }
-
- 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(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(rootBounds)) {
- 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;
- }
- }
- }
-
- private boolean isLandscape(Rect bounds) {
- return bounds.width() > bounds.height();
- }
-
- 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/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/DividerView.java
deleted file mode 100644
index 41b5e47e7fa9..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/DividerView.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * 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/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
new file mode 100644
index 000000000000..50d9fe8629ac
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
@@ -0,0 +1,171 @@
+/*
+ * 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.common.split;
+
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.SurfaceControlViewHost;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.policy.DividerSnapAlgorithm;
+
+/**
+ * Stack divider for app pair.
+ */
+// TODO(b/172704238): add handle view to indicate touching status.
+public class DividerView extends FrameLayout implements View.OnTouchListener {
+ private final int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
+
+ private SplitLayout mSplitLayout;
+ private SurfaceControlViewHost mViewHost;
+ private DragListener mDragListener;
+
+ private VelocityTracker mVelocityTracker;
+ private boolean mMoving;
+ private int mStartPos;
+
+ 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);
+ }
+
+ /** Sets up essential dependencies of the divider bar. */
+ public void setup(
+ SplitLayout layout,
+ SurfaceControlViewHost viewHost,
+ @Nullable DragListener dragListener) {
+ mSplitLayout = layout;
+ mViewHost = viewHost;
+ mDragListener = dragListener;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ setOnTouchListener(this);
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (mSplitLayout == null) {
+ return false;
+ }
+
+ final int action = event.getAction() & MotionEvent.ACTION_MASK;
+ final boolean isLandscape = isLandscape();
+ // Using raw xy to prevent lost track of motion events while moving divider bar.
+ final int touchPos = isLandscape ? (int) event.getRawX() : (int) event.getRawY();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mVelocityTracker = VelocityTracker.obtain();
+ mVelocityTracker.addMovement(event);
+ setSlippery(false);
+ mStartPos = touchPos;
+ mMoving = false;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ mVelocityTracker.addMovement(event);
+ if (!mMoving && Math.abs(touchPos - mStartPos) > mTouchSlop) {
+ mStartPos = touchPos;
+ mMoving = true;
+ if (mDragListener != null) {
+ mDragListener.onDragStart();
+ }
+ }
+ if (mMoving) {
+ final int position = mSplitLayout.getDividePosition() + touchPos - mStartPos;
+ mSplitLayout.updateDividePosition(position);
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ mVelocityTracker.addMovement(event);
+ mVelocityTracker.computeCurrentVelocity(1000 /* units */);
+ final float velocity = isLandscape
+ ? mVelocityTracker.getXVelocity()
+ : mVelocityTracker.getYVelocity();
+ setSlippery(true);
+ mMoving = false;
+ if (mDragListener != null) {
+ mDragListener.onDragEnd();
+ }
+
+ final int position = mSplitLayout.getDividePosition() + touchPos - mStartPos;
+ final DividerSnapAlgorithm.SnapTarget snapTarget =
+ mSplitLayout.findSnapTarget(position, velocity);
+ mSplitLayout.setSnapTarget(snapTarget);
+ break;
+ }
+ return true;
+ }
+
+ private void setSlippery(boolean slippery) {
+ if (mViewHost == null) {
+ return;
+ }
+
+ final WindowManager.LayoutParams lp = (WindowManager.LayoutParams) getLayoutParams();
+ final boolean isSlippery = (lp.flags & FLAG_SLIPPERY) != 0;
+ if (isSlippery == slippery) {
+ return;
+ }
+
+ if (slippery) {
+ lp.flags |= FLAG_SLIPPERY;
+ } else {
+ lp.flags &= ~FLAG_SLIPPERY;
+ }
+ mViewHost.relayout(lp);
+ }
+
+ private boolean isLandscape() {
+ return getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE;
+ }
+
+ /** Monitors dragging action of the divider bar. */
+ // TODO(b/172704238): add listeners to deal with resizing state of the app windows.
+ public interface DragListener {
+ /** Called when start dragging. */
+ void onDragStart();
+ /** Called when stop dragging. */
+ void onDragEnd();
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
new file mode 100644
index 000000000000..e11037f55cfa
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -0,0 +1,205 @@
+/*
+ * 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.common.split;
+
+import static android.view.WindowManager.DOCKED_LEFT;
+import static android.view.WindowManager.DOCKED_TOP;
+
+import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END;
+import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.view.SurfaceControl;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.policy.DividerSnapAlgorithm;
+
+/**
+ * Records and handles layout of splits. Helps to calculate proper bounds when configuration or
+ * divide position changes.
+ */
+public class SplitLayout {
+ private final int mDividerWindowWidth;
+ private final int mDividerInsets;
+ private final int mDividerSize;
+
+ private final Rect mRootBounds = new Rect();
+ private final Rect mDividerBounds = new Rect();
+ private final Rect mBounds1 = new Rect();
+ private final Rect mBounds2 = new Rect();
+ private final LayoutChangeListener mLayoutChangeListener;
+ private final SplitWindowManager mSplitWindowManager;
+
+ private Context mContext;
+ private DividerSnapAlgorithm mDividerSnapAlgorithm;
+ private int mDividePosition;
+
+ public SplitLayout(Context context, Configuration configuration,
+ LayoutChangeListener layoutChangeListener, SurfaceControl rootLeash) {
+ mContext = context.createConfigurationContext(configuration);
+ mLayoutChangeListener = layoutChangeListener;
+ mSplitWindowManager = new SplitWindowManager(mContext, configuration, rootLeash);
+
+ mDividerWindowWidth = context.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.docked_stack_divider_thickness);
+ mDividerInsets = context.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.docked_stack_divider_insets);
+ mDividerSize = mDividerWindowWidth - mDividerInsets * 2;
+
+ mRootBounds.set(configuration.windowConfiguration.getBounds());
+ mDividerSnapAlgorithm = getSnapAlgorithm(context.getResources(), mRootBounds);
+ mDividePosition = mDividerSnapAlgorithm.getMiddleTarget().position;
+ updateBounds(mDividePosition);
+ }
+
+ /** Gets bounds of the primary split. */
+ public Rect getBounds1() {
+ return mBounds1;
+ }
+
+ /** Gets bounds of the secondary split. */
+ public Rect getBounds2() {
+ return mBounds2;
+ }
+
+ /** Gets bounds of divider window. */
+ public Rect getDividerBounds() {
+ return mDividerBounds;
+ }
+
+ /** Returns leash of the current divider bar. */
+ @Nullable
+ public SurfaceControl getDividerLeash() {
+ return mSplitWindowManager == null ? null : mSplitWindowManager.getSurfaceControl();
+ }
+
+ int getDividePosition() {
+ return mDividePosition;
+ }
+
+ /** Applies new configuration, returns {@code false} if there's no effect to the layout. */
+ public boolean updateConfiguration(Configuration configuration) {
+ final Rect rootBounds = configuration.windowConfiguration.getBounds();
+ if (mRootBounds.equals(rootBounds)) {
+ return false;
+ }
+
+ mContext = mContext.createConfigurationContext(configuration);
+ mSplitWindowManager.setConfiguration(configuration);
+ mRootBounds.set(rootBounds);
+ mDividerSnapAlgorithm = getSnapAlgorithm(mContext.getResources(), mRootBounds);
+ mDividePosition = mDividerSnapAlgorithm.getMiddleTarget().position;
+ updateBounds(mDividePosition);
+ release();
+ init();
+ return true;
+ }
+
+ /** Updates recording bounds of divider window and both of the splits. */
+ private void updateBounds(int position) {
+ mDividerBounds.set(mRootBounds);
+ mBounds1.set(mRootBounds);
+ mBounds2.set(mRootBounds);
+ if (isLandscape(mRootBounds)) {
+ mDividerBounds.left = position - mDividerInsets;
+ mDividerBounds.right = mDividerBounds.left + mDividerWindowWidth;
+ mBounds1.right = mBounds1.left + position;
+ mBounds2.left = mBounds1.right + mDividerSize;
+ } else {
+ mDividerBounds.top = position - mDividerInsets;
+ mDividerBounds.bottom = mDividerBounds.top + mDividerWindowWidth;
+ mBounds1.bottom = mBounds1.top + position;
+ mBounds2.top = mBounds1.bottom + mDividerSize;
+ }
+ }
+
+ /** Inflates {@link DividerView} on the root surface. */
+ public void init() {
+ mSplitWindowManager.init(this);
+ }
+
+ /** Releases the surface holding the current {@link DividerView}. */
+ public void release() {
+ mSplitWindowManager.release();
+ }
+
+ /**
+ * Updates bounds with the passing position. Usually used to update recording bounds while
+ * performing animation or dragging divider bar to resize the splits.
+ */
+ public void updateDividePosition(int position) {
+ updateBounds(position);
+ mLayoutChangeListener.onBoundsChanging(this);
+ }
+
+ /**
+ * Sets new divide position and updates bounds correspondingly. Notifies listener if the new
+ * target indicates dismissing split.
+ */
+ public void setSnapTarget(DividerSnapAlgorithm.SnapTarget snapTarget) {
+ switch(snapTarget.flag) {
+ case FLAG_DISMISS_START:
+ mLayoutChangeListener.onSnappedToDismiss(false /* snappedToEnd */);
+ break;
+ case FLAG_DISMISS_END:
+ mLayoutChangeListener.onSnappedToDismiss(true /* snappedToEnd */);
+ break;
+ default:
+ mDividePosition = snapTarget.position;
+ updateBounds(mDividePosition);
+ mLayoutChangeListener.onBoundsChanged(this);
+ break;
+ }
+ }
+
+ /**
+ * Returns {@link DividerSnapAlgorithm.SnapTarget} which matches passing position and velocity.
+ */
+ public DividerSnapAlgorithm.SnapTarget findSnapTarget(int position, float velocity) {
+ return mDividerSnapAlgorithm.calculateSnapTarget(position, velocity);
+ }
+
+ private DividerSnapAlgorithm getSnapAlgorithm(Resources resources, Rect rootBounds) {
+ final boolean isLandscape = isLandscape(rootBounds);
+ return new DividerSnapAlgorithm(
+ resources,
+ rootBounds.width(),
+ rootBounds.height(),
+ mDividerSize,
+ !isLandscape,
+ new Rect() /* insets */,
+ isLandscape ? DOCKED_LEFT : DOCKED_TOP /* dockSide */);
+ }
+
+ private static boolean isLandscape(Rect bounds) {
+ return bounds.width() > bounds.height();
+ }
+
+ /** Listens layout change event. */
+ public interface LayoutChangeListener {
+ /** Calls when dismissing split. */
+ void onSnappedToDismiss(boolean snappedToEnd);
+ /** Calls when the bounds is changing due to animation or dragging divider bar. */
+ void onBoundsChanging(SplitLayout layout);
+ /** Calls when the target bounds changed. */
+ void onBoundsChanged(SplitLayout layout);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
new file mode 100644
index 000000000000..542867d83e29
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
@@ -0,0 +1,115 @@
+/*
+ * 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.common.split;
+
+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.IWindow;
+import android.view.LayoutInflater;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.WindowManager;
+import android.view.WindowlessWindowManager;
+
+import androidx.annotation.Nullable;
+
+import com.android.wm.shell.R;
+
+/**
+ * Holds view hierarchy of a root surface and helps to inflate {@link DividerView} for a split.
+ */
+public final class SplitWindowManager extends WindowlessWindowManager {
+ private static final String DIVIDER_WINDOW_TITLE = "SplitDivider";
+
+ private Context mContext;
+ private SurfaceControlViewHost mViewHost;
+
+ public SplitWindowManager(Context context, Configuration config, SurfaceControl rootSurface) {
+ super(config, rootSurface, null /* hostInputToken */);
+ mContext = context.createConfigurationContext(config);
+ }
+
+ @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);
+ mContext = mContext.createConfigurationContext(configuration);
+ }
+
+ /** Inflates {@link DividerView} on to the root surface. */
+ void init(SplitLayout splitLayout) {
+ if (mViewHost == null) {
+ mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this);
+ }
+
+ final Rect dividerBounds = splitLayout.getDividerBounds();
+ final DividerView dividerView = (DividerView) LayoutInflater.from(mContext)
+ .inflate(R.layout.split_divider, null /* root */);
+
+ WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+ dividerBounds.width(), dividerBounds.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);
+ dividerView.setup(splitLayout, mViewHost, null /* dragListener */);
+ }
+
+ /**
+ * Releases the surface control of the current {@link DividerView} and tear down the view
+ * hierarchy.
+ */
+ void release() {
+ if (mViewHost == null) return;
+ mViewHost.release();
+ mViewHost = null;
+ }
+
+ /**
+ * Gets {@link SurfaceControl} of the surface holding divider view. @return {@code null} if not
+ * feasible.
+ */
+ @Nullable
+ SurfaceControl getSurfaceControl() {
+ return mViewHost == null ? null : getSurfaceControl(mViewHost.getWindowToken());
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairLayoutTests.java
deleted file mode 100644
index c9d32c4b1f76..000000000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairLayoutTests.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * 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.content.res.Configuration.ORIENTATION_PORTRAIT;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.res.Configuration;
-import android.graphics.Rect;
-import android.view.Display;
-import android.view.SurfaceControl;
-
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.wm.shell.ShellTestCase;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/** Tests for {@link AppPairLayout} */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class AppPairLayoutTests extends ShellTestCase {
- @Mock SurfaceControl mSurfaceControl;
- private Display mDisplay;
- private Configuration mConfiguration;
- private AppPairLayout mAppPairLayout;
-
- @Before
- public void setup() {
- MockitoAnnotations.initMocks(this);
- mConfiguration = getConfiguration(false);
- mDisplay = mContext.getDisplay();
- mAppPairLayout = new AppPairLayout(mContext, mDisplay, mConfiguration, mSurfaceControl);
- }
-
- @After
- @UiThreadTest
- public void tearDown() {
- mAppPairLayout.release();
- }
-
- @Test
- @UiThreadTest
- public void testUpdateConfiguration() {
- assertThat(mAppPairLayout.updateConfiguration(getConfiguration(false))).isFalse();
- assertThat(mAppPairLayout.updateConfiguration(getConfiguration(true))).isTrue();
- }
-
- @Test
- @UiThreadTest
- public void testInitRelease() {
- mAppPairLayout.init();
- assertThat(mAppPairLayout.getDividerLeash()).isNotNull();
- mAppPairLayout.release();
- assertThat(mAppPairLayout.getDividerLeash()).isNull();
- }
-
- private static Configuration getConfiguration(boolean isLandscape) {
- final Configuration configuration = new Configuration();
- configuration.unset();
- configuration.orientation = isLandscape ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT;
- configuration.windowConfiguration.setBounds(
- new Rect(0, 0, isLandscape ? 2160 : 1080, isLandscape ? 1080 : 2160));
- return configuration;
- }
-}
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 f12648a7f709..8dbc1d56bcc2 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
@@ -55,13 +55,13 @@ public class AppPairTests extends ShellTestCase {
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
+ when(mDisplayController.getDisplayContext(anyInt())).thenReturn(mContext);
+ when(mDisplayController.getDisplay(anyInt())).thenReturn(
+ mContext.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY));
mController = new TestAppPairsController(
mTaskOrganizer,
mSyncQueue,
mDisplayController);
- when(mDisplayController.getDisplayContext(anyInt())).thenReturn(mContext);
- when(mDisplayController.getDisplay(anyInt())).thenReturn(
- mContext.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY));
}
@After
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 f8c68d2018da..fada694a4c07 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
@@ -55,14 +55,14 @@ public class AppPairsControllerTests extends ShellTestCase {
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
+ when(mDisplayController.getDisplayContext(anyInt())).thenReturn(mContext);
+ when(mDisplayController.getDisplay(anyInt())).thenReturn(
+ mContext.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY));
mController = new TestAppPairsController(
mTaskOrganizer,
mSyncQueue,
mDisplayController);
mPool = mController.getPool();
- when(mDisplayController.getDisplayContext(anyInt())).thenReturn(mContext);
- when(mDisplayController.getDisplay(anyInt())).thenReturn(
- mContext.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY));
}
@After
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 8ece913de53f..a3f134ee97ed 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
@@ -18,10 +18,14 @@ package com.android.wm.shell.apppairs;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.when;
+
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;
@@ -35,7 +39,7 @@ import org.mockito.MockitoAnnotations;
/** Tests for {@link AppPairsPool} */
@SmallTest
@RunWith(AndroidJUnit4.class)
-public class AppPairsPoolTests {
+public class AppPairsPoolTests extends ShellTestCase {
private TestAppPairsController mController;
private TestAppPairsPool mPool;
@Mock private SyncTransactionQueue mSyncQueue;
@@ -45,6 +49,7 @@ public class AppPairsPoolTests {
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
+ when(mDisplayController.getDisplayContext(anyInt())).thenReturn(mContext);
mController = new TestAppPairsController(
mTaskOrganizer,
mSyncQueue,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
new file mode 100644
index 000000000000..d87f4c60fad4
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
@@ -0,0 +1,106 @@
+/*
+ * 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.common.split;
+
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.view.SurfaceControl;
+
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.policy.DividerSnapAlgorithm;
+import com.android.wm.shell.ShellTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Tests for {@link SplitLayout} */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SplitLayoutTests extends ShellTestCase {
+ @Mock SplitLayout.LayoutChangeListener mLayoutChangeListener;
+ @Mock SurfaceControl mRootLeash;
+ private SplitLayout mSplitLayout;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mSplitLayout = new SplitLayout(
+ mContext,
+ getConfiguration(false),
+ mLayoutChangeListener,
+ mRootLeash);
+ }
+
+ @Test
+ @UiThreadTest
+ public void testUpdateConfiguration() {
+ assertThat(mSplitLayout.updateConfiguration(getConfiguration(false))).isFalse();
+ assertThat(mSplitLayout.updateConfiguration(getConfiguration(true))).isTrue();
+ }
+
+ @Test
+ public void testUpdateDividePosition() {
+ mSplitLayout.updateDividePosition(anyInt());
+ verify(mLayoutChangeListener).onBoundsChanging(any(SplitLayout.class));
+ }
+
+ @Test
+ public void testSetSnapTarget() {
+ DividerSnapAlgorithm.SnapTarget snapTarget = getSnapTarget(0,
+ DividerSnapAlgorithm.SnapTarget.FLAG_NONE);
+ mSplitLayout.setSnapTarget(snapTarget);
+ verify(mLayoutChangeListener).onBoundsChanged(any(SplitLayout.class));
+
+ // verify it callbacks properly when the snap target indicates dismissing split.
+ snapTarget = getSnapTarget(0, DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START);
+ mSplitLayout.setSnapTarget(snapTarget);
+ verify(mLayoutChangeListener).onSnappedToDismiss(eq(false));
+ snapTarget = getSnapTarget(0, DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END);
+ mSplitLayout.setSnapTarget(snapTarget);
+ verify(mLayoutChangeListener).onSnappedToDismiss(eq(true));
+ }
+
+ private static Configuration getConfiguration(boolean isLandscape) {
+ final Configuration configuration = new Configuration();
+ configuration.unset();
+ configuration.orientation = isLandscape ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT;
+ configuration.windowConfiguration.setBounds(
+ new Rect(0, 0, isLandscape ? 2160 : 1080, isLandscape ? 1080 : 2160));
+ return configuration;
+ }
+
+ private static DividerSnapAlgorithm.SnapTarget getSnapTarget(int position, int flag) {
+ return new DividerSnapAlgorithm.SnapTarget(
+ position /* position */, position /* taskPosition */, flag);
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java
new file mode 100644
index 000000000000..aa0eb2f95ed8
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java
@@ -0,0 +1,66 @@
+/*
+ * 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.common.split;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.view.SurfaceControl;
+
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.ShellTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Tests for {@link SplitWindowManager} */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SplitWindowManagerTests extends ShellTestCase {
+ @Mock SurfaceControl mSurfaceControl;
+ @Mock SplitLayout mSplitLayout;
+ private SplitWindowManager mSplitWindowManager;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ final Configuration configuration = new Configuration();
+ configuration.setToDefaults();
+ mSplitWindowManager = new SplitWindowManager(mContext, configuration, mSurfaceControl);
+ when(mSplitLayout.getDividerBounds()).thenReturn(
+ new Rect(0, 0, configuration.windowConfiguration.getBounds().width(),
+ configuration.windowConfiguration.getBounds().height()));
+ }
+
+ @Test
+ @UiThreadTest
+ public void testInitRelease() {
+ mSplitWindowManager.init(mSplitLayout);
+ assertThat(mSplitWindowManager.getSurfaceControl()).isNotNull();
+ mSplitWindowManager.release();
+ assertThat(mSplitWindowManager.getSurfaceControl()).isNull();
+ }
+}