Consolidate the behavior and style of side stage outline

Update the style of side stage outline to match the current design. And
make sure the outline will interact properly with task bar expanding,
collapsing, swipe up, drag to split, recents shortcut to split and drag
divider bar to resize gestures.

Bug: 200850654
Test: atest WMShellUnitTests
Test: enable side stage outline, observed it behaves properly.
Change-Id: I3f0795adc34a0efb19d35b78d54364da4189a043
This commit is contained in:
Jerry Chang
2021-09-24 13:03:52 +08:00
parent feae50d5a7
commit 08ac74f125
11 changed files with 230 additions and 164 deletions

View File

@@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<com.android.wm.shell.splitscreen.OutlineRoot
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
@@ -23,4 +23,4 @@
android:layout_height="match_parent"
android:layout_width="match_parent" />
</com.android.wm.shell.splitscreen.OutlineRoot>
</FrameLayout>

View File

@@ -22,19 +22,23 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMA
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.os.Binder;
import android.view.IWindow;
import android.view.InsetsSource;
import android.view.InsetsState;
import android.view.LayoutInflater;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
import android.view.WindowInsets;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.WindowMetrics;
import android.view.WindowlessWindowManager;
import android.widget.FrameLayout;
import com.android.wm.shell.R;
@@ -45,17 +49,22 @@ import com.android.wm.shell.R;
class OutlineManager extends WindowlessWindowManager {
private static final String WINDOW_NAME = "SplitOutlineLayer";
private final Context mContext;
private final Rect mOutlineBounds = new Rect();
private final Rect mTmpBounds = new Rect();
private final Rect mRootBounds = new Rect();
private final Rect mTempRect = new Rect();
private final Rect mLastOutlineBounds = new Rect();
private final InsetsState mInsetsState = new InsetsState();
private final int mExpandedTaskBarHeight;
private OutlineView mOutlineView;
private SurfaceControlViewHost mViewHost;
private SurfaceControl mHostLeash;
private SurfaceControl mLeash;
private int mOutlineColor;
OutlineManager(Context context, Configuration configuration) {
super(configuration, null /* rootSurface */, null /* hostInputToken */);
mContext = context.createWindowContext(context.getDisplay(), TYPE_APPLICATION_OVERLAY,
null /* options */);
mExpandedTaskBarHeight = mContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.taskbar_frame_height);
}
@Override
@@ -63,65 +72,110 @@ class OutlineManager extends WindowlessWindowManager {
b.setParent(mHostLeash);
}
boolean drawOutlineBounds(Rect rootBounds) {
if (mLeash == null || mViewHost == null) return false;
computeOutlineBounds(mContext, rootBounds, mTmpBounds);
if (mOutlineBounds.equals(mTmpBounds)) {
return false;
}
mOutlineBounds.set(mTmpBounds);
((OutlineRoot) mViewHost.getView()).updateOutlineBounds(mOutlineBounds, mOutlineColor);
final WindowManager.LayoutParams lp =
(WindowManager.LayoutParams) mViewHost.getView().getLayoutParams();
lp.width = rootBounds.width();
lp.height = rootBounds.height();
mViewHost.relayout(lp);
return true;
}
void inflate(SurfaceControl.Transaction t, SurfaceControl hostLeash, int color) {
void inflate(SurfaceControl rootLeash, Rect rootBounds) {
if (mLeash != null || mViewHost != null) return;
mHostLeash = hostLeash;
mOutlineColor = color;
mHostLeash = rootLeash;
mRootBounds.set(rootBounds);
mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this);
final OutlineRoot rootView = (OutlineRoot) LayoutInflater.from(mContext)
final FrameLayout rootLayout = (FrameLayout) LayoutInflater.from(mContext)
.inflate(R.layout.split_outline, null);
mOutlineView = rootLayout.findViewById(R.id.split_outline);
final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
0 /* width */, 0 /* height */, TYPE_APPLICATION_OVERLAY,
FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT);
lp.width = mRootBounds.width();
lp.height = mRootBounds.height();
lp.token = new Binder();
lp.setTitle(WINDOW_NAME);
lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY;
// TODO(b/189839391): Set INPUT_FEATURE_NO_INPUT_CHANNEL after WM supports
// TRUSTED_OVERLAY for windowless window without input channel.
mViewHost.setView(rootView, lp);
mViewHost.setView(rootLayout, lp);
mLeash = getSurfaceControl(mViewHost.getWindowToken());
t.setLayer(mLeash, Integer.MAX_VALUE);
drawOutline();
}
void release() {
if (mViewHost != null) {
mViewHost.release();
mViewHost = null;
}
mRootBounds.setEmpty();
mLastOutlineBounds.setEmpty();
mOutlineView = null;
mHostLeash = null;
mLeash = null;
}
@Nullable
SurfaceControl getOutlineLeash() {
return mLeash;
}
void setVisibility(boolean visible) {
if (mOutlineView != null) {
mOutlineView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
}
}
private static void computeOutlineBounds(Context context, Rect rootBounds, Rect outBounds) {
computeDisplayStableBounds(context, outBounds);
outBounds.intersect(rootBounds);
void setRootBounds(Rect rootBounds) {
if (mViewHost == null || mViewHost.getView() == null) {
return;
}
if (!mRootBounds.equals(rootBounds)) {
WindowManager.LayoutParams lp =
(WindowManager.LayoutParams) mViewHost.getView().getLayoutParams();
lp.width = rootBounds.width();
lp.height = rootBounds.height();
mViewHost.relayout(lp);
mRootBounds.set(rootBounds);
drawOutline();
}
}
void onInsetsChanged(InsetsState insetsState) {
if (!mInsetsState.equals(insetsState)) {
mInsetsState.set(insetsState);
drawOutline();
}
}
private void computeOutlineBounds(Rect rootBounds, InsetsState insetsState, Rect outBounds) {
outBounds.set(rootBounds);
final InsetsSource taskBarInsetsSource =
insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
// Only insets the divider bar with task bar when it's expanded so that the rounded corners
// will be drawn against task bar.
if (taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) {
outBounds.inset(taskBarInsetsSource.calculateVisibleInsets(outBounds));
}
// Offset the coordinate from screen based to surface based.
outBounds.offset(-rootBounds.left, -rootBounds.top);
}
private static void computeDisplayStableBounds(Context context, Rect outBounds) {
final WindowMetrics windowMetrics =
context.getSystemService(WindowManager.class).getMaximumWindowMetrics();
outBounds.set(windowMetrics.getBounds());
outBounds.inset(windowMetrics.getWindowInsets().getInsets(
WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()));
void drawOutline() {
if (mOutlineView == null) {
return;
}
computeOutlineBounds(mRootBounds, mInsetsState, mTempRect);
if (mTempRect.equals(mLastOutlineBounds)) {
return;
}
ViewGroup.MarginLayoutParams lp =
(ViewGroup.MarginLayoutParams) mOutlineView.getLayoutParams();
lp.leftMargin = mTempRect.left;
lp.topMargin = mTempRect.top;
lp.width = mTempRect.width();
lp.height = mTempRect.height();
mOutlineView.setLayoutParams(lp);
mLastOutlineBounds.set(mTempRect);
}
}

View File

@@ -1,62 +0,0 @@
/*
* Copyright (C) 2021 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.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.wm.shell.R;
/** Root layout for holding split outline. */
public class OutlineRoot extends FrameLayout {
public OutlineRoot(@NonNull Context context) {
super(context);
}
public OutlineRoot(@NonNull Context context,
@Nullable AttributeSet attrs) {
super(context, attrs);
}
public OutlineRoot(@NonNull Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public OutlineRoot(@NonNull Context context, @Nullable AttributeSet attrs,
int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
private OutlineView mOutlineView;
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mOutlineView = findViewById(R.id.split_outline);
}
void updateOutlineBounds(Rect bounds, int color) {
mOutlineView.updateOutlineBounds(bounds, color);
}
}

View File

@@ -16,13 +16,17 @@
package com.android.wm.shell.splitscreen;
import static android.view.RoundedCorner.POSITION_BOTTOM_LEFT;
import static android.view.RoundedCorner.POSITION_BOTTOM_RIGHT;
import static android.view.RoundedCorner.POSITION_TOP_LEFT;
import static android.view.RoundedCorner.POSITION_TOP_RIGHT;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.Region;
import android.util.AttributeSet;
import android.view.RoundedCorner;
import android.view.View;
import androidx.annotation.NonNull;
@@ -33,44 +37,46 @@ import com.android.internal.R;
/** View for drawing split outline. */
public class OutlineView extends View {
private final Paint mPaint = new Paint();
private final Rect mBounds = new Rect();
private final Path mPath = new Path();
private final float[] mRadii = new float[8];
public OutlineView(@NonNull Context context) {
super(context);
}
public OutlineView(@NonNull Context context,
@Nullable AttributeSet attrs) {
public OutlineView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public OutlineView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public OutlineView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(
getResources().getDimension(R.dimen.accessibility_focus_highlight_stroke_width));
mPaint.setColor(getResources().getColor(R.color.system_accent1_100, null));
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(getResources()
.getDimension(R.dimen.accessibility_focus_highlight_stroke_width));
protected void onAttachedToWindow() {
// TODO(b/200850654): match the screen corners with the actual display decor.
mRadii[0] = mRadii[1] = getCornerRadius(POSITION_TOP_LEFT);
mRadii[2] = mRadii[3] = getCornerRadius(POSITION_TOP_RIGHT);
mRadii[4] = mRadii[5] = getCornerRadius(POSITION_BOTTOM_RIGHT);
mRadii[6] = mRadii[7] = getCornerRadius(POSITION_BOTTOM_LEFT);
}
void updateOutlineBounds(Rect bounds, int color) {
if (mBounds.equals(bounds) && mPaint.getColor() == color) return;
mBounds.set(bounds);
mPaint.setColor(color);
private int getCornerRadius(@RoundedCorner.Position int position) {
final RoundedCorner roundedCorner = getDisplay().getRoundedCorner(position);
return roundedCorner == null ? 0 : roundedCorner.getRadius();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (changed) {
mPath.reset();
mPath.addRoundRect(0, 0, getWidth(), getHeight(), mRadii, Path.Direction.CW);
}
}
@Override
protected void onDraw(Canvas canvas) {
if (mBounds.isEmpty()) return;
final Path path = new Region(mBounds).getBoundaryPath();
canvas.drawPath(path, mPaint);
canvas.drawPath(mPath, mPaint);
}
@Override
public boolean hasOverlappingRendering() {
return false;
}
}

View File

@@ -17,15 +17,19 @@
package com.android.wm.shell.splitscreen;
import android.annotation.CallSuper;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Rect;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.SyncTransactionQueue;
/**
@@ -34,7 +38,8 @@ import com.android.wm.shell.common.SyncTransactionQueue;
*
* @see StageCoordinator
*/
class SideStage extends StageTaskListener {
class SideStage extends StageTaskListener implements
DisplayInsetsController.OnInsetsChangedListener {
private static final String TAG = SideStage.class.getSimpleName();
private final Context mContext;
private OutlineManager mOutlineManager;
@@ -77,33 +82,61 @@ class SideStage extends StageTaskListener {
return true;
}
void enableOutline(boolean enable) {
if (enable) {
if (mOutlineManager == null && mRootTaskInfo != null) {
mOutlineManager = new OutlineManager(mContext, mRootTaskInfo.configuration);
mSyncQueue.runInSync(t -> mOutlineManager.inflate(t, mRootLeash, Color.YELLOW));
updateOutlineBounds();
}
} else {
if (mOutlineManager != null) {
mOutlineManager.release();
mOutlineManager = null;
}
}
@Nullable
public SurfaceControl getOutlineLeash() {
return mOutlineManager.getOutlineLeash();
}
private void updateOutlineBounds() {
if (mOutlineManager == null || mRootTaskInfo == null || !mRootTaskInfo.isVisible) return;
mOutlineManager.drawOutlineBounds(
mRootTaskInfo.configuration.windowConfiguration.getBounds());
@Override
@CallSuper
public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
super.onTaskAppeared(taskInfo, leash);
if (isRootTask(taskInfo)) {
mOutlineManager = new OutlineManager(mContext, taskInfo.configuration);
enableOutline(true);
}
}
@Override
@CallSuper
public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
super.onTaskInfoChanged(taskInfo);
if (mRootTaskInfo != null && mRootTaskInfo.taskId == taskInfo.taskId) {
updateOutlineBounds();
if (isRootTask(taskInfo)) {
mOutlineManager.setRootBounds(taskInfo.configuration.windowConfiguration.getBounds());
}
}
private boolean isRootTask(ActivityManager.RunningTaskInfo taskInfo) {
return mRootTaskInfo != null && mRootTaskInfo.taskId == taskInfo.taskId;
}
void enableOutline(boolean enable) {
if (mOutlineManager == null) {
return;
}
if (enable) {
if (mRootTaskInfo != null) {
mOutlineManager.inflate(mRootLeash,
mRootTaskInfo.configuration.windowConfiguration.getBounds());
}
} else {
mOutlineManager.release();
}
}
void setOutlineVisibility(boolean visible) {
mOutlineManager.setVisibility(visible);
}
@Override
public void insetsChanged(InsetsState insetsState) {
mOutlineManager.onInsetsChanged(insetsState);
}
@Override
public void insetsControlChanged(InsetsState insetsState,
InsetsSourceControl[] activeControls) {
insetsChanged(insetsState);
}
}

View File

@@ -257,11 +257,6 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
}
}
final RemoteAnimationTarget divider = mStageCoordinator.getDividerBarLegacyTarget();
if (divider.leash != null) {
t.show(divider.leash);
}
t.apply();
if (finishedCallback != null) {
try {
@@ -300,7 +295,9 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
}
transaction.apply();
transaction.close();
return new RemoteAnimationTarget[]{mStageCoordinator.getDividerBarLegacyTarget()};
return new RemoteAnimationTarget[]{
mStageCoordinator.getDividerBarLegacyTarget(),
mStageCoordinator.getOutlineLegacyTarget()};
}
/**

View File

@@ -201,6 +201,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mSurfaceSession);
mDisplayImeController = displayImeController;
mDisplayInsetsController = displayInsetsController;
mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSideStage);
mRootTDAOrganizer.registerListener(displayId, this);
final DeviceStateManager deviceStateManager =
mContext.getSystemService(DeviceStateManager.class);
@@ -682,6 +683,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
t.setVisibility(mSideStage.mRootLeash, bothStageVisible)
.setVisibility(mMainStage.mRootLeash, bothStageVisible);
applyDividerVisibility(t);
applyOutlineVisibility(t);
}
});
}
@@ -703,6 +705,19 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
}
private void applyOutlineVisibility(SurfaceControl.Transaction t) {
final SurfaceControl outlineLeash = mSideStage.getOutlineLeash();
if (outlineLeash == null) {
return;
}
if (mDividerVisible) {
t.show(outlineLeash).setLayer(outlineLeash, SPLIT_DIVIDER_LAYER);
} else {
t.hide(outlineLeash);
}
}
private void onStageHasChildrenChanged(StageListenerImpl stageListener) {
final boolean hasChildren = stageListener.mHasChildren;
final boolean isSideStage = stageListener == mSideStageListener;
@@ -762,6 +777,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
@Override
public void onLayoutChanging(SplitLayout layout) {
mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t));
mSideStage.setOutlineVisibility(false);
}
@Override
@@ -770,6 +786,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
updateWindowBounds(layout, wct);
mSyncQueue.queue(wct);
mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t));
mSideStage.setOutlineVisibility(true);
mLogger.logResize(mSplitLayout.getDividerPositionAsFraction());
}
@@ -1145,6 +1162,18 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
null /* taskInfo */, false /* allowEnterPip */, TYPE_DOCK_DIVIDER);
}
RemoteAnimationTarget getOutlineLegacyTarget() {
final Rect bounds = mSideStage.mRootTaskInfo.configuration.windowConfiguration.getBounds();
// Leverage TYPE_DOCK_DIVIDER type when wrapping outline remote animation target in order to
// distinguish as a split auxiliary target in Launcher.
return new RemoteAnimationTarget(-1 /* taskId */, -1 /* mode */,
mSideStage.getOutlineLeash(), false /* isTranslucent */, null /* clipRect */,
null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */,
new android.graphics.Point(0, 0) /* position */, bounds, bounds,
new WindowConfiguration(), true, null /* startLeash */, null /* startBounds */,
null /* taskInfo */, false /* allowEnterPip */, TYPE_DOCK_DIVIDER);
}
@Override
public void dump(@NonNull PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";

View File

@@ -138,8 +138,11 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
mRootTaskInfo = taskInfo;
mCallbacks.onRootTaskAppeared();
sendStatusChanged();
mSyncQueue.runInSync(t -> mDimLayer =
SurfaceUtils.makeDimLayer(t, mRootLeash, "Dim layer", mSurfaceSession));
mSyncQueue.runInSync(t -> {
t.hide(mRootLeash);
mDimLayer =
SurfaceUtils.makeDimLayer(t, mRootLeash, "Dim layer", mSurfaceSession);
});
} else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) {
final int taskId = taskInfo.taskId;
mChildrenLeashes.put(taskId, leash);

View File

@@ -29,6 +29,7 @@ import android.view.SurfaceControl;
import android.view.SurfaceSession;
import android.window.WindowContainerTransaction;
import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -58,6 +59,7 @@ public class SideStageTests extends ShellTestCase {
private SideStage mSideStage;
@Before
@UiThreadTest
public void setup() {
MockitoAnnotations.initMocks(this);
mRootTask = new TestRunningTaskInfoBuilder().build();

View File

@@ -51,6 +51,7 @@ import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
import android.window.WindowContainerTransaction;
import androidx.test.annotation.UiThreadTest;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -96,6 +97,7 @@ public class SplitTransitionTests extends ShellTestCase {
private ActivityManager.RunningTaskInfo mSideChild;
@Before
@UiThreadTest
public void setup() {
MockitoAnnotations.initMocks(this);
final ShellExecutor mockExecutor = mock(ShellExecutor.class);

View File

@@ -62,6 +62,7 @@ public final class StageTaskListenerTests {
@Mock private SyncTransactionQueue mSyncQueue;
@Captor private ArgumentCaptor<SyncTransactionQueue.TransactionRunnable> mRunnableCaptor;
private SurfaceSession mSurfaceSession = new SurfaceSession();
private SurfaceControl mSurfaceControl;
private ActivityManager.RunningTaskInfo mRootTask;
private StageTaskListener mStageTaskListener;
@@ -76,7 +77,8 @@ public final class StageTaskListenerTests {
mSurfaceSession);
mRootTask = new TestRunningTaskInfoBuilder().build();
mRootTask.parentTaskId = INVALID_TASK_ID;
mStageTaskListener.onTaskAppeared(mRootTask, new SurfaceControl());
mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession).setName("test").build();
mStageTaskListener.onTaskAppeared(mRootTask, mSurfaceControl);
}
@Test
@@ -103,7 +105,7 @@ public final class StageTaskListenerTests {
final ActivityManager.RunningTaskInfo childTask =
new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build();
mStageTaskListener.onTaskAppeared(childTask, new SurfaceControl());
mStageTaskListener.onTaskAppeared(childTask, mSurfaceControl);
assertThat(mStageTaskListener.mChildrenTaskInfo.contains(childTask.taskId)).isTrue();
verify(mCallbacks).onStatusChanged(eq(mRootTask.isVisible), eq(true));