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

Commit 85ad95e6 authored by Winson Chung's avatar Winson Chung Committed by Jeremy Sim
Browse files

Clean up split after trampolines

- If a task trampoline gets collected into the same transition but after
  the transition request is handled, then StageCoordinator does not have
  an opportunity to detect and clean up after the change.  Instead, we
  try to detect that case and clean up split so it is not left in an
  inconsistent state

Bug: 426116216
Flag: EXEMPT bugfix
Test: Split trampoline app with another app, go home and relaunch
      trampoline app
Change-Id: Ifaa91f6b7f49b1e02d7b3c4ff68e7a7d7f07ff2f
parent 84512d9b
Loading
Loading
Loading
Loading
+96 −23
Original line number Diff line number Diff line
@@ -48,6 +48,7 @@ import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPos
import static com.android.wm.shell.common.split.SplitScreenUtils.splitFailureMessage;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN;
import static com.android.wm.shell.shared.TransitionUtil.isClosingType;
import static com.android.wm.shell.shared.TransitionUtil.isOpeningMode;
import static com.android.wm.shell.shared.TransitionUtil.isOpeningType;
import static com.android.wm.shell.shared.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_10_90;
@@ -1615,21 +1616,40 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
     * not in split screen, {@link #mLastActiveStage} should be set to STAGE_TYPE_UNDEFINED, and we
     * will do a no-op.
     */
    void dismissSplitKeepingLastActiveStage(@ExitReason int reason) {
        if (!isSplitActive() || mLastActiveStage == STAGE_TYPE_UNDEFINED) {
    void dismissSplitKeepingLastActiveStage(@ExitReason int exitReason) {
        if (mLastActiveStage == STAGE_TYPE_UNDEFINED) {
            // no-op
            return;
        }

        // Need manually clear here due to this transition might be aborted due to keyguard
        // on top and lead to no visible change.
        clearSplitPairedInRecents(reason);
        dismissSplit(mLastActiveStage, exitReason);
        mBreakOnNextWake = false;
    }

    /**
     * Dismisses split in the background.
     */
    public void dismissSplitInBackground(@ExitReason int exitReason) {
        dismissSplit(STAGE_TYPE_UNDEFINED, exitReason);
    }

    /**
     * Starts a new transition to dismiss split.
     */
    private void dismissSplit(@StageType int stageToTop, @ExitReason int exitReason) {
        if (!isSplitActive()) {
            return;
        }
        ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "dismissSplit: stageToTop=%s reason=%s",
                stageTypeToString(stageToTop), exitReasonToString(exitReason));

        // Need manually clear here due to this transition might be aborted due to no visible change
        clearSplitPairedInRecents(exitReason);
        final WindowContainerTransaction wct = new WindowContainerTransaction();
        prepareExitSplitScreen(mLastActiveStage, wct, reason);
        mSplitTransitions.startDismissTransition(wct, this, mLastActiveStage, reason);
        prepareExitSplitScreen(stageToTop, wct, exitReason);
        mSplitTransitions.startDismissTransition(wct, this, stageToTop, exitReason);
        setSplitsVisible(false);
        mBreakOnNextWake = false;
        logExit(reason);
        logExit(exitReason);
    }

    void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
@@ -3171,20 +3191,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
                    // the remote handler.
                    return null;
                }
                boolean anyStageContainsSingleFullscreenTask;
                if (enableFlexibleSplit()) {
                    anyStageContainsSingleFullscreenTask =
                            mStageOrderOperator.getActiveStages().stream()
                                    .anyMatch(stageListener ->
                                            stageListener.containsTask(triggerTask.taskId)
                                                    && stageListener.getChildCount() == 1);
                } else {
                    anyStageContainsSingleFullscreenTask =
                            (mMainStage.containsTask(triggerTask.taskId)
                                    && mMainStage.getChildCount() == 1)
                                    || (mSideStage.containsTask(triggerTask.taskId)
                                    && mSideStage.getChildCount() == 1);
                }
                boolean anyStageContainsSingleFullscreenTask = isLastTaskInAnyStage(
                        triggerTask.taskId);
                if (anyStageContainsSingleFullscreenTask) {
                    // A splitting task is opening to fullscreen causes one side of the split empty,
                    // so appends operations to exit split.
@@ -3245,6 +3253,23 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
        return out;
    }

    /**
     * @return The provided taskId is the last child of any stage.
     */
    private boolean isLastTaskInAnyStage(int taskId) {
        if (enableFlexibleSplit()) {
            return mStageOrderOperator.getActiveStages().stream()
                    .anyMatch(stageListener ->
                            stageListener.containsTask(taskId)
                                    && stageListener.getChildCount() == 1);
        } else {
            return (mMainStage.containsTask(taskId)
                    && mMainStage.getChildCount() == 1)
                    || (mSideStage.containsTask(taskId)
                    && mSideStage.getChildCount() == 1);
        }
    }

    /** @return whether the transition-request implies entering pip from split. */
    public boolean requestImpliesSplitToPip(TransitionRequestInfo request) {
        final TaskInfo triggerTask = request.getTriggerTask();
@@ -3394,6 +3419,53 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
        prepareExitSplitScreen(topStage, outWCT, EXIT_REASON_UNKNOWN);
    }

    /**
     * When we launch an app, there are times when the app trampolines itself into another activity
     * and ends up breaking a split pair (because that second activity already existed as part of a
     * pair). This method is used to detect whether that happened, so we can clean up the split
     * state.
     * @return whether the transition implies a split task being launched in fullscreen resulting
     *         in splitscreen being broken.
     */
    public boolean transitionImpliesSplitToFullscreen(TransitionInfo info) {
        if (!isSplitActive() || isSplitScreenVisible()) {
            // Not in split, or if visible then we would have already handled it.
            return false;
        }

        for (int i = 0; i < info.getChanges().size(); i++) {
            final TransitionInfo.Change change = info.getChanges().get(i);
            final TaskInfo task = change.getTaskInfo();
            if (task == null || task.getActivityType() == ACTIVITY_TYPE_HOME
                    || task.getActivityType() == ACTIVITY_TYPE_RECENTS) {
                continue;
            }

            final boolean isOpening = isOpeningMode(change.getMode());
            final boolean inFullscreen = task.getWindowingMode() == WINDOWING_MODE_FULLSCREEN;
            if (isOpening && inFullscreen) {
                // Note: currently we still rely on non-transition signals to update the stage
                // children so we may end up with an empty stage, but once we migrate away from that
                // we'll actually need to check that the task being launched into fullscreen is
                // actually the last child of a stage
                final boolean hasEmptyStage;
                if (enableFlexibleSplit()) {
                    hasEmptyStage = mStageOrderOperator.getActiveStages().stream()
                            .anyMatch(stage -> stage.getChildCount() == 0);
                } else {
                    hasEmptyStage = mMainStage.getChildCount() == 0
                            || mSideStage.getChildCount() == 0;
                }
                if (isLastTaskInAnyStage(task.taskId) || hasEmptyStage) {
                    ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
                            "Fullscreen task launch transition will break split");
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public void mergeAnimation(IBinder transition, TransitionInfo info,
            @NonNull SurfaceControl.Transaction startT,
@@ -4402,6 +4474,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
        pw.println(prefix + TAG + " mDisplayId=" + mDisplayId);
        pw.println(innerPrefix + "mDividerVisible=" + mDividerVisible);
        pw.println(innerPrefix + "isSplitActive=" + isSplitActive());
        pw.println(innerPrefix + "mLastActiveStage=" + mLastActiveStage);
        pw.println(innerPrefix + "isSplitVisible=" + isSplitScreenVisible());
        pw.println(innerPrefix + "isLeftRightSplit="
                + (mSplitLayout != null ? isLeftRightSplit() : "null"));
+17 −4
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_PIP;
import static android.view.WindowManager.TRANSIT_TO_BACK;

import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_REQUEST;
import static com.android.wm.shell.transition.DefaultMixedHandler.subCopy;
import static com.android.wm.shell.transition.MixedTransitionHelper.animateEnterPipFromSplit;
import static com.android.wm.shell.transition.MixedTransitionHelper.animateKeyguard;
@@ -113,7 +114,7 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition {
                            mKeyguardHandler, mPipHandler);
            case TYPE_OPTIONS_REMOTE_AND_PIP_OR_DESKTOP_CHANGE ->
                    animateOpenIntentWithRemoteAndPipOrDesktop(transition, info, startTransaction,
                            finishTransaction, finishCallback);
                            finishTransaction, finishCallback, mSplitHandler);
            case TYPE_UNFOLD ->
                    animateUnfold(transition, info, startTransaction, finishTransaction,
                            finishCallback);
@@ -197,11 +198,12 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition {
            @NonNull IBinder transition, @NonNull TransitionInfo info,
            @NonNull SurfaceControl.Transaction startTransaction,
            @NonNull SurfaceControl.Transaction finishTransaction,
            @NonNull Transitions.TransitionFinishCallback finishCallback) {
            @NonNull Transitions.TransitionFinishCallback finishCallback,
            @NonNull StageCoordinator splitHandler) {
        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Mixed transition for opening an intent"
                + " with a remote transition and PIP or Desktop #%d", info.getDebugId());
        boolean handledToPipOrDesktop = tryAnimateOpenIntentWithRemoteAndPipOrDesktop(
                info, startTransaction, finishTransaction, finishCallback);
                info, startTransaction, finishTransaction, finishCallback, splitHandler);
        // Consume the transition on remote handler if the leftover handler already handle this
        // transition. And if it cannot, the transition will be handled by remote handler, so don't
        // consume here.
@@ -218,9 +220,20 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition {
            @NonNull TransitionInfo info,
            @NonNull SurfaceControl.Transaction startTransaction,
            @NonNull SurfaceControl.Transaction finishTransaction,
            @NonNull Transitions.TransitionFinishCallback finishCallback) {
            @NonNull Transitions.TransitionFinishCallback finishCallback,
            @NonNull StageCoordinator splitHandler) {
        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
                "tryAnimateOpenIntentWithRemoteAndPipOrDesktop");

        // This is specifically for handling trampolines w/ previously split tasks. In the case that
        // an activity (which will trampoline into a previously split task) is launched into
        // fullscreen, the StageCoordinator does not have enough info at transition-request time to
        // decide whether to handle the transition and never gets a chance to clean up the split
        // state. So we check here to see if that happened, and clean up if so.
        if (splitHandler.transitionImpliesSplitToFullscreen(info)) {
            splitHandler.dismissSplitInBackground(EXIT_REASON_FULLSCREEN_REQUEST);
        }

        TransitionInfo.Change pipChange = null;
        TransitionInfo.Change pipActivityChange = null;
        for (int i = info.getChanges().size() - 1; i >= 0; --i) {