Merge "Baseline support for main/side stage split-screen model"
This commit is contained in:
committed by
Android (Google) Code Review
commit
8f2ca80002
@@ -111,6 +111,7 @@ public class RootTaskDisplayAreaOrganizer extends DisplayAreaOrganizer {
|
||||
}
|
||||
|
||||
mDisplayAreasInfo.put(displayId, displayAreaInfo);
|
||||
mLeashes.put(displayId, leash);
|
||||
|
||||
ArrayList<RootTaskDisplayAreaListener> listeners = mListeners.get(displayId);
|
||||
if (listeners != null) {
|
||||
|
||||
@@ -16,18 +16,16 @@
|
||||
|
||||
package com.android.wm.shell;
|
||||
|
||||
import android.util.Slog;
|
||||
|
||||
import com.android.wm.shell.apppairs.AppPairs;
|
||||
import com.android.wm.shell.common.ShellExecutor;
|
||||
import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
|
||||
import com.android.wm.shell.onehanded.OneHanded;
|
||||
import com.android.wm.shell.pip.Pip;
|
||||
import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
|
||||
import com.android.wm.shell.splitscreen.SplitScreen;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* An entry point into the shell for dumping shell internal state and running adb commands.
|
||||
@@ -38,6 +36,7 @@ public final class ShellCommandHandlerImpl {
|
||||
private static final String TAG = ShellCommandHandlerImpl.class.getSimpleName();
|
||||
|
||||
private final Optional<LegacySplitScreen> mLegacySplitScreenOptional;
|
||||
private final Optional<SplitScreen> mSplitScreenOptional;
|
||||
private final Optional<Pip> mPipOptional;
|
||||
private final Optional<OneHanded> mOneHandedOptional;
|
||||
private final Optional<HideDisplayCutout> mHideDisplayCutout;
|
||||
@@ -49,19 +48,21 @@ public final class ShellCommandHandlerImpl {
|
||||
public static ShellCommandHandler create(
|
||||
ShellTaskOrganizer shellTaskOrganizer,
|
||||
Optional<LegacySplitScreen> legacySplitScreenOptional,
|
||||
Optional<SplitScreen> splitScreenOptional,
|
||||
Optional<Pip> pipOptional,
|
||||
Optional<OneHanded> oneHandedOptional,
|
||||
Optional<HideDisplayCutout> hideDisplayCutout,
|
||||
Optional<AppPairs> appPairsOptional,
|
||||
ShellExecutor mainExecutor) {
|
||||
return new ShellCommandHandlerImpl(shellTaskOrganizer, legacySplitScreenOptional,
|
||||
pipOptional, oneHandedOptional, hideDisplayCutout, appPairsOptional,
|
||||
mainExecutor).mImpl;
|
||||
splitScreenOptional, pipOptional, oneHandedOptional, hideDisplayCutout,
|
||||
appPairsOptional, mainExecutor).mImpl;
|
||||
}
|
||||
|
||||
private ShellCommandHandlerImpl(
|
||||
ShellTaskOrganizer shellTaskOrganizer,
|
||||
Optional<LegacySplitScreen> legacySplitScreenOptional,
|
||||
Optional<SplitScreen> splitScreenOptional,
|
||||
Optional<Pip> pipOptional,
|
||||
Optional<OneHanded> oneHandedOptional,
|
||||
Optional<HideDisplayCutout> hideDisplayCutout,
|
||||
@@ -69,6 +70,7 @@ public final class ShellCommandHandlerImpl {
|
||||
ShellExecutor mainExecutor) {
|
||||
mShellTaskOrganizer = shellTaskOrganizer;
|
||||
mLegacySplitScreenOptional = legacySplitScreenOptional;
|
||||
mSplitScreenOptional = splitScreenOptional;
|
||||
mPipOptional = pipOptional;
|
||||
mOneHandedOptional = oneHandedOptional;
|
||||
mHideDisplayCutout = hideDisplayCutout;
|
||||
@@ -88,6 +90,9 @@ public final class ShellCommandHandlerImpl {
|
||||
pw.println();
|
||||
pw.println();
|
||||
mAppPairsOptional.ifPresent(appPairs -> appPairs.dump(pw, ""));
|
||||
pw.println();
|
||||
pw.println();
|
||||
mSplitScreenOptional.ifPresent(splitScreen -> splitScreen.dump(pw, ""));
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +107,10 @@ public final class ShellCommandHandlerImpl {
|
||||
return runPair(args, pw);
|
||||
case "unpair":
|
||||
return runUnpair(args, pw);
|
||||
case "pinTask":
|
||||
return runPinTask(args, pw);
|
||||
case "unpinTask":
|
||||
return runUnpinTask(args, pw);
|
||||
case "help":
|
||||
return runHelp(pw);
|
||||
default:
|
||||
@@ -109,7 +118,6 @@ public final class ShellCommandHandlerImpl {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private boolean runPair(String[] args, PrintWriter pw) {
|
||||
if (args.length < 4) {
|
||||
// First two arguments are "WMShell" and command name.
|
||||
@@ -133,6 +141,28 @@ public final class ShellCommandHandlerImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean runPinTask(String[] args, PrintWriter pw) {
|
||||
if (args.length < 3) {
|
||||
// First arguments are "WMShell" and command name.
|
||||
pw.println("Error: task id should be provided as arguments");
|
||||
return false;
|
||||
}
|
||||
final int taskId = new Integer(args[2]);
|
||||
mSplitScreenOptional.ifPresent(split -> split.pinTask(taskId));
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean runUnpinTask(String[] args, PrintWriter pw) {
|
||||
if (args.length < 3) {
|
||||
// First arguments are "WMShell" and command name.
|
||||
pw.println("Error: task id should be provided as arguments");
|
||||
return false;
|
||||
}
|
||||
final int taskId = new Integer(args[2]);
|
||||
mSplitScreenOptional.ifPresent(split -> split.unpinTask(taskId));
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean runHelp(PrintWriter pw) {
|
||||
pw.println("Window Manager Shell commands:");
|
||||
pw.println(" help");
|
||||
@@ -142,6 +172,9 @@ public final class ShellCommandHandlerImpl {
|
||||
pw.println(" pair <taskId1> <taskId2>");
|
||||
pw.println(" unpair <taskId>");
|
||||
pw.println(" Pairs/unpairs tasks with given ids.");
|
||||
pw.println(" pinTask <taskId>");
|
||||
pw.println(" unpinTask <taskId>");
|
||||
pw.println(" Pin/Unpin a task with given id in split-screen mode.");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.android.wm.shell.common.ShellExecutor;
|
||||
import com.android.wm.shell.common.annotations.ExternalThread;
|
||||
import com.android.wm.shell.draganddrop.DragAndDropController;
|
||||
import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
|
||||
import com.android.wm.shell.splitscreen.SplitScreen;
|
||||
import com.android.wm.shell.transition.Transitions;
|
||||
|
||||
import java.util.Optional;
|
||||
@@ -38,6 +39,7 @@ public class ShellInitImpl {
|
||||
private final DragAndDropController mDragAndDropController;
|
||||
private final ShellTaskOrganizer mShellTaskOrganizer;
|
||||
private final Optional<LegacySplitScreen> mLegacySplitScreenOptional;
|
||||
private final Optional<SplitScreen> mSplitScreenOptional;
|
||||
private final Optional<AppPairs> mAppPairsOptional;
|
||||
private final FullscreenTaskListener mFullscreenTaskListener;
|
||||
private final ShellExecutor mMainExecutor;
|
||||
@@ -49,6 +51,7 @@ public class ShellInitImpl {
|
||||
DragAndDropController dragAndDropController,
|
||||
ShellTaskOrganizer shellTaskOrganizer,
|
||||
Optional<LegacySplitScreen> legacySplitScreenOptional,
|
||||
Optional<SplitScreen> splitScreenOptional,
|
||||
Optional<AppPairs> appPairsOptional,
|
||||
FullscreenTaskListener fullscreenTaskListener,
|
||||
Transitions transitions,
|
||||
@@ -57,6 +60,7 @@ public class ShellInitImpl {
|
||||
dragAndDropController,
|
||||
shellTaskOrganizer,
|
||||
legacySplitScreenOptional,
|
||||
splitScreenOptional,
|
||||
appPairsOptional,
|
||||
fullscreenTaskListener,
|
||||
transitions,
|
||||
@@ -67,6 +71,7 @@ public class ShellInitImpl {
|
||||
DragAndDropController dragAndDropController,
|
||||
ShellTaskOrganizer shellTaskOrganizer,
|
||||
Optional<LegacySplitScreen> legacySplitScreenOptional,
|
||||
Optional<SplitScreen> splitScreenOptional,
|
||||
Optional<AppPairs> appPairsOptional,
|
||||
FullscreenTaskListener fullscreenTaskListener,
|
||||
Transitions transitions,
|
||||
@@ -75,6 +80,7 @@ public class ShellInitImpl {
|
||||
mDragAndDropController = dragAndDropController;
|
||||
mShellTaskOrganizer = shellTaskOrganizer;
|
||||
mLegacySplitScreenOptional = legacySplitScreenOptional;
|
||||
mSplitScreenOptional = splitScreenOptional;
|
||||
mAppPairsOptional = appPairsOptional;
|
||||
mFullscreenTaskListener = fullscreenTaskListener;
|
||||
mTransitions = transitions;
|
||||
@@ -91,6 +97,7 @@ public class ShellInitImpl {
|
||||
mShellTaskOrganizer.registerOrganizer();
|
||||
|
||||
mAppPairsOptional.ifPresent(AppPairs::onOrganizerRegistered);
|
||||
mSplitScreenOptional.ifPresent(SplitScreen::onOrganizerRegistered);
|
||||
|
||||
// Bind the splitscreen impl to the drag drop controller
|
||||
mDragAndDropController.initialize(mLegacySplitScreenOptional);
|
||||
|
||||
@@ -94,7 +94,7 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.LayoutChan
|
||||
|
||||
mTaskInfo1 = task1;
|
||||
mTaskInfo2 = task2;
|
||||
mSplitLayout = new SplitLayout(
|
||||
mSplitLayout = new SplitLayout(TAG + "SplitDivider",
|
||||
mDisplayController.getDisplayContext(mRootTaskInfo.displayId),
|
||||
mRootTaskInfo.configuration, this /* layoutChangeListener */,
|
||||
b -> b.setParent(mRootTaskLeash));
|
||||
@@ -248,13 +248,13 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.LayoutChan
|
||||
final Rect bounds1 = layout.getBounds1();
|
||||
final Rect bounds2 = layout.getBounds2();
|
||||
mSyncQueue.runInSync(t -> t
|
||||
// Ignores the original surface bounds so that the app could fill up the gap
|
||||
// between each surface with corresponding background while resizing.
|
||||
.setWindowCrop(mTaskLeash1, bounds1.width(), bounds1.height())
|
||||
.setWindowCrop(mTaskLeash2, bounds2.width(), bounds2.height())
|
||||
.setPosition(dividerLeash, dividerBounds.left, dividerBounds.top)
|
||||
.setPosition(mTaskLeash1, bounds1.left, bounds1.top)
|
||||
.setPosition(mTaskLeash2, bounds2.left, bounds2.top));
|
||||
.setPosition(mTaskLeash2, bounds2.left, bounds2.top)
|
||||
// Sets crop to prevent visible region of tasks overlap with each other when
|
||||
// re-positioning surfaces while resizing.
|
||||
.setWindowCrop(mTaskLeash1, bounds1.width(), bounds1.height())
|
||||
.setWindowCrop(mTaskLeash2, bounds2.width(), bounds2.height()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -271,10 +271,11 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.LayoutChan
|
||||
mSyncQueue.runInSync(t -> t
|
||||
// Resets layer of divider bar to make sure it is always on top.
|
||||
.setLayer(dividerLeash, Integer.MAX_VALUE)
|
||||
.setWindowCrop(mTaskLeash1, bounds1.width(), bounds1.height())
|
||||
.setWindowCrop(mTaskLeash2, bounds2.width(), bounds2.height())
|
||||
.setPosition(dividerLeash, dividerBounds.left, dividerBounds.top)
|
||||
.setPosition(mTaskLeash1, bounds1.left, bounds1.top)
|
||||
.setPosition(mTaskLeash2, bounds2.left, bounds2.top));
|
||||
.setPosition(mTaskLeash2, bounds2.left, bounds2.top)
|
||||
// Resets crop to apply new surface bounds directly.
|
||||
.setWindowCrop(mTaskLeash1, null)
|
||||
.setWindowCrop(mTaskLeash2, null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,14 +55,15 @@ public class SplitLayout {
|
||||
private Context mContext;
|
||||
private DividerSnapAlgorithm mDividerSnapAlgorithm;
|
||||
private int mDividePosition;
|
||||
private boolean mInitialized = false;
|
||||
|
||||
public SplitLayout(Context context, Configuration configuration,
|
||||
public SplitLayout(String windowName, Context context, Configuration configuration,
|
||||
LayoutChangeListener layoutChangeListener,
|
||||
SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks) {
|
||||
mContext = context.createConfigurationContext(configuration);
|
||||
mLayoutChangeListener = layoutChangeListener;
|
||||
mSplitWindowManager = new SplitWindowManager(
|
||||
mContext, configuration, parentContainerCallbacks);
|
||||
windowName, mContext, configuration, parentContainerCallbacks);
|
||||
|
||||
mDividerWindowWidth = context.getResources().getDimensionPixelSize(
|
||||
com.android.internal.R.dimen.docked_stack_divider_thickness);
|
||||
@@ -72,8 +73,7 @@ public class SplitLayout {
|
||||
|
||||
mRootBounds.set(configuration.windowConfiguration.getBounds());
|
||||
mDividerSnapAlgorithm = getSnapAlgorithm(context.getResources(), mRootBounds);
|
||||
mDividePosition = mDividerSnapAlgorithm.getMiddleTarget().position;
|
||||
updateBounds(mDividePosition);
|
||||
resetDividerPosition();
|
||||
}
|
||||
|
||||
/** Gets bounds of the primary split. */
|
||||
@@ -112,8 +112,13 @@ public class SplitLayout {
|
||||
mSplitWindowManager.setConfiguration(configuration);
|
||||
mRootBounds.set(rootBounds);
|
||||
mDividerSnapAlgorithm = getSnapAlgorithm(mContext.getResources(), mRootBounds);
|
||||
mDividePosition = mDividerSnapAlgorithm.getMiddleTarget().position;
|
||||
updateBounds(mDividePosition);
|
||||
resetDividerPosition();
|
||||
|
||||
// Don't inflate divider bar if it is not initialized.
|
||||
if (!mInitialized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
release();
|
||||
init();
|
||||
return true;
|
||||
@@ -141,11 +146,15 @@ public class SplitLayout {
|
||||
|
||||
/** Inflates {@link DividerView} on the root surface. */
|
||||
public void init() {
|
||||
if (mInitialized) return;
|
||||
mInitialized = true;
|
||||
mSplitWindowManager.init(this);
|
||||
}
|
||||
|
||||
/** Releases the surface holding the current {@link DividerView}. */
|
||||
public void release() {
|
||||
if (!mInitialized) return;
|
||||
mInitialized = false;
|
||||
mSplitWindowManager.release();
|
||||
}
|
||||
|
||||
@@ -166,6 +175,12 @@ public class SplitLayout {
|
||||
mSplitWindowManager.setResizingSplits(false);
|
||||
}
|
||||
|
||||
/** Resets divider position. */
|
||||
public void resetDividerPosition() {
|
||||
mDividePosition = mDividerSnapAlgorithm.getMiddleTarget().position;
|
||||
updateBounds(mDividePosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets new divide position and updates bounds correspondingly. Notifies listener if the new
|
||||
* target indicates dismissing split.
|
||||
|
||||
@@ -51,22 +51,23 @@ import com.android.wm.shell.R;
|
||||
*/
|
||||
public final class SplitWindowManager extends WindowlessWindowManager {
|
||||
private static final String TAG = SplitWindowManager.class.getSimpleName();
|
||||
private static final String DIVIDER_WINDOW_TITLE = "SplitDivider";
|
||||
|
||||
private final ParentContainerCallbacks mParentContainerCallbacks;
|
||||
private Context mContext;
|
||||
private SurfaceControlViewHost mViewHost;
|
||||
private boolean mResizingSplits;
|
||||
private final String mWindowName;
|
||||
|
||||
public interface ParentContainerCallbacks {
|
||||
void attachToParentSurface(SurfaceControl.Builder b);
|
||||
}
|
||||
|
||||
public SplitWindowManager(Context context, Configuration config,
|
||||
public SplitWindowManager(String windowName, Context context, Configuration config,
|
||||
ParentContainerCallbacks parentContainerCallbacks) {
|
||||
super(config, null /* rootSurface */, null /* hostInputToken */);
|
||||
mContext = context.createConfigurationContext(config);
|
||||
mParentContainerCallbacks = parentContainerCallbacks;
|
||||
mWindowName = windowName;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -106,7 +107,7 @@ public final class SplitWindowManager extends WindowlessWindowManager {
|
||||
| FLAG_SPLIT_TOUCH | FLAG_SLIPPERY,
|
||||
PixelFormat.TRANSLUCENT);
|
||||
lp.token = new Binder();
|
||||
lp.setTitle(DIVIDER_WINDOW_TITLE);
|
||||
lp.setTitle(mWindowName);
|
||||
lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY;
|
||||
mViewHost.setView(dividerView, lp);
|
||||
dividerView.setup(splitLayout, mViewHost);
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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.splitscreen;
|
||||
|
||||
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
|
||||
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
|
||||
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
|
||||
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.window.WindowContainerToken;
|
||||
import android.window.WindowContainerTransaction;
|
||||
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.common.SyncTransactionQueue;
|
||||
|
||||
/**
|
||||
* Main stage for split-screen mode. When split-screen is active all standard activity types launch
|
||||
* on the main stage, except for task that are explicitly pinned to the {@link SideStage}.
|
||||
* @see StageCoordinator
|
||||
*/
|
||||
class MainStage extends StageTaskListener {
|
||||
private static final String TAG = MainStage.class.getSimpleName();
|
||||
|
||||
private boolean mIsActive = false;
|
||||
|
||||
private static final int[] CONTROLLED_ACTIVITY_TYPES = {ACTIVITY_TYPE_STANDARD};
|
||||
private static final int[] CONTROLLED_WINDOWING_MODES =
|
||||
{WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED};
|
||||
private static final int[] CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE =
|
||||
{WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED, WINDOWING_MODE_MULTI_WINDOW};
|
||||
|
||||
MainStage(ShellTaskOrganizer taskOrganizer, int displayId,
|
||||
StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue) {
|
||||
super(taskOrganizer, displayId, callbacks, syncQueue);
|
||||
}
|
||||
|
||||
boolean isActive() {
|
||||
return mIsActive;
|
||||
}
|
||||
|
||||
void activate(Rect rootBounds, WindowContainerTransaction wct) {
|
||||
if (mIsActive) return;
|
||||
|
||||
final WindowContainerToken rootToken = mRootTaskInfo.token;
|
||||
wct.setHidden(rootToken, false)
|
||||
.setBounds(rootToken, rootBounds)
|
||||
.setLaunchRoot(
|
||||
rootToken,
|
||||
CONTROLLED_WINDOWING_MODES,
|
||||
CONTROLLED_ACTIVITY_TYPES)
|
||||
.reparentTasks(
|
||||
null /* currentParent */,
|
||||
rootToken,
|
||||
CONTROLLED_WINDOWING_MODES,
|
||||
CONTROLLED_ACTIVITY_TYPES,
|
||||
true /* onTop */)
|
||||
// Moving the root task to top after the child tasks were repareted , or the root
|
||||
// task cannot be visible and focused.
|
||||
.reorder(rootToken, true /* onTop */);
|
||||
|
||||
mIsActive = true;
|
||||
}
|
||||
|
||||
void deactivate(WindowContainerTransaction wct) {
|
||||
if (!mIsActive) return;
|
||||
mIsActive = false;
|
||||
|
||||
if (mRootTaskInfo == null) return;
|
||||
final WindowContainerToken rootToken = mRootTaskInfo.token;
|
||||
wct.setHidden(rootToken, true)
|
||||
.setLaunchRoot(
|
||||
rootToken,
|
||||
null,
|
||||
null)
|
||||
.reparentTasks(
|
||||
rootToken,
|
||||
null /* newParent */,
|
||||
CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE,
|
||||
CONTROLLED_ACTIVITY_TYPES,
|
||||
true /* onTop */)
|
||||
.reorder(rootToken, false /* onTop */);
|
||||
}
|
||||
|
||||
void updateConfiguration(int windowingMode, Rect bounds, WindowContainerTransaction wct) {
|
||||
wct.setBounds(mRootTaskInfo.token, bounds)
|
||||
.setWindowingMode(mRootTaskInfo.token, windowingMode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.splitscreen;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.graphics.Rect;
|
||||
import android.window.WindowContainerToken;
|
||||
import android.window.WindowContainerTransaction;
|
||||
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.common.SyncTransactionQueue;
|
||||
|
||||
/**
|
||||
* Side stage for split-screen mode. Only tasks that are explicitly pinned to this stage show up
|
||||
* here. All other task are launch in the {@link MainStage}.
|
||||
* @see StageCoordinator
|
||||
*/
|
||||
class SideStage extends StageTaskListener {
|
||||
private static final String TAG = SideStage.class.getSimpleName();
|
||||
|
||||
SideStage(ShellTaskOrganizer taskOrganizer, int displayId,
|
||||
StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue) {
|
||||
super(taskOrganizer, displayId, callbacks, syncQueue);
|
||||
}
|
||||
|
||||
public void addTask(ActivityManager.RunningTaskInfo task, Rect rootBounds,
|
||||
WindowContainerTransaction wct) {
|
||||
final WindowContainerToken rootToken = mRootTaskInfo.token;
|
||||
wct.setHidden(rootToken, false)
|
||||
.setBounds(rootToken, rootBounds)
|
||||
.reparent(task.token, rootToken, true /* onTop*/)
|
||||
// Moving the root task to top after the child tasks were repareted , or the root
|
||||
// task cannot be visible and focused.
|
||||
.reorder(rootToken, true);
|
||||
}
|
||||
|
||||
public boolean removeTask(int taskId, WindowContainerTransaction wct) {
|
||||
final ActivityManager.RunningTaskInfo task = mChildrenTaskInfo.get(taskId);
|
||||
if (task == null) return false;
|
||||
|
||||
wct.setHidden(mRootTaskInfo.token, true)
|
||||
.reorder(mRootTaskInfo.token, false)
|
||||
.reparent(task.token, null, false /* onTop */);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.splitscreen;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.wm.shell.common.annotations.ExternalThread;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
|
||||
/**
|
||||
* Interface to engage split-screen feature.
|
||||
*/
|
||||
@ExternalThread
|
||||
public interface SplitScreen {
|
||||
/** Unpin a task in the side-stage of split-screen. */
|
||||
boolean pinTask(int taskId);
|
||||
/** Unpin a task in the side-stage of split-screen. */
|
||||
boolean pinTask(ActivityManager.RunningTaskInfo task);
|
||||
/** Unpin a task from the side-stage of split-screen. */
|
||||
boolean unpinTask(int taskId);
|
||||
// TODO: Do we need show/hide side stage or is startActivity and sendToBack good enough?
|
||||
/** Dumps current status of split-screen. */
|
||||
void dump(@NonNull PrintWriter pw, String prefix);
|
||||
/** Called when the shell organizer has been registered. */
|
||||
void onOrganizerRegistered();
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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.splitscreen;
|
||||
|
||||
import static android.view.Display.DEFAULT_DISPLAY;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.common.SyncTransactionQueue;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
|
||||
/**
|
||||
* Class manages split-screen multitasking mode and implements the main interface
|
||||
* {@link SplitScreen}.
|
||||
* @see StageCoordinator
|
||||
*/
|
||||
public class SplitScreenController implements SplitScreen {
|
||||
private static final String TAG = SplitScreenController.class.getSimpleName();
|
||||
|
||||
private final ShellTaskOrganizer mTaskOrganizer;
|
||||
private final SyncTransactionQueue mSyncQueue;
|
||||
private final Context mContext;
|
||||
private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
|
||||
private StageCoordinator mStageCoordinator;
|
||||
|
||||
public SplitScreenController(ShellTaskOrganizer shellTaskOrganizer,
|
||||
SyncTransactionQueue syncQueue, Context context,
|
||||
RootTaskDisplayAreaOrganizer rootTDAOrganizer) {
|
||||
mTaskOrganizer = shellTaskOrganizer;
|
||||
mSyncQueue = syncQueue;
|
||||
mContext = context;
|
||||
mRootTDAOrganizer = rootTDAOrganizer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOrganizerRegistered() {
|
||||
if (mStageCoordinator == null) {
|
||||
// TODO: Multi-display
|
||||
mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
|
||||
mRootTDAOrganizer, mTaskOrganizer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean pinTask(int taskId) {
|
||||
final ActivityManager.RunningTaskInfo task = mTaskOrganizer.getRunningTaskInfo(taskId);
|
||||
return task != null && pinTask(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean pinTask(ActivityManager.RunningTaskInfo task) {
|
||||
return mStageCoordinator.pinTask(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unpinTask(int taskId) {
|
||||
return mStageCoordinator.unpinTask(taskId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dump(@NonNull PrintWriter pw, String prefix) {
|
||||
pw.println(prefix + TAG);
|
||||
if (mStageCoordinator != null) {
|
||||
mStageCoordinator.dump(pw, prefix);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
/*
|
||||
* 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.splitscreen;
|
||||
|
||||
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
|
||||
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.view.SurfaceControl;
|
||||
import android.window.DisplayAreaInfo;
|
||||
import android.window.WindowContainerTransaction;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.common.SyncTransactionQueue;
|
||||
import com.android.wm.shell.common.split.SplitLayout;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
|
||||
/**
|
||||
* Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and
|
||||
* {@link SideStage} stages.
|
||||
* Some high-level rules:
|
||||
* - The {@link StageCoordinator} is only considered active if the {@link SideStage} contains at
|
||||
* least one child task.
|
||||
* - The {@link MainStage} should only have children if the coordinator is active.
|
||||
* - The {@link SplitLayout} divider is only visible if both the {@link MainStage}
|
||||
* and {@link SideStage} are visible.
|
||||
* - The {@link MainStage} configuration is fullscreen when the {@link SideStage} isn't visible.
|
||||
* This rules are mostly implemented in {@link #onStageVisibilityChanged(StageListenerImpl)} and
|
||||
* {@link #onStageHasChildrenChanged(StageListenerImpl).}
|
||||
*/
|
||||
class StageCoordinator implements SplitLayout.LayoutChangeListener,
|
||||
RootTaskDisplayAreaOrganizer.RootTaskDisplayAreaListener {
|
||||
|
||||
private static final String TAG = StageCoordinator.class.getSimpleName();
|
||||
|
||||
private final MainStage mMainStage;
|
||||
private final StageListenerImpl mMainStageListener = new StageListenerImpl();
|
||||
private final SideStage mSideStage;
|
||||
private final StageListenerImpl mSideStageListener = new StageListenerImpl();
|
||||
|
||||
private final int mDisplayId;
|
||||
private SplitLayout mSplitLayout;
|
||||
private boolean mDividerVisible;
|
||||
private final SyncTransactionQueue mSyncQueue;
|
||||
private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
|
||||
private final ShellTaskOrganizer mTaskOrganizer;
|
||||
private DisplayAreaInfo mDisplayAreaInfo;
|
||||
private final Context mContext;
|
||||
|
||||
StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
|
||||
RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer) {
|
||||
mContext = context;
|
||||
mDisplayId = displayId;
|
||||
mSyncQueue = syncQueue;
|
||||
mRootTDAOrganizer = rootTDAOrganizer;
|
||||
mTaskOrganizer = taskOrganizer;
|
||||
mMainStage = new MainStage(mTaskOrganizer, mDisplayId, mMainStageListener, mSyncQueue);
|
||||
mSideStage = new SideStage(mTaskOrganizer, mDisplayId, mSideStageListener, mSyncQueue);
|
||||
mRootTDAOrganizer.registerListener(displayId, this);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
|
||||
RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer,
|
||||
MainStage mainStage, SideStage sideStage) {
|
||||
mContext = context;
|
||||
mDisplayId = displayId;
|
||||
mSyncQueue = syncQueue;
|
||||
mRootTDAOrganizer = rootTDAOrganizer;
|
||||
mTaskOrganizer = taskOrganizer;
|
||||
mMainStage = mainStage;
|
||||
mSideStage = sideStage;
|
||||
mRootTDAOrganizer.registerListener(displayId, this);
|
||||
}
|
||||
|
||||
boolean pinTask(ActivityManager.RunningTaskInfo task) {
|
||||
final WindowContainerTransaction wct = new WindowContainerTransaction();
|
||||
|
||||
mMainStage.activate(getMainStageBounds(), wct);
|
||||
mSideStage.addTask(task, getSideStageBounds(), wct);
|
||||
mTaskOrganizer.applyTransaction(wct);
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean unpinTask(int taskId) {
|
||||
final WindowContainerTransaction wct = new WindowContainerTransaction();
|
||||
|
||||
/**
|
||||
* {@link MainStage} will be deactivated in {@link #onStageHasChildrenChanged} if the
|
||||
* {@link SideStage} no longer has children.
|
||||
*/
|
||||
final boolean result = mSideStage.removeTask(taskId, wct);
|
||||
mTaskOrganizer.applyTransaction(wct);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void onStageRootTaskVanished(StageListenerImpl stageListener) {
|
||||
if (stageListener == mMainStageListener || stageListener == mSideStageListener) {
|
||||
final WindowContainerTransaction wct = new WindowContainerTransaction();
|
||||
// Deactivate the main stage if it no longer has a root task.
|
||||
mMainStage.deactivate(wct);
|
||||
mTaskOrganizer.applyTransaction(wct);
|
||||
}
|
||||
}
|
||||
|
||||
private void onStageVisibilityChanged(StageListenerImpl stageListener) {
|
||||
final boolean sideStageVisible = mSideStageListener.mVisible;
|
||||
final boolean mainStageVisible = mMainStageListener.mVisible;
|
||||
// Divider is only visible if both the main stage and side stages are visible
|
||||
final boolean dividerVisible = sideStageVisible && mainStageVisible;
|
||||
|
||||
if (mDividerVisible != dividerVisible) {
|
||||
mDividerVisible = dividerVisible;
|
||||
if (mDividerVisible) {
|
||||
mSplitLayout.init();
|
||||
} else {
|
||||
mSplitLayout.release();
|
||||
}
|
||||
}
|
||||
|
||||
if (mainStageVisible) {
|
||||
final WindowContainerTransaction wct = new WindowContainerTransaction();
|
||||
if (sideStageVisible) {
|
||||
// The main stage configuration should to follow split layout when side stage is
|
||||
// visible.
|
||||
mMainStage.updateConfiguration(
|
||||
WINDOWING_MODE_MULTI_WINDOW, getMainStageBounds(), wct);
|
||||
} else {
|
||||
// We want the main stage configuration to be fullscreen when the side stage isn't
|
||||
// visible.
|
||||
mMainStage.updateConfiguration(WINDOWING_MODE_FULLSCREEN, null, wct);
|
||||
}
|
||||
// TODO: Change to `mSyncQueue.queue(wct)` once BLAST is stable.
|
||||
mTaskOrganizer.applyTransaction(wct);
|
||||
}
|
||||
|
||||
mSyncQueue.runInSync(t -> {
|
||||
final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash();
|
||||
final SurfaceControl sideStageLeash = mSideStage.mRootLeash;
|
||||
final SurfaceControl mainStageLeash = mMainStage.mRootLeash;
|
||||
|
||||
if (dividerLeash != null) {
|
||||
if (mDividerVisible) {
|
||||
t.show(dividerLeash)
|
||||
.setLayer(dividerLeash, Integer.MAX_VALUE)
|
||||
.setPosition(dividerLeash,
|
||||
mSplitLayout.getDividerBounds().left,
|
||||
mSplitLayout.getDividerBounds().top);
|
||||
} else {
|
||||
t.hide(dividerLeash);
|
||||
}
|
||||
}
|
||||
if (sideStageVisible) {
|
||||
final Rect sideStageBounds = getSideStageBounds();
|
||||
t.show(sideStageLeash)
|
||||
.setPosition(sideStageLeash,
|
||||
sideStageBounds.left, sideStageBounds.top)
|
||||
.setWindowCrop(sideStageLeash,
|
||||
sideStageBounds.width(), sideStageBounds.height());
|
||||
} else {
|
||||
t.hide(sideStageLeash);
|
||||
}
|
||||
if (mainStageVisible) {
|
||||
final Rect mainStageBounds = getMainStageBounds();
|
||||
t.show(mainStageLeash)
|
||||
.setPosition(mainStageLeash,
|
||||
mainStageBounds.left, mainStageBounds.top);
|
||||
if (sideStageVisible) {
|
||||
t.setWindowCrop(mainStageLeash,
|
||||
mainStageBounds.width(), mainStageBounds.height());
|
||||
} else {
|
||||
// Clear window crop if side stage isn't visible.
|
||||
t.setWindowCrop(mainStageLeash, null);
|
||||
}
|
||||
} else {
|
||||
t.hide(mainStageLeash);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onStageHasChildrenChanged(StageListenerImpl stageListener) {
|
||||
if (stageListener == mSideStageListener) {
|
||||
final WindowContainerTransaction wct = new WindowContainerTransaction();
|
||||
if (mSideStageListener.mHasChildren) {
|
||||
// Make sure the main stage is active.
|
||||
mMainStage.activate(mSplitLayout.getBounds1(), wct);
|
||||
} else {
|
||||
// The side stage no long has children so we can deactivate the main stage.
|
||||
mMainStage.deactivate(wct);
|
||||
}
|
||||
mTaskOrganizer.applyTransaction(wct);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSnappedToDismiss(boolean snappedToEnd) {
|
||||
// TODO: What to do...what to do...
|
||||
mSplitLayout.resetDividerPosition();
|
||||
onBoundsChanged(mSplitLayout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBoundsChanging(SplitLayout layout) {
|
||||
final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash();
|
||||
if (dividerLeash == null) return;
|
||||
final Rect mainStageBounds = getMainStageBounds();
|
||||
final Rect sideStageBounds = getSideStageBounds();
|
||||
|
||||
mSyncQueue.runInSync(t -> t
|
||||
.setPosition(dividerLeash,
|
||||
mSplitLayout.getDividerBounds().left, mSplitLayout.getDividerBounds().top)
|
||||
.setPosition(mMainStage.mRootLeash, mainStageBounds.left, mainStageBounds.top)
|
||||
.setPosition(mSideStage.mRootLeash, sideStageBounds.left, sideStageBounds.top)
|
||||
// Sets crop to prevent visible region of tasks overlap with each other when
|
||||
// re-positioning surfaces while resizing.
|
||||
.setWindowCrop(mMainStage.mRootLeash,
|
||||
mainStageBounds.width(), mainStageBounds.height())
|
||||
.setWindowCrop(mSideStage.mRootLeash,
|
||||
sideStageBounds.width(), sideStageBounds.height()));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBoundsChanged(SplitLayout layout) {
|
||||
final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash();
|
||||
if (dividerLeash == null) return;
|
||||
final Rect mainStageBounds = getMainStageBounds();
|
||||
final Rect sideStageBounds = getSideStageBounds();
|
||||
final WindowContainerTransaction wct = new WindowContainerTransaction();
|
||||
mMainStage.setBounds(mainStageBounds, wct);
|
||||
mSideStage.setBounds(sideStageBounds, wct);
|
||||
mTaskOrganizer.applyTransaction(wct);
|
||||
|
||||
mSyncQueue.runInSync(t -> t
|
||||
// Resets layer of divider bar to make sure it is always on top.
|
||||
.setLayer(dividerLeash, Integer.MAX_VALUE)
|
||||
.setPosition(dividerLeash,
|
||||
mSplitLayout.getDividerBounds().left, mSplitLayout.getDividerBounds().top)
|
||||
.setPosition(mMainStage.mRootLeash,
|
||||
mainStageBounds.left, mainStageBounds.top)
|
||||
.setPosition(mSideStage.mRootLeash,
|
||||
sideStageBounds.left, sideStageBounds.top)
|
||||
// Resets crop to apply new surface bounds directly.
|
||||
.setWindowCrop(mMainStage.mRootLeash, null)
|
||||
.setWindowCrop(mSideStage.mRootLeash, null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo) {
|
||||
mDisplayAreaInfo = displayAreaInfo;
|
||||
if (mSplitLayout == null) {
|
||||
mSplitLayout = new SplitLayout(TAG + "SplitDivider", mContext,
|
||||
mDisplayAreaInfo.configuration, this,
|
||||
b -> mRootTDAOrganizer.attachToDisplayArea(mDisplayId, b));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisplayAreaVanished(DisplayAreaInfo displayAreaInfo) {
|
||||
throw new IllegalStateException("Well that was unexpected...");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisplayAreaInfoChanged(DisplayAreaInfo displayAreaInfo) {
|
||||
mDisplayAreaInfo = displayAreaInfo;
|
||||
if (mSplitLayout != null
|
||||
&& mSplitLayout.updateConfiguration(mDisplayAreaInfo.configuration)) {
|
||||
onBoundsChanged(mSplitLayout);
|
||||
}
|
||||
}
|
||||
|
||||
private Rect getSideStageBounds() {
|
||||
return mSplitLayout.getBounds2();
|
||||
}
|
||||
|
||||
private Rect getMainStageBounds() {
|
||||
return mSplitLayout.getBounds1();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dump(@NonNull PrintWriter pw, String prefix) {
|
||||
final String innerPrefix = prefix + " ";
|
||||
final String childPrefix = innerPrefix + " ";
|
||||
pw.println(prefix + TAG + " mDisplayId=" + mDisplayId);
|
||||
pw.println(innerPrefix + "mDividerVisible=" + mDividerVisible);
|
||||
pw.println(innerPrefix + "MainStage");
|
||||
pw.println(childPrefix + "isActive=" + mMainStage.isActive());
|
||||
mMainStageListener.dump(pw, childPrefix);
|
||||
pw.println(innerPrefix + "SideStage");
|
||||
mSideStageListener.dump(pw, childPrefix);
|
||||
pw.println(innerPrefix + "mSplitLayout=" + mSplitLayout);
|
||||
}
|
||||
|
||||
class StageListenerImpl implements StageTaskListener.StageListenerCallbacks {
|
||||
boolean mHasRootTask = false;
|
||||
boolean mVisible = false;
|
||||
boolean mHasChildren = false;
|
||||
|
||||
@Override
|
||||
public void onRootTaskAppeared() {
|
||||
mHasRootTask = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(boolean visible, boolean hasChildren) {
|
||||
if (!mHasRootTask) return;
|
||||
|
||||
if (mHasChildren != hasChildren) {
|
||||
mHasChildren = hasChildren;
|
||||
StageCoordinator.this.onStageHasChildrenChanged(this);
|
||||
}
|
||||
if (mVisible != visible) {
|
||||
mVisible = visible;
|
||||
StageCoordinator.this.onStageVisibilityChanged(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRootTaskVanished() {
|
||||
reset();
|
||||
StageCoordinator.this.onStageRootTaskVanished(this);
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
mHasRootTask = false;
|
||||
mVisible = false;
|
||||
mHasChildren = false;
|
||||
}
|
||||
|
||||
public void dump(@NonNull PrintWriter pw, String prefix) {
|
||||
pw.println(prefix + "mHasRootTask=" + mHasRootTask);
|
||||
pw.println(prefix + "mVisible=" + mVisible);
|
||||
pw.println(prefix + "mHasChildren=" + mHasChildren);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* 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.splitscreen;
|
||||
|
||||
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
|
||||
|
||||
import android.annotation.CallSuper;
|
||||
import android.app.ActivityManager;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.util.SparseArray;
|
||||
import android.view.SurfaceControl;
|
||||
import android.window.WindowContainerTransaction;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.common.SyncTransactionQueue;
|
||||
import com.android.wm.shell.transition.Transitions;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
|
||||
/**
|
||||
* Base class that handle common task org. related for split-screen stages.
|
||||
* Note that this class and its sub-class do not directly perform hierarchy operations.
|
||||
* They only serve to hold a collection of tasks and provide APIs like
|
||||
* {@link #setBounds(Rect, WindowContainerTransaction)} for the centralized {@link StageCoordinator}
|
||||
* to perform operations in-sync with other containers.
|
||||
* @see StageCoordinator
|
||||
*/
|
||||
class StageTaskListener implements ShellTaskOrganizer.TaskListener {
|
||||
private static final String TAG = StageTaskListener.class.getSimpleName();
|
||||
|
||||
/** Callback interface for listening to changes in a split-screen stage. */
|
||||
public interface StageListenerCallbacks {
|
||||
void onRootTaskAppeared();
|
||||
void onStatusChanged(boolean visible, boolean hasChildren);
|
||||
void onRootTaskVanished();
|
||||
}
|
||||
private final StageListenerCallbacks mCallbacks;
|
||||
private final SyncTransactionQueue mSyncQueue;
|
||||
|
||||
protected ActivityManager.RunningTaskInfo mRootTaskInfo;
|
||||
protected SurfaceControl mRootLeash;
|
||||
protected SparseArray<ActivityManager.RunningTaskInfo> mChildrenTaskInfo = new SparseArray<>();
|
||||
private final SparseArray<SurfaceControl> mChildrenLeashes = new SparseArray<>();
|
||||
|
||||
StageTaskListener(ShellTaskOrganizer taskOrganizer, int displayId,
|
||||
StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue) {
|
||||
mCallbacks = callbacks;
|
||||
mSyncQueue = syncQueue;
|
||||
taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
|
||||
if (!taskInfo.hasParentTask()) {
|
||||
mCallbacks.onRootTaskAppeared();
|
||||
mRootLeash = leash;
|
||||
mRootTaskInfo = taskInfo;
|
||||
} else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) {
|
||||
mChildrenLeashes.put(taskInfo.taskId, leash);
|
||||
mChildrenTaskInfo.put(taskInfo.taskId, taskInfo);
|
||||
updateChildTaskSurface(taskInfo, leash, true /* firstAppeared */);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown task: " + taskInfo);
|
||||
}
|
||||
sendStatusChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
|
||||
if (mRootTaskInfo.taskId == taskInfo.taskId) {
|
||||
mRootTaskInfo = taskInfo;
|
||||
} else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) {
|
||||
mChildrenTaskInfo.put(taskInfo.taskId, taskInfo);
|
||||
updateChildTaskSurface(
|
||||
taskInfo, mChildrenLeashes.get(taskInfo.taskId), false /* firstAppeared */);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown task: " + taskInfo);
|
||||
}
|
||||
sendStatusChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
|
||||
final int taskId = taskInfo.taskId;
|
||||
if (mRootTaskInfo.taskId == taskId) {
|
||||
mCallbacks.onRootTaskVanished();
|
||||
mRootTaskInfo = null;
|
||||
} else if (mChildrenTaskInfo.contains(taskId)) {
|
||||
mChildrenTaskInfo.remove(taskId);
|
||||
mChildrenLeashes.remove(taskId);
|
||||
sendStatusChanged();
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown task: " + taskInfo);
|
||||
}
|
||||
}
|
||||
|
||||
void setBounds(Rect bounds, WindowContainerTransaction wct) {
|
||||
wct.setBounds(mRootTaskInfo.token, bounds);
|
||||
}
|
||||
|
||||
private void updateChildTaskSurface(ActivityManager.RunningTaskInfo taskInfo,
|
||||
SurfaceControl leash, boolean firstAppeared) {
|
||||
final Point taskPositionInParent = taskInfo.positionInParent;
|
||||
mSyncQueue.runInSync(t -> {
|
||||
t.setWindowCrop(leash, null);
|
||||
t.setPosition(leash, taskPositionInParent.x, taskPositionInParent.y);
|
||||
if (firstAppeared && !Transitions.ENABLE_SHELL_TRANSITIONS) {
|
||||
t.setAlpha(leash, 1f);
|
||||
t.setMatrix(leash, 1, 0, 0, 1);
|
||||
t.show(leash);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void sendStatusChanged() {
|
||||
mCallbacks.onStatusChanged(mRootTaskInfo.isVisible, mChildrenTaskInfo.size() > 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
public void dump(@NonNull PrintWriter pw, String prefix) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ fun LayersAssertion.appPairsDividerIsVisible(
|
||||
enabled: Boolean = bugId == 0
|
||||
) {
|
||||
end("appPairsDividerIsVisible", bugId, enabled) {
|
||||
this.showsLayer(FlickerTestBase.SPLIT_DIVIDER)
|
||||
this.showsLayer(FlickerTestBase.APP_PAIR_SPLIT_DIVIDER)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ fun LayersAssertion.appPairsDividerIsInvisible(
|
||||
enabled: Boolean = bugId == 0
|
||||
) {
|
||||
end("appPairsDividerIsInVisible", bugId, enabled) {
|
||||
this.hasNotLayer(FlickerTestBase.SPLIT_DIVIDER)
|
||||
this.hasNotLayer(FlickerTestBase.APP_PAIR_SPLIT_DIVIDER)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ fun LayersAssertion.appPairsPrimaryBoundsIsVisible(
|
||||
end("PrimaryAppBounds", bugId, enabled) {
|
||||
val entry = this.trace.entries.firstOrNull()
|
||||
?: throw IllegalStateException("Trace is empty")
|
||||
val dividerRegion = entry.getVisibleBounds(FlickerTestBase.SPLIT_DIVIDER)
|
||||
val dividerRegion = entry.getVisibleBounds(FlickerTestBase.APP_PAIR_SPLIT_DIVIDER)
|
||||
this.hasVisibleRegion(primaryLayerName, getPrimaryRegion(dividerRegion, rotation))
|
||||
}
|
||||
}
|
||||
@@ -124,7 +124,7 @@ fun LayersAssertion.appPairsSecondaryBoundsIsVisible(
|
||||
end("SecondaryAppBounds", bugId, enabled) {
|
||||
val entry = this.trace.entries.firstOrNull()
|
||||
?: throw IllegalStateException("Trace is empty")
|
||||
val dividerRegion = entry.getVisibleBounds(FlickerTestBase.SPLIT_DIVIDER)
|
||||
val dividerRegion = entry.getVisibleBounds(FlickerTestBase.APP_PAIR_SPLIT_DIVIDER)
|
||||
this.hasVisibleRegion(secondaryLayerName, getSecondaryRegion(dividerRegion, rotation))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ abstract class FlickerTestBase {
|
||||
const val NAVIGATION_BAR_WINDOW_TITLE = "NavigationBar"
|
||||
const val STATUS_BAR_WINDOW_TITLE = "StatusBar"
|
||||
const val DOCKED_STACK_DIVIDER = "DockedStackDivider"
|
||||
const val SPLIT_DIVIDER = "SplitDivider"
|
||||
const val APP_PAIR_SPLIT_DIVIDER = "AppPairSplitDivider"
|
||||
const val IMAGE_WALLPAPER = "ImageWallpaper"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ class AppPairsTest(
|
||||
end("appsEndingBounds", enabled = false) {
|
||||
val entry = this.trace.entries.firstOrNull()
|
||||
?: throw IllegalStateException("Trace is empty")
|
||||
val dividerRegion = entry.getVisibleBounds(SPLIT_DIVIDER)
|
||||
val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER)
|
||||
this.hasVisibleRegion(primaryApp.defaultWindowName,
|
||||
appPairsHelper.getPrimaryBounds(dividerRegion))
|
||||
.and()
|
||||
@@ -151,7 +151,7 @@ class AppPairsTest(
|
||||
start("appsStartingBounds", enabled = false) {
|
||||
val entry = this.trace.entries.firstOrNull()
|
||||
?: throw IllegalStateException("Trace is empty")
|
||||
val dividerRegion = entry.getVisibleBounds(SPLIT_DIVIDER)
|
||||
val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER)
|
||||
this.hasVisibleRegion(primaryApp.defaultWindowName,
|
||||
appPairsHelper.getPrimaryBounds(dividerRegion))
|
||||
.and()
|
||||
|
||||
@@ -14,27 +14,37 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.wm.shell.apppairs;
|
||||
package com.android.wm.shell;
|
||||
|
||||
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.graphics.Rect;
|
||||
import android.window.IWindowContainerToken;
|
||||
import android.window.WindowContainerToken;
|
||||
|
||||
public class TestRunningTaskInfoBuilder {
|
||||
public final class TestRunningTaskInfoBuilder {
|
||||
static int sNextTaskId = 500;
|
||||
private Rect mBounds = new Rect(0, 0, 100, 100);
|
||||
private WindowContainerToken mToken =
|
||||
new WindowContainerToken(new IWindowContainerToken.Default());
|
||||
private int mParentTaskId = INVALID_TASK_ID;
|
||||
|
||||
TestRunningTaskInfoBuilder setBounds(Rect bounds) {
|
||||
public TestRunningTaskInfoBuilder setBounds(Rect bounds) {
|
||||
mBounds.set(bounds);
|
||||
return this;
|
||||
}
|
||||
|
||||
ActivityManager.RunningTaskInfo build() {
|
||||
public TestRunningTaskInfoBuilder setParentTaskId(int taskId) {
|
||||
mParentTaskId = taskId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ActivityManager.RunningTaskInfo build() {
|
||||
final ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo();
|
||||
info.parentTaskId = INVALID_TASK_ID;
|
||||
info.taskId = sNextTaskId++;
|
||||
info.parentTaskId = mParentTaskId;
|
||||
info.configuration.windowConfiguration.setBounds(mBounds);
|
||||
info.token = mToken;
|
||||
info.isResizeable = true;
|
||||
@@ -32,6 +32,7 @@ import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.ShellTestCase;
|
||||
import com.android.wm.shell.TestRunningTaskInfoBuilder;
|
||||
import com.android.wm.shell.common.DisplayController;
|
||||
import com.android.wm.shell.common.SyncTransactionQueue;
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.ShellTestCase;
|
||||
import com.android.wm.shell.TestRunningTaskInfoBuilder;
|
||||
import com.android.wm.shell.common.DisplayController;
|
||||
import com.android.wm.shell.common.SyncTransactionQueue;
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ package com.android.wm.shell.apppairs;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
|
||||
import com.android.wm.shell.TestRunningTaskInfoBuilder;
|
||||
|
||||
public class TestAppPairsPool extends AppPairsPool{
|
||||
TestAppPairsPool(AppPairsController controller) {
|
||||
super(controller);
|
||||
|
||||
@@ -55,6 +55,7 @@ public class SplitLayoutTests extends ShellTestCase {
|
||||
public void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mSplitLayout = new SplitLayout(
|
||||
"TestSplitLayout",
|
||||
mContext,
|
||||
getConfiguration(false),
|
||||
mLayoutChangeListener,
|
||||
@@ -64,6 +65,7 @@ public class SplitLayoutTests extends ShellTestCase {
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void testUpdateConfiguration() {
|
||||
mSplitLayout.init();
|
||||
assertThat(mSplitLayout.updateConfiguration(getConfiguration(false))).isFalse();
|
||||
assertThat(mSplitLayout.updateConfiguration(getConfiguration(true))).isTrue();
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ public class SplitWindowManagerTests extends ShellTestCase {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
final Configuration configuration = new Configuration();
|
||||
configuration.setToDefaults();
|
||||
mSplitWindowManager = new SplitWindowManager(mContext, configuration,
|
||||
mSplitWindowManager = new SplitWindowManager("TestSplitDivider", mContext, configuration,
|
||||
b -> b.setParent(mSurfaceControl));
|
||||
when(mSplitLayout.getDividerBounds()).thenReturn(
|
||||
new Rect(0, 0, configuration.windowConfiguration.getBounds().width(),
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.splitscreen;
|
||||
|
||||
import static android.view.Display.DEFAULT_DISPLAY;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.view.SurfaceControl;
|
||||
import android.window.WindowContainerTransaction;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.TestRunningTaskInfoBuilder;
|
||||
import com.android.wm.shell.common.SyncTransactionQueue;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.mockito.Spy;
|
||||
|
||||
/** Tests for {@link MainStage} */
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class MainStageTests {
|
||||
@Mock private ShellTaskOrganizer mTaskOrganizer;
|
||||
@Mock private StageTaskListener.StageListenerCallbacks mCallbacks;
|
||||
@Mock private SyncTransactionQueue mSyncQueue;
|
||||
@Mock private ActivityManager.RunningTaskInfo mRootTaskInfo;
|
||||
@Mock private SurfaceControl mRootLeash;
|
||||
@Spy private WindowContainerTransaction mWct;
|
||||
private MainStage mMainStage;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mRootTaskInfo = new TestRunningTaskInfoBuilder().build();
|
||||
mMainStage = new MainStage(mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, mSyncQueue);
|
||||
mMainStage.onTaskAppeared(mRootTaskInfo, mRootLeash);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testActiveDeactivate() {
|
||||
mMainStage.activate(mRootTaskInfo.configuration.windowConfiguration.getBounds(), mWct);
|
||||
assertThat(mMainStage.isActive()).isTrue();
|
||||
|
||||
mMainStage.deactivate(mWct);
|
||||
assertThat(mMainStage.isActive()).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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.splitscreen;
|
||||
|
||||
import static android.view.Display.DEFAULT_DISPLAY;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.view.SurfaceControl;
|
||||
import android.window.WindowContainerTransaction;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.TestRunningTaskInfoBuilder;
|
||||
import com.android.wm.shell.common.SyncTransactionQueue;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.mockito.Spy;
|
||||
|
||||
/** Tests for {@link SideStage} */
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class SideStageTests {
|
||||
@Mock private ShellTaskOrganizer mTaskOrganizer;
|
||||
@Mock private StageTaskListener.StageListenerCallbacks mCallbacks;
|
||||
@Mock private SyncTransactionQueue mSyncQueue;
|
||||
@Mock private ActivityManager.RunningTaskInfo mRootTask;
|
||||
@Mock private SurfaceControl mRootLeash;
|
||||
@Spy private WindowContainerTransaction mWct;
|
||||
private SideStage mSideStage;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mRootTask = new TestRunningTaskInfoBuilder().build();
|
||||
mSideStage = new SideStage(mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, mSyncQueue);
|
||||
mSideStage.onTaskAppeared(mRootTask, mRootLeash);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddTask() {
|
||||
final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build();
|
||||
|
||||
mSideStage.addTask(task, mRootTask.configuration.windowConfiguration.getBounds(), mWct);
|
||||
|
||||
verify(mWct).reparent(eq(task.token), eq(mRootTask.token), eq(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveTask() {
|
||||
final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build();
|
||||
assertThat(mSideStage.removeTask(task.taskId, mWct)).isFalse();
|
||||
|
||||
mSideStage.mChildrenTaskInfo.put(task.taskId, task);
|
||||
assertThat(mSideStage.removeTask(task.taskId, mWct)).isTrue();
|
||||
verify(mWct).reparent(eq(task.token), isNull(), eq(false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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.splitscreen;
|
||||
|
||||
import static android.view.Display.DEFAULT_DISPLAY;
|
||||
import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.window.DisplayAreaInfo;
|
||||
import android.window.IWindowContainerToken;
|
||||
import android.window.WindowContainerToken;
|
||||
import android.window.WindowContainerTransaction;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.ShellTestCase;
|
||||
import com.android.wm.shell.TestRunningTaskInfoBuilder;
|
||||
import com.android.wm.shell.common.SyncTransactionQueue;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
/** Tests for {@link StageCoordinator} */
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class StageCoordinatorTests extends ShellTestCase {
|
||||
@Mock private ShellTaskOrganizer mTaskOrganizer;
|
||||
@Mock private SyncTransactionQueue mSyncQueue;
|
||||
@Mock private RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
|
||||
@Mock private MainStage mMainStage;
|
||||
@Mock private SideStage mSideStage;
|
||||
private StageCoordinator mStageCoordinator;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mStageCoordinator = new TestStageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
|
||||
mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPinTask() {
|
||||
final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build();
|
||||
|
||||
mStageCoordinator.pinTask(task);
|
||||
|
||||
verify(mMainStage).activate(any(Rect.class), any(WindowContainerTransaction.class));
|
||||
verify(mSideStage).addTask(eq(task), any(Rect.class),
|
||||
any(WindowContainerTransaction.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnpinTask() {
|
||||
final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build();
|
||||
|
||||
mStageCoordinator.unpinTask(task.taskId);
|
||||
|
||||
verify(mSideStage).removeTask(eq(task.taskId), any(WindowContainerTransaction.class));
|
||||
}
|
||||
|
||||
private static class TestStageCoordinator extends StageCoordinator {
|
||||
final DisplayAreaInfo mDisplayAreaInfo;
|
||||
|
||||
TestStageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
|
||||
RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer,
|
||||
MainStage mainStage, SideStage sideStage) {
|
||||
super(context, displayId, syncQueue, rootTDAOrganizer, taskOrganizer, mainStage,
|
||||
sideStage);
|
||||
|
||||
// Prepare default TaskDisplayArea for testing.
|
||||
mDisplayAreaInfo = new DisplayAreaInfo(
|
||||
new WindowContainerToken(new IWindowContainerToken.Default()),
|
||||
DEFAULT_DISPLAY,
|
||||
FEATURE_DEFAULT_TASK_CONTAINER);
|
||||
this.onDisplayAreaAppeared(mDisplayAreaInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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.splitscreen;
|
||||
|
||||
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
|
||||
import static android.view.Display.DEFAULT_DISPLAY;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.view.SurfaceControl;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.TestRunningTaskInfoBuilder;
|
||||
import com.android.wm.shell.common.SyncTransactionQueue;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
/** Tests for {@link StageTaskListener} */
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class StageTaskListenerTests {
|
||||
@Mock private ShellTaskOrganizer mTaskOrganizer;
|
||||
@Mock private StageTaskListener.StageListenerCallbacks mCallbacks;
|
||||
@Mock private SyncTransactionQueue mSyncQueue;
|
||||
private ActivityManager.RunningTaskInfo mRootTask;
|
||||
private StageTaskListener mStageTaskListener;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mStageTaskListener = new StageTaskListener(
|
||||
mTaskOrganizer,
|
||||
DEFAULT_DISPLAY,
|
||||
mCallbacks,
|
||||
mSyncQueue);
|
||||
mRootTask = new TestRunningTaskInfoBuilder().build();
|
||||
mRootTask.parentTaskId = INVALID_TASK_ID;
|
||||
mStageTaskListener.onTaskAppeared(mRootTask, new SurfaceControl());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRootTaskAppeared() {
|
||||
assertThat(mStageTaskListener.mRootTaskInfo.taskId).isEqualTo(mRootTask.taskId);
|
||||
verify(mCallbacks).onRootTaskAppeared();
|
||||
verify(mCallbacks).onStatusChanged(eq(mRootTask.isVisible), eq(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChildTaskAppeared() {
|
||||
final ActivityManager.RunningTaskInfo childTask =
|
||||
new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build();
|
||||
|
||||
mStageTaskListener.onTaskAppeared(childTask, new SurfaceControl());
|
||||
|
||||
assertThat(mStageTaskListener.mChildrenTaskInfo.contains(childTask.taskId)).isTrue();
|
||||
verify(mCallbacks).onStatusChanged(eq(mRootTask.isVisible), eq(true));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testUnknownTaskVanished() {
|
||||
final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build();
|
||||
mStageTaskListener.onTaskVanished(task);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTaskVanished() {
|
||||
final ActivityManager.RunningTaskInfo childTask =
|
||||
new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build();
|
||||
mStageTaskListener.mRootTaskInfo = mRootTask;
|
||||
mStageTaskListener.mChildrenTaskInfo.put(childTask.taskId, childTask);
|
||||
|
||||
mStageTaskListener.onTaskVanished(childTask);
|
||||
verify(mCallbacks, times(2)).onStatusChanged(eq(mRootTask.isVisible), eq(false));
|
||||
|
||||
mStageTaskListener.onTaskVanished(mRootTask);
|
||||
verify(mCallbacks).onRootTaskVanished();
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,7 @@ public class SystemUIFactory {
|
||||
builder = prepareSysUIComponentBuilder(builder, mWMComponent)
|
||||
.setPip(mWMComponent.getPip())
|
||||
.setLegacySplitScreen(mWMComponent.getLegacySplitScreen())
|
||||
.setSplitScreen(mWMComponent.getSplitScreen())
|
||||
.setOneHanded(mWMComponent.getOneHanded())
|
||||
.setBubbles(mWMComponent.getBubbles())
|
||||
.setHideDisplayCutout(mWMComponent.getHideDisplayCutout())
|
||||
@@ -119,6 +120,7 @@ public class SystemUIFactory {
|
||||
builder = prepareSysUIComponentBuilder(builder, mWMComponent)
|
||||
.setPip(Optional.ofNullable(null))
|
||||
.setLegacySplitScreen(Optional.ofNullable(null))
|
||||
.setSplitScreen(Optional.ofNullable(null))
|
||||
.setOneHanded(Optional.ofNullable(null))
|
||||
.setBubbles(Optional.ofNullable(null))
|
||||
.setHideDisplayCutout(Optional.ofNullable(null))
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
|
||||
import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
|
||||
import com.android.wm.shell.onehanded.OneHanded;
|
||||
import com.android.wm.shell.pip.Pip;
|
||||
import com.android.wm.shell.splitscreen.SplitScreen;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -62,6 +63,9 @@ public interface SysUIComponent {
|
||||
@BindsInstance
|
||||
Builder setLegacySplitScreen(Optional<LegacySplitScreen> s);
|
||||
|
||||
@BindsInstance
|
||||
Builder setSplitScreen(Optional<SplitScreen> s);
|
||||
|
||||
@BindsInstance
|
||||
Builder setAppPairs(Optional<AppPairs> s);
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
|
||||
import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
|
||||
import com.android.wm.shell.onehanded.OneHanded;
|
||||
import com.android.wm.shell.pip.Pip;
|
||||
import com.android.wm.shell.splitscreen.SplitScreen;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -72,6 +73,9 @@ public interface WMComponent {
|
||||
@WMSingleton
|
||||
Optional<LegacySplitScreen> getLegacySplitScreen();
|
||||
|
||||
@WMSingleton
|
||||
Optional<SplitScreen> getSplitScreen();
|
||||
|
||||
@WMSingleton
|
||||
Optional<AppPairs> getAppPairs();
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
|
||||
import com.android.wm.shell.pip.PipUiEventLogger;
|
||||
import com.android.wm.shell.pip.phone.PipAppOpsListener;
|
||||
import com.android.wm.shell.pip.phone.PipTouchHandler;
|
||||
import com.android.wm.shell.splitscreen.SplitScreen;
|
||||
import com.android.wm.shell.transition.Transitions;
|
||||
|
||||
import java.util.Optional;
|
||||
@@ -174,6 +175,7 @@ public abstract class WMShellBaseModule {
|
||||
DragAndDropController dragAndDropController,
|
||||
ShellTaskOrganizer shellTaskOrganizer,
|
||||
Optional<LegacySplitScreen> legacySplitScreenOptional,
|
||||
Optional<SplitScreen> splitScreenOptional,
|
||||
Optional<AppPairs> appPairsOptional,
|
||||
FullscreenTaskListener fullscreenTaskListener,
|
||||
Transitions transitions,
|
||||
@@ -182,6 +184,7 @@ public abstract class WMShellBaseModule {
|
||||
dragAndDropController,
|
||||
shellTaskOrganizer,
|
||||
legacySplitScreenOptional,
|
||||
splitScreenOptional,
|
||||
appPairsOptional,
|
||||
fullscreenTaskListener,
|
||||
transitions,
|
||||
@@ -197,14 +200,15 @@ public abstract class WMShellBaseModule {
|
||||
static Optional<ShellCommandHandler> provideShellCommandHandler(
|
||||
ShellTaskOrganizer shellTaskOrganizer,
|
||||
Optional<LegacySplitScreen> legacySplitScreenOptional,
|
||||
Optional<SplitScreen> splitScreenOptional,
|
||||
Optional<Pip> pipOptional,
|
||||
Optional<OneHanded> oneHandedOptional,
|
||||
Optional<HideDisplayCutout> hideDisplayCutout,
|
||||
Optional<AppPairs> appPairsOptional,
|
||||
@ShellMainThread ShellExecutor mainExecutor) {
|
||||
return Optional.of(ShellCommandHandlerImpl.create(shellTaskOrganizer,
|
||||
legacySplitScreenOptional, pipOptional, oneHandedOptional, hideDisplayCutout,
|
||||
appPairsOptional, mainExecutor));
|
||||
legacySplitScreenOptional, splitScreenOptional, pipOptional, oneHandedOptional,
|
||||
hideDisplayCutout, appPairsOptional, mainExecutor));
|
||||
}
|
||||
|
||||
@WMSingleton
|
||||
@@ -306,6 +310,9 @@ public abstract class WMShellBaseModule {
|
||||
@BindsOptionalOf
|
||||
abstract LegacySplitScreen optionalLegacySplitScreen();
|
||||
|
||||
@BindsOptionalOf
|
||||
abstract SplitScreen optionalSplitScreen();
|
||||
|
||||
@BindsOptionalOf
|
||||
abstract AppPairs optionalAppPairs();
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.content.Context;
|
||||
import android.view.IWindowManager;
|
||||
|
||||
import com.android.systemui.dagger.WMSingleton;
|
||||
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.WindowManagerShellWrapper;
|
||||
import com.android.wm.shell.apppairs.AppPairs;
|
||||
@@ -46,6 +47,8 @@ import com.android.wm.shell.pip.phone.PhonePipMenuController;
|
||||
import com.android.wm.shell.pip.phone.PipAppOpsListener;
|
||||
import com.android.wm.shell.pip.phone.PipController;
|
||||
import com.android.wm.shell.pip.phone.PipTouchHandler;
|
||||
import com.android.wm.shell.splitscreen.SplitScreen;
|
||||
import com.android.wm.shell.splitscreen.SplitScreenController;
|
||||
import com.android.wm.shell.transition.Transitions;
|
||||
|
||||
import java.util.Optional;
|
||||
@@ -81,6 +84,15 @@ public class WMShellModule {
|
||||
taskStackListener, transitions, mainExecutor);
|
||||
}
|
||||
|
||||
@WMSingleton
|
||||
@Provides
|
||||
static SplitScreen provideSplitScreen(ShellTaskOrganizer shellTaskOrganizer,
|
||||
SyncTransactionQueue syncQueue, Context context,
|
||||
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
|
||||
return new SplitScreenController(shellTaskOrganizer, syncQueue, context,
|
||||
rootTaskDisplayAreaOrganizer);
|
||||
}
|
||||
|
||||
@WMSingleton
|
||||
@Provides
|
||||
static AppPairs provideAppPairs(ShellTaskOrganizer shellTaskOrganizer,
|
||||
|
||||
@@ -53,7 +53,7 @@ public abstract class SysuiBaseFragmentTest extends BaseFragmentTest {
|
||||
|
||||
@Before
|
||||
public void SysuiSetup() {
|
||||
SystemUIFactory.createFromConfig(mContext);
|
||||
SystemUIFactory.createFromConfig(mContext, true);
|
||||
mDependency = new TestableDependency(
|
||||
SystemUIFactory.getInstance().getSysUIComponent().createDependency());
|
||||
Dependency.setInstance(mDependency);
|
||||
|
||||
Reference in New Issue
Block a user