Loading libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +63 −118 Original line number Diff line number Diff line Loading @@ -168,12 +168,10 @@ import java.util.concurrent.Executor; * - The {@link SplitLayout} divider is only visible if multiple {@link StageTaskListener}s are * visible * - Both stages are put under a single-top root task. * This rules are mostly implemented in {@link #onStageVisibilityChanged(StageListenerImpl)} and * {@link #onStageHasChildrenChanged(StageListenerImpl).} */ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, DisplayController.OnDisplaysChangedListener, Transitions.TransitionHandler, ShellTaskOrganizer.TaskListener { ShellTaskOrganizer.TaskListener, StageTaskListener.StageListenerCallbacks { private static final String TAG = StageCoordinator.class.getSimpleName(); Loading @@ -182,9 +180,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private static final int DISABLE_LAUNCH_ADJACENT_AFTER_ENTER_TIMEOUT_MS = 1000; private final StageTaskListener mMainStage; private final StageListenerImpl mMainStageListener = new StageListenerImpl(); private final StageTaskListener mSideStage; private final StageListenerImpl mSideStageListener = new StageListenerImpl(); @SplitPosition private int mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT; Loading Loading @@ -344,7 +340,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mContext, mTaskOrganizer, mDisplayId, mMainStageListener, this /*stageListenerCallbacks*/, mSyncQueue, iconProvider, mWindowDecorViewModel); Loading @@ -352,7 +348,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mContext, mTaskOrganizer, mDisplayId, mSideStageListener, this /*stageListenerCallbacks*/, mSyncQueue, iconProvider, mWindowDecorViewModel); Loading Loading @@ -427,7 +423,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } public boolean isSplitScreenVisible() { return mSideStageListener.mVisible && mMainStageListener.mVisible; return mSideStage.mVisible && mMainStage.mVisible; } private void activateSplit(WindowContainerTransaction wct, boolean includingTopTask) { Loading Loading @@ -1112,7 +1108,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSideStagePosition = sideStagePosition; sendOnStagePositionChanged(); if (mSideStageListener.mVisible && updateBounds) { if (mSideStage.mVisible && updateBounds) { if (wct == null) { // onLayoutChanged builds/applies a wct with the contents of updateWindowBounds. onLayoutSizeChanged(mSplitLayout); Loading Loading @@ -1333,8 +1329,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private void clearRequestIfPresented() { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "clearRequestIfPresented"); if (mSideStageListener.mVisible && mSideStageListener.mHasChildren && mMainStageListener.mVisible && mSideStageListener.mHasChildren) { if (mSideStage.mVisible && mSideStage.mHasChildren && mMainStage.mVisible && mSideStage.mHasChildren) { mSplitRequest = null; } } Loading Loading @@ -1587,11 +1583,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } private void onStageChildTaskStatusChanged(StageListenerImpl stageListener, int taskId, @Override public void onChildTaskStatusChanged(StageTaskListener stageListener, int taskId, boolean present, boolean visible) { int stage; if (present) { stage = stageListener == mSideStageListener ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN; stage = stageListener == mSideStage ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN; } else { // No longer on any stage stage = STAGE_TYPE_UNDEFINED; Loading Loading @@ -1722,13 +1719,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, @VisibleForTesting void onRootTaskAppeared() { @Override public void onRootTaskAppeared() { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRootTaskAppeared: rootTask=%s mainRoot=%b sideRoot=%b", mRootTaskInfo, mMainStageListener.mHasRootTask, mSideStageListener.mHasRootTask); mRootTaskInfo, mMainStage.mHasRootTask, mSideStage.mHasRootTask); // Wait unit all root tasks appeared. if (mRootTaskInfo == null || !mMainStageListener.mHasRootTask || !mSideStageListener.mHasRootTask) { || !mMainStage.mHasRootTask || !mSideStage.mHasRootTask) { return; } Loading @@ -1751,11 +1749,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, /** Callback when split roots have child task appeared under it, this is a little different from * #onStageHasChildrenChanged because this would be called every time child task appeared. * NOTICE: This only be called on legacy transition. */ private void onChildTaskAppeared(StageListenerImpl stageListener, int taskId) { @Override public void onChildTaskAppeared(StageTaskListener stageListener, int taskId) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onChildTaskAppeared: isMainStage=%b task=%d", stageListener == mMainStageListener, taskId); stageListener == mMainStage, taskId); // Handle entering split screen while there is a split pair running in the background. if (stageListener == mSideStageListener && !isSplitScreenVisible() && isSplitActive() if (stageListener == mSideStage && !isSplitScreenVisible() && isSplitActive() && mSplitRequest == null) { final WindowContainerTransaction wct = new WindowContainerTransaction(); prepareEnterSplitScreen(wct); Loading @@ -1776,7 +1775,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } private void onRootTaskVanished() { @Override public void onRootTaskVanished() { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRootTaskVanished"); final WindowContainerTransaction wct = new WindowContainerTransaction(); mLaunchAdjacentController.clearLaunchAdjacentRoot(); Loading @@ -1793,15 +1793,16 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, /** Callback when split roots visiblility changed. * NOTICE: This only be called on legacy transition. */ private void onStageVisibilityChanged(StageListenerImpl stageListener) { @Override public void onStageVisibilityChanged(StageTaskListener stageListener) { // If split didn't active, just ignore this callback because we should already did these // on #applyExitSplitScreen. if (!isSplitActive()) { return; } final boolean sideStageVisible = mSideStageListener.mVisible; final boolean mainStageVisible = mMainStageListener.mVisible; final boolean sideStageVisible = mSideStage.mVisible; final boolean mainStageVisible = mMainStage.mVisible; // Wait for both stages having the same visibility to prevent causing flicker. if (mainStageVisible != sideStageVisible) { Loading Loading @@ -1938,18 +1939,19 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, /** Callback when split roots have child or haven't under it. * NOTICE: This only be called on legacy transition. */ private void onStageHasChildrenChanged(StageListenerImpl stageListener) { @Override public void onStageHasChildrenChanged(StageTaskListener stageListener) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onStageHasChildrenChanged: isMainStage=%b", stageListener == mMainStageListener); stageListener == mMainStage); final boolean hasChildren = stageListener.mHasChildren; final boolean isSideStage = stageListener == mSideStageListener; final boolean isSideStage = stageListener == mSideStage; if (!hasChildren && !mIsExiting && isSplitActive()) { if (isSideStage && mMainStageListener.mVisible) { if (isSideStage && mMainStage.mVisible) { // Exit to main stage if side stage no longer has children. mSplitLayout.flingDividerToDismiss( mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT, EXIT_REASON_APP_FINISHED); } else if (!isSideStage && mSideStageListener.mVisible) { } else if (!isSideStage && mSideStage.mVisible) { // Exit to side stage if main stage no longer has children. mSplitLayout.flingDividerToDismiss( mSideStagePosition != SPLIT_POSITION_BOTTOM_OR_RIGHT, Loading @@ -1974,7 +1976,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } }); } if (mMainStageListener.mHasChildren && mSideStageListener.mHasChildren) { if (mMainStage.mHasChildren && mSideStage.mHasChildren) { mShouldUpdateRecents = true; clearRequestIfPresented(); updateRecentTasksSplitPair(); Loading @@ -1989,6 +1991,35 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } @Override public void onNoLongerSupportMultiWindow(StageTaskListener stageTaskListener, ActivityManager.RunningTaskInfo taskInfo) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onNoLongerSupportMultiWindow: task=%s", taskInfo); if (isSplitActive()) { final boolean isMainStage = mMainStage == stageTaskListener; // If visible, we preserve the app and keep it running. If an app becomes // unsupported in the bg, break split without putting anything on top boolean splitScreenVisible = isSplitScreenVisible(); int stageType = STAGE_TYPE_UNDEFINED; if (splitScreenVisible) { stageType = isMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE; } final WindowContainerTransaction wct = new WindowContainerTransaction(); prepareExitSplitScreen(stageType, wct); clearSplitPairedInRecents(EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); mSplitTransitions.startDismissTransition(wct, StageCoordinator.this, stageType, EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); Log.w(TAG, splitFailureMessage("onNoLongerSupportMultiWindow", "app package " + taskInfo.baseIntent.getComponent() + " does not support splitscreen, or is a controlled activity" + " type")); if (splitScreenVisible) { handleUnsupportedSplitStart(); } } } @Override public void onSnappedToDismiss(boolean bottomOrRight, @ExitReason int exitReason) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onSnappedToDismiss: bottomOrRight=%b reason=%s", Loading Loading @@ -3243,13 +3274,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, pw.println(childPrefix + "stagePosition=" + splitPositionToString(getMainStagePosition())); pw.println(childPrefix + "isActive=" + isSplitActive()); mMainStage.dump(pw, childPrefix); pw.println(innerPrefix + "MainStageListener"); mMainStageListener.dump(pw, childPrefix); pw.println(innerPrefix + "SideStage"); pw.println(childPrefix + "stagePosition=" + splitPositionToString(getSideStagePosition())); mSideStage.dump(pw, childPrefix); pw.println(innerPrefix + "SideStageListener"); mSideStageListener.dump(pw, childPrefix); if (mSplitLayout != null) { mSplitLayout.dump(pw, childPrefix); } Loading @@ -3265,8 +3292,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, */ private void setSplitsVisible(boolean visible) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "setSplitsVisible: visible=%b", visible); mMainStageListener.mVisible = mSideStageListener.mVisible = visible; mMainStageListener.mHasChildren = mSideStageListener.mHasChildren = visible; mMainStage.mVisible = mSideStage.mVisible = visible; mMainStage.mHasChildren = mSideStage.mHasChildren = visible; } /** Loading Loading @@ -3316,86 +3343,4 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, !toMainStage ? mSideStage.getTopChildTaskUid() : 0 /* sideStageUid */, mSplitLayout.isLeftRightSplit()); } class StageListenerImpl implements StageTaskListener.StageListenerCallbacks { boolean mHasRootTask = false; boolean mVisible = false; boolean mHasChildren = false; @Override public void onRootTaskAppeared() { mHasRootTask = true; StageCoordinator.this.onRootTaskAppeared(); } @Override public void onChildTaskAppeared(int taskId) { StageCoordinator.this.onChildTaskAppeared(this, taskId); } @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 onChildTaskStatusChanged(int taskId, boolean present, boolean visible) { StageCoordinator.this.onStageChildTaskStatusChanged(this, taskId, present, visible); } @Override public void onRootTaskVanished() { reset(); StageCoordinator.this.onRootTaskVanished(); } @Override public void onNoLongerSupportMultiWindow(ActivityManager.RunningTaskInfo taskInfo) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onNoLongerSupportMultiWindow: task=%s", taskInfo); if (isSplitActive()) { final boolean isMainStage = mMainStageListener == this; // If visible, we preserve the app and keep it running. If an app becomes // unsupported in the bg, break split without putting anything on top boolean splitScreenVisible = isSplitScreenVisible(); int stageType = STAGE_TYPE_UNDEFINED; if (splitScreenVisible) { stageType = isMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE; } final WindowContainerTransaction wct = new WindowContainerTransaction(); prepareExitSplitScreen(stageType, wct); clearSplitPairedInRecents(EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); mSplitTransitions.startDismissTransition(wct, StageCoordinator.this, stageType, EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); Log.w(TAG, splitFailureMessage("onNoLongerSupportMultiWindow", "app package " + taskInfo.baseIntent.getComponent() + " does not support splitscreen, or is a controlled activity" + " type")); if (splitScreenVisible) { handleUnsupportedSplitStart(); } } } 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); } } } libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java +39 −11 Original line number Diff line number Diff line Loading @@ -74,20 +74,22 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { // No current way to enforce this but if enableFlexibleSplit() is enabled, then only 1 of the // stages should have this be set/being used private boolean mIsActive; /** Callback interface for listening to changes in a split-screen stage. */ public interface StageListenerCallbacks { void onRootTaskAppeared(); void onChildTaskAppeared(StageTaskListener stageTaskListener, int taskId); void onChildTaskAppeared(int taskId); void onStageHasChildrenChanged(StageTaskListener stageTaskListener); void onStatusChanged(boolean visible, boolean hasChildren); void onStageVisibilityChanged(StageTaskListener stageTaskListener); void onChildTaskStatusChanged(int taskId, boolean present, boolean visible); void onChildTaskStatusChanged(StageTaskListener stage, int taskId, boolean present, boolean visible); void onRootTaskVanished(); void onNoLongerSupportMultiWindow(ActivityManager.RunningTaskInfo taskInfo); void onNoLongerSupportMultiWindow(StageTaskListener stageTaskListener, ActivityManager.RunningTaskInfo taskInfo); } private final Context mContext; Loading @@ -96,6 +98,12 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { private final IconProvider mIconProvider; private final Optional<WindowDecorViewModel> mWindowDecorViewModel; /** Whether or not the root task has been created. */ boolean mHasRootTask = false; /** Whether or not the root task is visible. */ boolean mVisible = false; /** Whether or not the root task has any children or not. */ boolean mHasChildren = false; protected ActivityManager.RunningTaskInfo mRootTaskInfo; protected SurfaceControl mRootLeash; protected SurfaceControl mDimLayer; Loading Loading @@ -201,6 +209,7 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { mSplitDecorManager = new SplitDecorManager( mRootTaskInfo.configuration, mIconProvider); mHasRootTask = true; mCallbacks.onRootTaskAppeared(); sendStatusChanged(); mSyncQueue.runInSync(t -> mDimLayer = Loading @@ -209,14 +218,14 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { final int taskId = taskInfo.taskId; mChildrenLeashes.put(taskId, leash); mChildrenTaskInfo.put(taskId, taskInfo); mCallbacks.onChildTaskStatusChanged(taskId, true /* present */, mCallbacks.onChildTaskStatusChanged(this, taskId, true /* present */, taskInfo.isVisible && taskInfo.isVisibleRequested); if (ENABLE_SHELL_TRANSITIONS) { // Status is managed/synchronized by the transition lifecycle. return; } updateChildTaskSurface(taskInfo, leash, true /* firstAppeared */); mCallbacks.onChildTaskAppeared(taskId); mCallbacks.onChildTaskAppeared(this, taskId); sendStatusChanged(); } else { throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo Loading Loading @@ -250,11 +259,11 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { taskInfo.taskId); // Leave split screen if the task no longer supports multi window or have // uncontrolled task. mCallbacks.onNoLongerSupportMultiWindow(taskInfo); mCallbacks.onNoLongerSupportMultiWindow(this, taskInfo); return; } mChildrenTaskInfo.put(taskInfo.taskId, taskInfo); mCallbacks.onChildTaskStatusChanged(taskInfo.taskId, true /* present */, mCallbacks.onChildTaskStatusChanged(this, taskInfo.taskId, true /* present */, taskInfo.isVisible && taskInfo.isVisibleRequested); if (!ENABLE_SHELL_TRANSITIONS) { updateChildTaskSurface( Loading @@ -278,6 +287,9 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { final int taskId = taskInfo.taskId; mWindowDecorViewModel.ifPresent(vm -> vm.onTaskVanished(taskInfo)); if (mRootTaskInfo.taskId == taskId) { mHasRootTask = false; mVisible = false; mHasChildren = false; mCallbacks.onRootTaskVanished(); mRootTaskInfo = null; mRootLeash = null; Loading @@ -288,7 +300,8 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { } else if (mChildrenTaskInfo.contains(taskId)) { mChildrenTaskInfo.remove(taskId); mChildrenLeashes.remove(taskId); mCallbacks.onChildTaskStatusChanged(taskId, false /* present */, taskInfo.isVisible); mCallbacks.onChildTaskStatusChanged(this, taskId, false /* present */, taskInfo.isVisible); if (ENABLE_SHELL_TRANSITIONS) { // Status is managed/synchronized by the transition lifecycle. return; Loading Loading @@ -538,7 +551,19 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { } private void sendStatusChanged() { mCallbacks.onStatusChanged(mRootTaskInfo.isVisible, mChildrenTaskInfo.size() > 0); boolean hasChildren = mChildrenTaskInfo.size() > 0; boolean visible = mRootTaskInfo.isVisible; if (!mHasRootTask) return; if (mHasChildren != hasChildren) { mHasChildren = hasChildren; mCallbacks.onStageHasChildrenChanged(this); } if (mVisible != visible) { mVisible = visible; mCallbacks.onStageVisibilityChanged(this); } } @Override Loading @@ -554,5 +579,8 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { + " baseActivity=" + taskInfo.baseActivity); } } pw.println(prefix + "mHasRootTask=" + mHasRootTask); pw.println(prefix + "mVisible=" + mVisible); pw.println(prefix + "mHasChildren=" + mHasChildren); } } libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java +11 −30 Original line number Diff line number Diff line Loading @@ -23,10 +23,9 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeFalse; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.times; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.app.ActivityManager; Loading Loading @@ -64,8 +63,6 @@ import java.util.Optional; @SmallTest @RunWith(AndroidJUnit4.class) public final class StageTaskListenerTests extends ShellTestCase { private static final boolean ENABLE_SHELL_TRANSITIONS = SystemProperties.getBoolean("persist.wm.debug.shell_transit", true); @Mock private ShellTaskOrganizer mTaskOrganizer; Loading Loading @@ -117,20 +114,20 @@ public final class StageTaskListenerTests extends ShellTestCase { public void testRootTaskAppeared() { assertThat(mStageTaskListener.mRootTaskInfo.taskId).isEqualTo(mRootTask.taskId); verify(mCallbacks).onRootTaskAppeared(); verify(mCallbacks).onStatusChanged(eq(mRootTask.isVisible), eq(false)); verify(mCallbacks, never()).onStageHasChildrenChanged(mStageTaskListener); verify(mCallbacks, never()).onStageVisibilityChanged(mStageTaskListener); } @Test public void testChildTaskAppeared() { // With shell transitions, the transition manages status changes, so skip this test. assumeFalse(ENABLE_SHELL_TRANSITIONS); final ActivityManager.RunningTaskInfo childTask = new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build(); public void testRootTaskVisible() { mStageTaskListener.onTaskVanished(mRootTask); mRootTask = new TestRunningTaskInfoBuilder().setVisible(true).build(); mRootTask.parentTaskId = INVALID_TASK_ID; mSurfaceControl = new SurfaceControl.Builder().setName("test").build(); mStageTaskListener.onTaskAppeared(mRootTask, mSurfaceControl); mStageTaskListener.onTaskAppeared(childTask, mSurfaceControl); verify(mCallbacks).onStageVisibilityChanged(mStageTaskListener); assertThat(mStageTaskListener.mChildrenTaskInfo.contains(childTask.taskId)).isTrue(); verify(mCallbacks).onStatusChanged(eq(mRootTask.isVisible), eq(true)); } @Test(expected = IllegalArgumentException.class) Loading @@ -139,22 +136,6 @@ public final class StageTaskListenerTests extends ShellTestCase { mStageTaskListener.onTaskVanished(task); } @Test public void testTaskVanished() { // With shell transitions, the transition manages status changes, so skip this test. assumeFalse(ENABLE_SHELL_TRANSITIONS); 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(); } @Test public void testTaskInfoChanged_notSupportsMultiWindow() { final ActivityManager.RunningTaskInfo childTask = Loading @@ -162,7 +143,7 @@ public final class StageTaskListenerTests extends ShellTestCase { childTask.supportsMultiWindow = false; mStageTaskListener.onTaskInfoChanged(childTask); verify(mCallbacks).onNoLongerSupportMultiWindow(childTask); verify(mCallbacks).onNoLongerSupportMultiWindow(mStageTaskListener, childTask); } @Test Loading Loading
libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +63 −118 Original line number Diff line number Diff line Loading @@ -168,12 +168,10 @@ import java.util.concurrent.Executor; * - The {@link SplitLayout} divider is only visible if multiple {@link StageTaskListener}s are * visible * - Both stages are put under a single-top root task. * This rules are mostly implemented in {@link #onStageVisibilityChanged(StageListenerImpl)} and * {@link #onStageHasChildrenChanged(StageListenerImpl).} */ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, DisplayController.OnDisplaysChangedListener, Transitions.TransitionHandler, ShellTaskOrganizer.TaskListener { ShellTaskOrganizer.TaskListener, StageTaskListener.StageListenerCallbacks { private static final String TAG = StageCoordinator.class.getSimpleName(); Loading @@ -182,9 +180,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private static final int DISABLE_LAUNCH_ADJACENT_AFTER_ENTER_TIMEOUT_MS = 1000; private final StageTaskListener mMainStage; private final StageListenerImpl mMainStageListener = new StageListenerImpl(); private final StageTaskListener mSideStage; private final StageListenerImpl mSideStageListener = new StageListenerImpl(); @SplitPosition private int mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT; Loading Loading @@ -344,7 +340,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mContext, mTaskOrganizer, mDisplayId, mMainStageListener, this /*stageListenerCallbacks*/, mSyncQueue, iconProvider, mWindowDecorViewModel); Loading @@ -352,7 +348,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mContext, mTaskOrganizer, mDisplayId, mSideStageListener, this /*stageListenerCallbacks*/, mSyncQueue, iconProvider, mWindowDecorViewModel); Loading Loading @@ -427,7 +423,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } public boolean isSplitScreenVisible() { return mSideStageListener.mVisible && mMainStageListener.mVisible; return mSideStage.mVisible && mMainStage.mVisible; } private void activateSplit(WindowContainerTransaction wct, boolean includingTopTask) { Loading Loading @@ -1112,7 +1108,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSideStagePosition = sideStagePosition; sendOnStagePositionChanged(); if (mSideStageListener.mVisible && updateBounds) { if (mSideStage.mVisible && updateBounds) { if (wct == null) { // onLayoutChanged builds/applies a wct with the contents of updateWindowBounds. onLayoutSizeChanged(mSplitLayout); Loading Loading @@ -1333,8 +1329,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private void clearRequestIfPresented() { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "clearRequestIfPresented"); if (mSideStageListener.mVisible && mSideStageListener.mHasChildren && mMainStageListener.mVisible && mSideStageListener.mHasChildren) { if (mSideStage.mVisible && mSideStage.mHasChildren && mMainStage.mVisible && mSideStage.mHasChildren) { mSplitRequest = null; } } Loading Loading @@ -1587,11 +1583,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } private void onStageChildTaskStatusChanged(StageListenerImpl stageListener, int taskId, @Override public void onChildTaskStatusChanged(StageTaskListener stageListener, int taskId, boolean present, boolean visible) { int stage; if (present) { stage = stageListener == mSideStageListener ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN; stage = stageListener == mSideStage ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN; } else { // No longer on any stage stage = STAGE_TYPE_UNDEFINED; Loading Loading @@ -1722,13 +1719,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, @VisibleForTesting void onRootTaskAppeared() { @Override public void onRootTaskAppeared() { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRootTaskAppeared: rootTask=%s mainRoot=%b sideRoot=%b", mRootTaskInfo, mMainStageListener.mHasRootTask, mSideStageListener.mHasRootTask); mRootTaskInfo, mMainStage.mHasRootTask, mSideStage.mHasRootTask); // Wait unit all root tasks appeared. if (mRootTaskInfo == null || !mMainStageListener.mHasRootTask || !mSideStageListener.mHasRootTask) { || !mMainStage.mHasRootTask || !mSideStage.mHasRootTask) { return; } Loading @@ -1751,11 +1749,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, /** Callback when split roots have child task appeared under it, this is a little different from * #onStageHasChildrenChanged because this would be called every time child task appeared. * NOTICE: This only be called on legacy transition. */ private void onChildTaskAppeared(StageListenerImpl stageListener, int taskId) { @Override public void onChildTaskAppeared(StageTaskListener stageListener, int taskId) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onChildTaskAppeared: isMainStage=%b task=%d", stageListener == mMainStageListener, taskId); stageListener == mMainStage, taskId); // Handle entering split screen while there is a split pair running in the background. if (stageListener == mSideStageListener && !isSplitScreenVisible() && isSplitActive() if (stageListener == mSideStage && !isSplitScreenVisible() && isSplitActive() && mSplitRequest == null) { final WindowContainerTransaction wct = new WindowContainerTransaction(); prepareEnterSplitScreen(wct); Loading @@ -1776,7 +1775,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } private void onRootTaskVanished() { @Override public void onRootTaskVanished() { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRootTaskVanished"); final WindowContainerTransaction wct = new WindowContainerTransaction(); mLaunchAdjacentController.clearLaunchAdjacentRoot(); Loading @@ -1793,15 +1793,16 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, /** Callback when split roots visiblility changed. * NOTICE: This only be called on legacy transition. */ private void onStageVisibilityChanged(StageListenerImpl stageListener) { @Override public void onStageVisibilityChanged(StageTaskListener stageListener) { // If split didn't active, just ignore this callback because we should already did these // on #applyExitSplitScreen. if (!isSplitActive()) { return; } final boolean sideStageVisible = mSideStageListener.mVisible; final boolean mainStageVisible = mMainStageListener.mVisible; final boolean sideStageVisible = mSideStage.mVisible; final boolean mainStageVisible = mMainStage.mVisible; // Wait for both stages having the same visibility to prevent causing flicker. if (mainStageVisible != sideStageVisible) { Loading Loading @@ -1938,18 +1939,19 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, /** Callback when split roots have child or haven't under it. * NOTICE: This only be called on legacy transition. */ private void onStageHasChildrenChanged(StageListenerImpl stageListener) { @Override public void onStageHasChildrenChanged(StageTaskListener stageListener) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onStageHasChildrenChanged: isMainStage=%b", stageListener == mMainStageListener); stageListener == mMainStage); final boolean hasChildren = stageListener.mHasChildren; final boolean isSideStage = stageListener == mSideStageListener; final boolean isSideStage = stageListener == mSideStage; if (!hasChildren && !mIsExiting && isSplitActive()) { if (isSideStage && mMainStageListener.mVisible) { if (isSideStage && mMainStage.mVisible) { // Exit to main stage if side stage no longer has children. mSplitLayout.flingDividerToDismiss( mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT, EXIT_REASON_APP_FINISHED); } else if (!isSideStage && mSideStageListener.mVisible) { } else if (!isSideStage && mSideStage.mVisible) { // Exit to side stage if main stage no longer has children. mSplitLayout.flingDividerToDismiss( mSideStagePosition != SPLIT_POSITION_BOTTOM_OR_RIGHT, Loading @@ -1974,7 +1976,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } }); } if (mMainStageListener.mHasChildren && mSideStageListener.mHasChildren) { if (mMainStage.mHasChildren && mSideStage.mHasChildren) { mShouldUpdateRecents = true; clearRequestIfPresented(); updateRecentTasksSplitPair(); Loading @@ -1989,6 +1991,35 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } @Override public void onNoLongerSupportMultiWindow(StageTaskListener stageTaskListener, ActivityManager.RunningTaskInfo taskInfo) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onNoLongerSupportMultiWindow: task=%s", taskInfo); if (isSplitActive()) { final boolean isMainStage = mMainStage == stageTaskListener; // If visible, we preserve the app and keep it running. If an app becomes // unsupported in the bg, break split without putting anything on top boolean splitScreenVisible = isSplitScreenVisible(); int stageType = STAGE_TYPE_UNDEFINED; if (splitScreenVisible) { stageType = isMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE; } final WindowContainerTransaction wct = new WindowContainerTransaction(); prepareExitSplitScreen(stageType, wct); clearSplitPairedInRecents(EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); mSplitTransitions.startDismissTransition(wct, StageCoordinator.this, stageType, EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); Log.w(TAG, splitFailureMessage("onNoLongerSupportMultiWindow", "app package " + taskInfo.baseIntent.getComponent() + " does not support splitscreen, or is a controlled activity" + " type")); if (splitScreenVisible) { handleUnsupportedSplitStart(); } } } @Override public void onSnappedToDismiss(boolean bottomOrRight, @ExitReason int exitReason) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onSnappedToDismiss: bottomOrRight=%b reason=%s", Loading Loading @@ -3243,13 +3274,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, pw.println(childPrefix + "stagePosition=" + splitPositionToString(getMainStagePosition())); pw.println(childPrefix + "isActive=" + isSplitActive()); mMainStage.dump(pw, childPrefix); pw.println(innerPrefix + "MainStageListener"); mMainStageListener.dump(pw, childPrefix); pw.println(innerPrefix + "SideStage"); pw.println(childPrefix + "stagePosition=" + splitPositionToString(getSideStagePosition())); mSideStage.dump(pw, childPrefix); pw.println(innerPrefix + "SideStageListener"); mSideStageListener.dump(pw, childPrefix); if (mSplitLayout != null) { mSplitLayout.dump(pw, childPrefix); } Loading @@ -3265,8 +3292,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, */ private void setSplitsVisible(boolean visible) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "setSplitsVisible: visible=%b", visible); mMainStageListener.mVisible = mSideStageListener.mVisible = visible; mMainStageListener.mHasChildren = mSideStageListener.mHasChildren = visible; mMainStage.mVisible = mSideStage.mVisible = visible; mMainStage.mHasChildren = mSideStage.mHasChildren = visible; } /** Loading Loading @@ -3316,86 +3343,4 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, !toMainStage ? mSideStage.getTopChildTaskUid() : 0 /* sideStageUid */, mSplitLayout.isLeftRightSplit()); } class StageListenerImpl implements StageTaskListener.StageListenerCallbacks { boolean mHasRootTask = false; boolean mVisible = false; boolean mHasChildren = false; @Override public void onRootTaskAppeared() { mHasRootTask = true; StageCoordinator.this.onRootTaskAppeared(); } @Override public void onChildTaskAppeared(int taskId) { StageCoordinator.this.onChildTaskAppeared(this, taskId); } @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 onChildTaskStatusChanged(int taskId, boolean present, boolean visible) { StageCoordinator.this.onStageChildTaskStatusChanged(this, taskId, present, visible); } @Override public void onRootTaskVanished() { reset(); StageCoordinator.this.onRootTaskVanished(); } @Override public void onNoLongerSupportMultiWindow(ActivityManager.RunningTaskInfo taskInfo) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onNoLongerSupportMultiWindow: task=%s", taskInfo); if (isSplitActive()) { final boolean isMainStage = mMainStageListener == this; // If visible, we preserve the app and keep it running. If an app becomes // unsupported in the bg, break split without putting anything on top boolean splitScreenVisible = isSplitScreenVisible(); int stageType = STAGE_TYPE_UNDEFINED; if (splitScreenVisible) { stageType = isMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE; } final WindowContainerTransaction wct = new WindowContainerTransaction(); prepareExitSplitScreen(stageType, wct); clearSplitPairedInRecents(EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); mSplitTransitions.startDismissTransition(wct, StageCoordinator.this, stageType, EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); Log.w(TAG, splitFailureMessage("onNoLongerSupportMultiWindow", "app package " + taskInfo.baseIntent.getComponent() + " does not support splitscreen, or is a controlled activity" + " type")); if (splitScreenVisible) { handleUnsupportedSplitStart(); } } } 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); } } }
libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java +39 −11 Original line number Diff line number Diff line Loading @@ -74,20 +74,22 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { // No current way to enforce this but if enableFlexibleSplit() is enabled, then only 1 of the // stages should have this be set/being used private boolean mIsActive; /** Callback interface for listening to changes in a split-screen stage. */ public interface StageListenerCallbacks { void onRootTaskAppeared(); void onChildTaskAppeared(StageTaskListener stageTaskListener, int taskId); void onChildTaskAppeared(int taskId); void onStageHasChildrenChanged(StageTaskListener stageTaskListener); void onStatusChanged(boolean visible, boolean hasChildren); void onStageVisibilityChanged(StageTaskListener stageTaskListener); void onChildTaskStatusChanged(int taskId, boolean present, boolean visible); void onChildTaskStatusChanged(StageTaskListener stage, int taskId, boolean present, boolean visible); void onRootTaskVanished(); void onNoLongerSupportMultiWindow(ActivityManager.RunningTaskInfo taskInfo); void onNoLongerSupportMultiWindow(StageTaskListener stageTaskListener, ActivityManager.RunningTaskInfo taskInfo); } private final Context mContext; Loading @@ -96,6 +98,12 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { private final IconProvider mIconProvider; private final Optional<WindowDecorViewModel> mWindowDecorViewModel; /** Whether or not the root task has been created. */ boolean mHasRootTask = false; /** Whether or not the root task is visible. */ boolean mVisible = false; /** Whether or not the root task has any children or not. */ boolean mHasChildren = false; protected ActivityManager.RunningTaskInfo mRootTaskInfo; protected SurfaceControl mRootLeash; protected SurfaceControl mDimLayer; Loading Loading @@ -201,6 +209,7 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { mSplitDecorManager = new SplitDecorManager( mRootTaskInfo.configuration, mIconProvider); mHasRootTask = true; mCallbacks.onRootTaskAppeared(); sendStatusChanged(); mSyncQueue.runInSync(t -> mDimLayer = Loading @@ -209,14 +218,14 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { final int taskId = taskInfo.taskId; mChildrenLeashes.put(taskId, leash); mChildrenTaskInfo.put(taskId, taskInfo); mCallbacks.onChildTaskStatusChanged(taskId, true /* present */, mCallbacks.onChildTaskStatusChanged(this, taskId, true /* present */, taskInfo.isVisible && taskInfo.isVisibleRequested); if (ENABLE_SHELL_TRANSITIONS) { // Status is managed/synchronized by the transition lifecycle. return; } updateChildTaskSurface(taskInfo, leash, true /* firstAppeared */); mCallbacks.onChildTaskAppeared(taskId); mCallbacks.onChildTaskAppeared(this, taskId); sendStatusChanged(); } else { throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo Loading Loading @@ -250,11 +259,11 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { taskInfo.taskId); // Leave split screen if the task no longer supports multi window or have // uncontrolled task. mCallbacks.onNoLongerSupportMultiWindow(taskInfo); mCallbacks.onNoLongerSupportMultiWindow(this, taskInfo); return; } mChildrenTaskInfo.put(taskInfo.taskId, taskInfo); mCallbacks.onChildTaskStatusChanged(taskInfo.taskId, true /* present */, mCallbacks.onChildTaskStatusChanged(this, taskInfo.taskId, true /* present */, taskInfo.isVisible && taskInfo.isVisibleRequested); if (!ENABLE_SHELL_TRANSITIONS) { updateChildTaskSurface( Loading @@ -278,6 +287,9 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { final int taskId = taskInfo.taskId; mWindowDecorViewModel.ifPresent(vm -> vm.onTaskVanished(taskInfo)); if (mRootTaskInfo.taskId == taskId) { mHasRootTask = false; mVisible = false; mHasChildren = false; mCallbacks.onRootTaskVanished(); mRootTaskInfo = null; mRootLeash = null; Loading @@ -288,7 +300,8 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { } else if (mChildrenTaskInfo.contains(taskId)) { mChildrenTaskInfo.remove(taskId); mChildrenLeashes.remove(taskId); mCallbacks.onChildTaskStatusChanged(taskId, false /* present */, taskInfo.isVisible); mCallbacks.onChildTaskStatusChanged(this, taskId, false /* present */, taskInfo.isVisible); if (ENABLE_SHELL_TRANSITIONS) { // Status is managed/synchronized by the transition lifecycle. return; Loading Loading @@ -538,7 +551,19 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { } private void sendStatusChanged() { mCallbacks.onStatusChanged(mRootTaskInfo.isVisible, mChildrenTaskInfo.size() > 0); boolean hasChildren = mChildrenTaskInfo.size() > 0; boolean visible = mRootTaskInfo.isVisible; if (!mHasRootTask) return; if (mHasChildren != hasChildren) { mHasChildren = hasChildren; mCallbacks.onStageHasChildrenChanged(this); } if (mVisible != visible) { mVisible = visible; mCallbacks.onStageVisibilityChanged(this); } } @Override Loading @@ -554,5 +579,8 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { + " baseActivity=" + taskInfo.baseActivity); } } pw.println(prefix + "mHasRootTask=" + mHasRootTask); pw.println(prefix + "mVisible=" + mVisible); pw.println(prefix + "mHasChildren=" + mHasChildren); } }
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java +11 −30 Original line number Diff line number Diff line Loading @@ -23,10 +23,9 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeFalse; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.times; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.app.ActivityManager; Loading Loading @@ -64,8 +63,6 @@ import java.util.Optional; @SmallTest @RunWith(AndroidJUnit4.class) public final class StageTaskListenerTests extends ShellTestCase { private static final boolean ENABLE_SHELL_TRANSITIONS = SystemProperties.getBoolean("persist.wm.debug.shell_transit", true); @Mock private ShellTaskOrganizer mTaskOrganizer; Loading Loading @@ -117,20 +114,20 @@ public final class StageTaskListenerTests extends ShellTestCase { public void testRootTaskAppeared() { assertThat(mStageTaskListener.mRootTaskInfo.taskId).isEqualTo(mRootTask.taskId); verify(mCallbacks).onRootTaskAppeared(); verify(mCallbacks).onStatusChanged(eq(mRootTask.isVisible), eq(false)); verify(mCallbacks, never()).onStageHasChildrenChanged(mStageTaskListener); verify(mCallbacks, never()).onStageVisibilityChanged(mStageTaskListener); } @Test public void testChildTaskAppeared() { // With shell transitions, the transition manages status changes, so skip this test. assumeFalse(ENABLE_SHELL_TRANSITIONS); final ActivityManager.RunningTaskInfo childTask = new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build(); public void testRootTaskVisible() { mStageTaskListener.onTaskVanished(mRootTask); mRootTask = new TestRunningTaskInfoBuilder().setVisible(true).build(); mRootTask.parentTaskId = INVALID_TASK_ID; mSurfaceControl = new SurfaceControl.Builder().setName("test").build(); mStageTaskListener.onTaskAppeared(mRootTask, mSurfaceControl); mStageTaskListener.onTaskAppeared(childTask, mSurfaceControl); verify(mCallbacks).onStageVisibilityChanged(mStageTaskListener); assertThat(mStageTaskListener.mChildrenTaskInfo.contains(childTask.taskId)).isTrue(); verify(mCallbacks).onStatusChanged(eq(mRootTask.isVisible), eq(true)); } @Test(expected = IllegalArgumentException.class) Loading @@ -139,22 +136,6 @@ public final class StageTaskListenerTests extends ShellTestCase { mStageTaskListener.onTaskVanished(task); } @Test public void testTaskVanished() { // With shell transitions, the transition manages status changes, so skip this test. assumeFalse(ENABLE_SHELL_TRANSITIONS); 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(); } @Test public void testTaskInfoChanged_notSupportsMultiWindow() { final ActivityManager.RunningTaskInfo childTask = Loading @@ -162,7 +143,7 @@ public final class StageTaskListenerTests extends ShellTestCase { childTask.supportsMultiWindow = false; mStageTaskListener.onTaskInfoChanged(childTask); verify(mCallbacks).onNoLongerSupportMultiWindow(childTask); verify(mCallbacks).onNoLongerSupportMultiWindow(mStageTaskListener, childTask); } @Test Loading