Merge "Support dragging divider bar of app-pair to resize the splits"
This commit is contained in:
committed by
Android (Google) Code Review
commit
1fa042b64f
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user