diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java index 4ef489ffd4802..cb54021d7a237 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java @@ -111,6 +111,7 @@ public class RootTaskDisplayAreaOrganizer extends DisplayAreaOrganizer { } mDisplayAreasInfo.put(displayId, displayAreaInfo); + mLeashes.put(displayId, leash); ArrayList listeners = mListeners.get(displayId); if (listeners != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java index f213af752d55a..9ad5ed86905d8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java @@ -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 mLegacySplitScreenOptional; + private final Optional mSplitScreenOptional; private final Optional mPipOptional; private final Optional mOneHandedOptional; private final Optional mHideDisplayCutout; @@ -49,19 +48,21 @@ public final class ShellCommandHandlerImpl { public static ShellCommandHandler create( ShellTaskOrganizer shellTaskOrganizer, Optional legacySplitScreenOptional, + Optional splitScreenOptional, Optional pipOptional, Optional oneHandedOptional, Optional hideDisplayCutout, Optional 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 legacySplitScreenOptional, + Optional splitScreenOptional, Optional pipOptional, Optional oneHandedOptional, Optional 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 "); pw.println(" unpair "); pw.println(" Pairs/unpairs tasks with given ids."); + pw.println(" pinTask "); + pw.println(" unpinTask "); + pw.println(" Pin/Unpin a task with given id in split-screen mode."); return true; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java index 0cee0a21bde3d..b43203dbfd77a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java @@ -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 mLegacySplitScreenOptional; + private final Optional mSplitScreenOptional; private final Optional mAppPairsOptional; private final FullscreenTaskListener mFullscreenTaskListener; private final ShellExecutor mMainExecutor; @@ -49,6 +51,7 @@ public class ShellInitImpl { DragAndDropController dragAndDropController, ShellTaskOrganizer shellTaskOrganizer, Optional legacySplitScreenOptional, + Optional splitScreenOptional, Optional 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 legacySplitScreenOptional, + Optional splitScreenOptional, Optional 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); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java index 563de06fe40c0..bab5140e2f52a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java @@ -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)); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index 291e9bdad69bf..9721d302a98a2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -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. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java index 29116bd6f956a..7f9c34f5df7a2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java @@ -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); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java new file mode 100644 index 0000000000000..552eba4ed5b36 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java @@ -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); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java new file mode 100644 index 0000000000000..2c26003fcb9da --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java @@ -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; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java new file mode 100644 index 0000000000000..152c40eedc946 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java @@ -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(); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java new file mode 100644 index 0000000000000..3183f762263c0 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -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); + } + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java new file mode 100644 index 0000000000000..bed033b851e21 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -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); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java new file mode 100644 index 0000000000000..de51623618509 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java @@ -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 mChildrenTaskInfo = new SparseArray<>(); + private final SparseArray 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) { + + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt index 854385020f039..94c1f59d957e6 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt @@ -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)) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt index 7809be04de96f..24e5fef32c0ad 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/FlickerTestBase.kt @@ -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" } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTest.kt index 22b1eb7037fcc..cb9fabd3d5ba8 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTest.kt @@ -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() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestRunningTaskInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java similarity index 71% rename from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestRunningTaskInfoBuilder.java rename to libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java index 76d3a6aaee226..1f58a8546796a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestRunningTaskInfoBuilder.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java @@ -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; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairTests.java index 8dbc1d56bcc2f..d21183e10ed90 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairTests.java @@ -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; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsControllerTests.java index fada694a4c074..505c153eff9c5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/AppPairsControllerTests.java @@ -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; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsPool.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsPool.java index 080f2075344af..1ee7fff44892d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsPool.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsPool.java @@ -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); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java index cd468167e372b..0d1c6f93d8e3c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java @@ -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(); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java index c17056353136c..698315a77d8e5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java @@ -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(), diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java new file mode 100644 index 0000000000000..702e8945de01d --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java @@ -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(); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java new file mode 100644 index 0000000000000..25bf9c6c947e7 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java @@ -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)); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java new file mode 100644 index 0000000000000..e3c36469587b7 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -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); + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java new file mode 100644 index 0000000000000..c66e0730422ce --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java @@ -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(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java index eefae5b586ee6..deca14a3cad3e 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java @@ -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)) diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java index 612a55970f025..82d313e7e706f 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java @@ -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 s); + @BindsInstance + Builder setSplitScreen(Optional s); + @BindsInstance Builder setAppPairs(Optional s); diff --git a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java index c75dc84f85709..ced606c6915ef 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java @@ -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 getLegacySplitScreen(); + @WMSingleton + Optional getSplitScreen(); + @WMSingleton Optional getAppPairs(); diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java index bcb4386c2cebb..02eb26d19343c 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java @@ -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 legacySplitScreenOptional, + Optional splitScreenOptional, Optional 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 provideShellCommandHandler( ShellTaskOrganizer shellTaskOrganizer, Optional legacySplitScreenOptional, + Optional splitScreenOptional, Optional pipOptional, Optional oneHandedOptional, Optional hideDisplayCutout, Optional 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(); diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java index 88f6e1ffac0c5..4351c4f0b9cec 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellModule.java @@ -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, diff --git a/packages/SystemUI/tests/src/com/android/systemui/SysuiBaseFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/SysuiBaseFragmentTest.java index 53bae8657d8ff..40549d69dc745 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/SysuiBaseFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/SysuiBaseFragmentTest.java @@ -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);