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

Commit 33a4e555 authored by Chris Li's avatar Chris Li
Browse files

Add jumpcut TransitionHandler for Task Trampoline Bubble launch

Change to apply a jumpcut animation so that user won't see the
"trampoline".

Bug: 417848405
Test: atest WMShellUnitTests:DefaultMixedHandlerTest
Test: atest WMShellUnitTests:BubbleTransitionsTest
Flag: com.android.window.flags.fix_bubble_trampoline_animation
Change-Id: I298a2cebb470859198c5458e4ef3c655c1da0e64
parent 4d932b19
Loading
Loading
Loading
Loading
+38 −0
Original line number Diff line number Diff line
@@ -1812,6 +1812,44 @@ public class BubbleController implements ConfigurationChangeListener,
                onInflatedCallback);
    }

    /**
     * Jumpcut animation to switch the Task showing in expanded Bubble.
     */
    @NonNull
    Transitions.TransitionHandler jumpcutBubbleSwitchTransition(
            @NonNull ActivityManager.RunningTaskInfo openingTaskInfo,
            @NonNull ActivityManager.RunningTaskInfo closingTaskInfo,
            @NonNull IBinder transition,
            @NonNull Consumer<Transitions.TransitionHandler> onInflatedCallback) {
        final Bubble existingBubble = mBubbleData.getBubbleInStackWithTaskId(
                closingTaskInfo.taskId);
        if (existingBubble == null || mBubbleData.getSelectedBubble() != existingBubble
                || !mBubbleData.isExpanded()) {
            ProtoLog.w(WM_SHELL_BUBBLES, "The existing Bubble for taskId=%s was not expanded,"
                            + " fallback to expandStackAndSelectBubbleForExistingTransition",
                    closingTaskInfo.taskId);
            return expandStackAndSelectBubbleForExistingTransition(openingTaskInfo, transition,
                    onInflatedCallback);
        } else if (mBubbleData.getBubbleInStackWithTaskId(openingTaskInfo.taskId) != null) {
            ProtoLog.w(WM_SHELL_BUBBLES, "There is an existing Bubble for the new launch taskId=%s,"
                            + " fallback to expandStackAndSelectBubbleForExistingTransition",
                    closingTaskInfo.taskId);
            return expandStackAndSelectBubbleForExistingTransition(openingTaskInfo, transition,
                    onInflatedCallback);
        }

        ProtoLog.v(WM_SHELL_BUBBLES, "jumpcutBubbleSwitchTransition() newTaskId=%s oldTaskId=%s",
                openingTaskInfo.taskId, closingTaskInfo.taskId);

        final Bubble newBubble = mBubbleData.getOrCreateBubble(openingTaskInfo);
        newBubble.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);

        return mBubbleTransitions.startJumpcutBubbleSwitchTransition(newBubble, existingBubble,
                mExpandedViewManager, mBubbleTaskViewFactory, mBubblePositioner, mStackView,
                mLayerView, mBubbleIconFactory, mInflateSynchronously, transition,
                onInflatedCallback);
    }

    /**
     * Expands and selects a bubble based on the provided {@link BubbleEntry}. If no bubble
     * exists for this entry, and it is able to bubble, a new bubble will be created.
+257 −0
Original line number Diff line number Diff line
@@ -245,6 +245,19 @@ public class BubbleTransitions {
        mEnterTransitions.put(transition, handler);
    }

    /**
     * Initiates a Task Trampoline Bubble launch for the given transition.
     */
    public void startTaskTrampolineBubbleLaunch(@NonNull IBinder transition,
            @NonNull ActivityManager.RunningTaskInfo openingTask,
            @NonNull ActivityManager.RunningTaskInfo closingTask,
            @NonNull Consumer<TransitionHandler> onInflatedCallback) {
        final TransitionHandler handler =
                mBubbleController.jumpcutBubbleSwitchTransition(openingTask, closingTask,
                        transition, onInflatedCallback);
        mEnterTransitions.put(transition, handler);
    }

    /**
     * Starts a new launch or convert transition to show the given bubble.
     */
