Merge "Baseline support for main/side stage split-screen model"

This commit is contained in:
Wale Ogunwale
2021-01-13 16:05:41 +00:00
committed by Android (Google) Code Review
31 changed files with 1297 additions and 39 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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