Merge "Support dragging divider bar of app-pair to resize the splits"

This commit is contained in:
TreeHugger Robot
2020-12-04 03:03:34 +00:00
committed by Android (Google) Code Review
13 changed files with 743 additions and 390 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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