Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8b4781e5 authored by Vinit Nayak's avatar Vinit Nayak
Browse files

Removed StageListenerImpl and moved data into StageTaskListener

* No logical changes, did split onStatusChanged() callback into two
more granular callbacks for child count and visibility changed
* StageTaskListener holds the data for each stage and calls back into
StageCoordinator when necessary
* This will help keeping one-off classes for the stages in an effort
to generify the stages from each other.
* Also removed tests for non-shell transition paths

Bug: 349828130
Flag: EXEMPT refactor
Test: Existing tests pass, added a new test for root task visibility
Played with splitscreen on device, works as before

Change-Id: Ib1aa0bf33dc3dd641d8eb7dc9d31107eec6f62ed
parent c09e63ed
Loading
Loading
Loading
Loading
+63 −118
Original line number Diff line number Diff line
@@ -160,19 +160,15 @@ 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();

    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;

@@ -328,7 +324,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
                mContext,
                mTaskOrganizer,
                mDisplayId,
                mMainStageListener,
                this /*stageListenerCallbacks*/,
                mSyncQueue,
                iconProvider,
                mWindowDecorViewModel);
@@ -336,7 +332,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
                mContext,
                mTaskOrganizer,
                mDisplayId,
                mSideStageListener,
                this /*stageListenerCallbacks*/,
                mSyncQueue,
                iconProvider,
                mWindowDecorViewModel);
@@ -411,7 +407,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) {
@@ -1111,7 +1107,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);
@@ -1332,8 +1328,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;
        }
    }
@@ -1586,11 +1582,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;
@@ -1721,13 +1718,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;
        }

@@ -1750,11 +1748,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);
@@ -1775,7 +1774,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();
@@ -1792,15 +1792,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) {
@@ -1937,18 +1938,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,
@@ -1973,7 +1975,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
                }
            });
        }
        if (mMainStageListener.mHasChildren && mSideStageListener.mHasChildren) {
        if (mMainStage.mHasChildren && mSideStage.mHasChildren) {
            mShouldUpdateRecents = true;
            clearRequestIfPresented();
            updateRecentTasksSplitPair();
@@ -1988,6 +1990,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",
@@ -3197,13 +3228,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);
        }
@@ -3219,8 +3246,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;
    }

    /**
@@ -3270,86 +3297,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);
        }
    }
}
+39 −11
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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 =
@@ -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
@@ -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(
@@ -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;
@@ -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;
@@ -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
@@ -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);
    }
}
+11 −30
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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)
@@ -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 =
@@ -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