@@ -259,6 +272,19 @@ public class BubbleTransitions {
                onInflatedCallback);
    }

    /**
     * Starts a jumpcut transition to update Task in the expanding Bubble.
     */
    public TransitionHandler startJumpcutBubbleSwitchTransition(Bubble openingBubble,
            Bubble closingBubble, BubbleExpandedViewManager expandedViewManager,
            BubbleTaskViewFactory factory, BubblePositioner positioner, BubbleStackView stackView,
            BubbleBarLayerView layerView, BubbleIconFactory iconFactory, boolean inflateSync,
            IBinder transition, Consumer<TransitionHandler> onInflatedCallback) {
        return new JumpcutBubbleSwitchTransition(openingBubble, closingBubble, mContext,
                expandedViewManager, factory, positioner, stackView, layerView, iconFactory,
                inflateSync, transition, onInflatedCallback);
    }

    /**
     * Starts a convert-to-bubble transition.
     *
@@ -792,6 +818,237 @@ public class BubbleTransitions {
        }
    }

    /**
     * Starts a jumpcut to update Task in the expanding Bubble transition.
     *
     * 1. In transition startTransaction, ensure the closing Task surface is attached to its Bubble
     *    TaskView, so that it is visible and unchanged.
     * 2. When opening Bubble TaskView is ready, ensure the opening Task surface is attached to its
     *    TaskView, and being visible, so that we only need to animate TaskView next.
     * 3. Apply jumpcut for the Bubble switch animation, and apply the Transition finishCallback
     *    after the TaskViews finish surface update.
     * TODO(b/408328557): To be consolidated with LaunchOrConvertToBubble and ConvertToBubble
     */
    @VisibleForTesting
    class JumpcutBubbleSwitchTransition implements TransitionHandler, BubbleTransition {
        final BubbleExpandedViewTransitionAnimator mExpandedViewAnimator;
        private final TransitionProgress mTransitionProgress;
        Bubble mOpeningBubble;
        Bubble mClosingBubble;

        IBinder mTransition;
        Transitions.TransitionFinishCallback mFinishCb;
        WindowContainerTransaction mFinishWct = null;
        // The task info is resolved once we find the task from the transition info using the
        // pending launch cookie otherwise
        @Nullable
        TaskInfo mOpeningTaskInfo;

        private SurfaceControl.Transaction mFinishT;
        private SurfaceControl mTaskLeash;

        JumpcutBubbleSwitchTransition(Bubble openingBubble, Bubble closingBubble, Context context,
                BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory factory,
                BubblePositioner positioner, BubbleStackView stackView,
                BubbleBarLayerView layerView, BubbleIconFactory iconFactory,
                boolean inflateSync, IBinder transition,
                Consumer<TransitionHandler> onInflatedCallback) {
            if (layerView != null) {
                mExpandedViewAnimator = layerView;
            } else {
                mExpandedViewAnimator = stackView;
            }
            mOpeningBubble = openingBubble;
            mClosingBubble = closingBubble;
            mTransition = transition;
            mTransitionProgress = new TransitionProgress(openingBubble);
            mOpeningBubble.setInflateSynchronously(inflateSync);
            mOpeningBubble.setPreparingTransition(this);
            // Still need the inflate to update the app icon in Bubble.
            mOpeningBubble.inflate(
                    b -> {
                        onInflated(b);
                        onInflatedCallback.accept(JumpcutBubbleSwitchTransition.this);
                    },
                    context,
                    expandedViewManager,
                    factory,
                    positioner,
                    stackView,
                    layerView,
                    iconFactory,
                    mAppInfoProvider,
                    false /* skipInflation */);
        }

        @VisibleForTesting
        void onInflated(Bubble b) {
            ProtoLog.d(WM_SHELL_BUBBLES_NOISY, "JumpcutBubbleSwitchTransition.onInflated()");
            if (b != mOpeningBubble) {
                throw new IllegalArgumentException("inflate callback doesn't match bubble");
            }
            final TaskView tv = b.getTaskView();
            tv.setSurfaceLifecycle(SurfaceView.SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT);
            final TaskViewRepository.TaskViewState state = mRepository.byTaskView(
                    tv.getController());
            if (state != null) {
                state.mVisible = true;
            }
            mTransitionProgress.setInflated();
            // Remove any intermediate queued transitions that were started as a result of the
            // inflation (the task view will be in the right bounds)
            mTaskViewTransitions.removePendingTransitions(tv.getController());
            mTaskViewTransitions.enqueueExternal(tv.getController(), () -> mTransition);
        }

        @Override
        public void skip() {
            ProtoLog.d(WM_SHELL_BUBBLES_NOISY, "JumpcutBubbleSwitchTransition.skip()");
            mOpeningBubble.setPreparingTransition(null);
            cleanup();
        }

        @Override
        public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
                @Nullable TransitionRequestInfo request) {
            return null;
        }

        @Override
        public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
                @NonNull SurfaceControl.Transaction startT,
                @NonNull SurfaceControl.Transaction finishT,
                @NonNull IBinder mergeTarget,
                @NonNull Transitions.TransitionFinishCallback finishCallback) {
        }

        @Override
        public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
                @NonNull SurfaceControl.Transaction finishTransaction) {
            if (!aborted) return;
            mTaskViewTransitions.onExternalDone(mTransition);
            mTransition = null;
        }

        /**
         * @return true As DefaultMixedTransition assumes that this transition will be handled by
         * this handler in all cases.
         */
        @Override
        public boolean startAnimation(@NonNull IBinder transition,
                @NonNull TransitionInfo info,
                @NonNull SurfaceControl.Transaction startTransaction,
                @NonNull SurfaceControl.Transaction finishTransaction,
                @NonNull Transitions.TransitionFinishCallback finishCallback) {
            ProtoLog.d(WM_SHELL_BUBBLES_NOISY, "JumpcutBubbleSwitchTransition.startAnimation()");

            final TransitionInfo.Change enterBubbleTask = getEnterBubbleTask(info);
            final TransitionInfo.Change closingBubbleTask = getClosingBubbleTask(info);
            mOpeningTaskInfo = enterBubbleTask.getTaskInfo();
            mFinishWct = new WindowContainerTransaction();
            mFinishT = finishTransaction;
            mFinishCb = finishCallback;
            mTaskLeash = enterBubbleTask.getLeash();

            mBubbleData.notificationEntryUpdated(mOpeningBubble, /* suppressFlyout= */ true,
                    /* showInShade= */ false);

            // Keep showing the closing Bubble Task within the closing Bubble TaskView until the
            // opening Bubble TaskView is ready.
            final SurfaceControl closingBubbleTaskLeash = closingBubbleTask.getLeash();
            final SurfaceControl closingBubbleTaskView = mClosingBubble.getTaskView()
                    .getSurfaceControl();
            startTransaction.setAlpha(closingBubbleTaskLeash, 1f)
                    .setPosition(closingBubbleTaskLeash, 0, 0)
                    .reparent(closingBubbleTaskLeash, closingBubbleTaskView)
                    .show(closingBubbleTaskLeash);

            startTransaction.apply();

            mTransitionProgress.setTransitionReady();
            if (mExpandedViewAnimator.canExpandView(mOpeningBubble)) {
                final BubbleViewProvider priorBubble =
                        mExpandedViewAnimator.prepareConvertedView(mOpeningBubble);
                if (priorBubble != mClosingBubble
                        // TODO b/419347947 BubbleStackView will return null for non-overflow bubble
                        && priorBubble != null) {
                    throw new IllegalStateException("Previous expanded Bubble was taskId="
                            + priorBubble.getTaskId() + " but expect taskId="
                            + mClosingBubble.getTaskId());
                }
            } else if (mExpandedViewAnimator.isExpanded()) {
                mTransitionProgress.setReadyToExpand();
            }
            if (mTransitionProgress.isReadyToAnimate()) {
                animateJumpcut();
            }

            return true;
        }

        @Override
        public void continueExpand() {
            mTransitionProgress.setReadyToExpand();
        }

        @Override
        public void surfaceCreated() {
            mTransitionProgress.setSurfaceReady();
            mMainExecutor.execute(() -> {
                ProtoLog.d(WM_SHELL_BUBBLES_NOISY,
                        "JumpcutBubbleSwitchTransition.surfaceCreated()");
                final TaskViewTaskController tvc = mOpeningBubble.getTaskView().getController();
                final TaskViewRepository.TaskViewState state = mRepository.byTaskView(tvc);
                if (state == null) return;
                state.mVisible = true;
                if (mTransitionProgress.isReadyToAnimate()) {
                    animateJumpcut();
                }
            });
        }

        private void animateJumpcut() {
            ProtoLog.d(WM_SHELL_BUBBLES_NOISY,
                    "JumpcutBubbleSwitchTransition.applyJumpcut()");
            mTaskViewTransitions.onExternalDone(mTransition);
            final TaskViewTaskController tv = mOpeningBubble.getTaskView().getController();

            // Prepare the transaction to apply when the TaskView surface is ready.
            final SurfaceControl.Transaction startT = new SurfaceControl.Transaction();
            final SurfaceControl openingTaskViewLeash = mOpeningBubble.getTaskView()
                    .getSurfaceControl();
            startT.setAlpha(mTaskLeash, 1f)
                    // Set task position to 0,0 as it will be placed inside the TaskView
                    .setPosition(mTaskLeash, 0, 0)
                    .reparent(mTaskLeash, openingTaskViewLeash)
                    .show(mTaskLeash);
            mTaskViewTransitions.prepareOpenAnimation(tv, true /* new */, startT, mFinishT,
                    (ActivityManager.RunningTaskInfo) mOpeningTaskInfo, mTaskLeash, mFinishWct);
            startT.apply();

            // Add the task view task listener manually since we aren't going through
            // TaskViewTransitions (which normally sets up the listener via a pending launch cookie)
            // Note: In this path, because a new task is being started, the transition may receive
            // the transition for the task before the organizer does
            mTaskOrganizer.addListenerForTaskId(tv, mOpeningTaskInfo.taskId);

            if (mFinishWct.isEmpty()) {
                mFinishWct = null;
            }

            mExpandedViewAnimator.animateExpand(mClosingBubble, this::cleanup);
        }

        private void cleanup() {
            if (mFinishCb != null) {
                ProtoLog.d(WM_SHELL_BUBBLES_NOISY, "JumpcutBubbleSwitchTransition.cleanup()");
                mFinishCb.onTransitionFinished(mFinishWct);
                mFinishCb = null;
            }
            mOpeningBubble.setPreparingTransition(null);
        }
    }

    /**
     * Starts a new transition into a bubble, which will either play a launch animation (if the task
     * was not previously visible) or a convert animation (if the task is currently visible).
+5 −4
Original line number Diff line number Diff line
@@ -500,13 +500,14 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition {
                    transition, info, startTransaction, finishTransaction, finishCallback);
        };

        if (closingBubble != null && isOpeningType(enterBubbleTask.getMode())) {
        if (com.android.window.flags.Flags.fixBubbleTrampolineAnimation()
                && closingBubble != null && isOpeningType(enterBubbleTask.getMode())) {
            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for "
                    + "opening bubble from another closing bubbled task");
            // Task Trampoline (Case 5)
            // TODO(b/417848405): Update the trampoline transition to jumpcut.
            bubbleTransitions.startBubbleToBubbleLaunchOrExistingBubbleConvert(
                    transition, enterBubbleTask.getTaskInfo(), onInflatedCallback);
            bubbleTransitions.startTaskTrampolineBubbleLaunch(
                    transition, enterBubbleTask.getTaskInfo(),
                    closingBubble.getTaskInfo(), onInflatedCallback);
        } else {
            // Opening a Bubble Task (Case 3/6)
            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for "
+120 −27

File changed.

Preview size limit exceeded, changes collapsed.

+28 −0
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
import android.os.Binder
import android.platform.test.annotations.EnableFlags
import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_CLOSE
import android.view.WindowManager.TRANSIT_OPEN
import android.window.IRemoteTransition
import android.window.RemoteTransition
@@ -28,6 +29,7 @@ import android.window.TransitionInfo
import android.window.TransitionRequestInfo
import android.window.WindowContainerToken
import androidx.test.filters.SmallTest
import com.android.internal.hidden_from_bootclasspath.com.android.window.flags.Flags.FLAG_FIX_BUBBLE_TRAMPOLINE_ANIMATION
import com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestShellExecutor
@@ -267,6 +269,32 @@ class DefaultMixedHandlerTest : ShellTestCase() {
            any(), any(), any())
    }

    @Test
    @EnableFlags(FLAG_ENABLE_CREATE_ANY_BUBBLE, FLAG_FIX_BUBBLE_TRAMPOLINE_ANIMATION)
    fun test_startAnimation_taskTrampolineBubbleLaunch() {
        val openingChange =
            TransitionInfo.Change(mock<WindowContainerToken>(), mock<SurfaceControl>())
        openingChange.mode = TRANSIT_OPEN
        openingChange.taskInfo = createRunningTask()
        val closingChange =
            TransitionInfo.Change(mock<WindowContainerToken>(), mock<SurfaceControl>())
        closingChange.mode = TRANSIT_CLOSE
        closingChange.taskInfo = createRunningTask()
        val info = TransitionInfo(TRANSIT_OPEN, 0)
        info.addChange(openingChange)
        info.addChange(closingChange)

        bubbleController.stub {
            on { shouldBeAppBubble(any()) } doReturn true
        }

        mixedHandler.startAnimation(Binder(), info, mock<SurfaceControl.Transaction>(),
            mock<SurfaceControl.Transaction>(), mock<Transitions.TransitionFinishCallback>())

        verify(bubbleTransitions).startTaskTrampolineBubbleLaunch(
            any(), eq(openingChange.taskInfo!!), eq(closingChange.taskInfo!!), any())
    }

    private fun createTransitionRequestInfo(
        runningTask: RunningTaskInfo? = null,
        remote: RemoteTransition? = null,