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

Commit 915bffe5 authored by Nick Chameyev's avatar Nick Chameyev
Browse files

Merge CLOSE transitions to unfold transitions

When we unfold while we hold the -1 screen on launcher
in slightly opened state, the TaskFragment will be dismissed
and hidden. This might happen when we already started animating
unfold shell transition, in this case the dismissal will
be animated only after the unfold shell transition is finished.

To fix this issue we could accept TRANSIT_CLOSE merge request
for the task fragment close transitions, and hide the participating
layers immediately as part of startTransaction.

Bug: 431983411
Flag: EXEMPT bugfix
Test: atest UnfoldTransitionHandlerTest
Change-Id: I0b870dfe080bd89ebbc6c00ec9de2704c2227ff6
parent fc0658ab
Loading
Loading
Loading
Loading
+46 −13
Original line number Diff line number Diff line
@@ -19,6 +19,8 @@ package com.android.wm.shell.unfold;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.KEYGUARD_VISIBILITY_TRANSIT_FLAGS;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_TO_BACK;

import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TRANSITIONS;
import static com.android.wm.shell.transition.Transitions.TRANSIT_BUBBLE_CONVERT_FLOATING_TO_BAR;
@@ -234,16 +236,34 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene
            @NonNull SurfaceControl.Transaction finishT,
            @NonNull IBinder mergeTarget,
            @NonNull TransitionFinishCallback finishCallback) {
        if (info.getType() != TRANSIT_CHANGE
                && info.getType() != TRANSIT_BUBBLE_CONVERT_FLOATING_TO_BAR) {
            return;
        }
        if ((info.getFlags() & KEYGUARD_VISIBILITY_TRANSIT_FLAGS) != 0) {
            return;
        }
        // TODO (b/286928742) unfold transition handler should be part of mixed handler to
        //  handle merges better.
        final boolean merged = switch (info.getType()) {
            case TRANSIT_CHANGE, TRANSIT_BUBBLE_CONVERT_FLOATING_TO_BAR ->
                    tryMergeBubbleTaskTransition(info, startT, finishT);
            case TRANSIT_CLOSE -> tryMergeTaskFragmentClose(info, startT);
            default -> false;
        };

        if (!merged) return;

        // Apply changes happening during the unfold animation immediately
        startT.apply();
        finishCallback.onTransitionFinished(null);

        if (getDefaultDisplayChange(info) == DefaultDisplayChange.DEFAULT_DISPLAY_FOLD) {
            // Force-finish current unfold animation as we are processing folding now which doesn't
            // have any animations on the Shell side
            finishTransitionIfNeeded();
        }
    }

    private boolean tryMergeBubbleTaskTransition(@NonNull TransitionInfo info,
            @NonNull SurfaceControl.Transaction startT,
            @NonNull SurfaceControl.Transaction finishT) {
        for (int i = 0; i < info.getChanges().size(); ++i) {
            final TransitionInfo.Change change = info.getChanges().get(i);
            final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
@@ -258,24 +278,37 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene
                                    .get()
                                    .mergeTaskWithUnfold(taskInfo, info, change, startT, finishT);
                    if (!merged) {
                        return;
                        return false;
                    }
                } else {
                    return;
                    return false;
                }
            }
        }
        // Apply changes happening during the unfold animation immediately
        startT.apply();
        finishCallback.onTransitionFinished(null);
        return true;
    }

        if (getDefaultDisplayChange(info) == DefaultDisplayChange.DEFAULT_DISPLAY_FOLD) {
            // Force-finish current unfold animation as we are processing folding now which doesn't
            // have any animations on the Shell side
            finishTransitionIfNeeded();
    /**
     * Tries to merge TRANSIT_CLOSE for task fragments
     * @return true if we should continue with merging, false otherwise
     */
    private boolean tryMergeTaskFragmentClose(@NonNull TransitionInfo info,
            @NonNull SurfaceControl.Transaction startT) {
        boolean shouldMerge = false;
        for (int i = 0; i < info.getChanges().size(); ++i) {
            final TransitionInfo.Change change = info.getChanges().get(i);
            final boolean isHideChange = change.getMode() == TRANSIT_TO_BACK
                    || change.getMode() == TRANSIT_CLOSE;

            if (change.getTaskFragmentToken() != null && isHideChange) {
                shouldMerge = true;
                startT.hide(change.getLeash());
            }
        }

        return shouldMerge;
    }

    /** Whether `request` contains an unfold action. */
    public boolean shouldPlayUnfoldAnimation(@NonNull TransitionRequestInfo request) {
        // Unfold animation won't play when animations are disabled
+101 −88
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.wm.shell.unfold;

import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
import static android.view.WindowManager.TRANSIT_NONE;

@@ -25,12 +26,15 @@ import static com.android.wm.shell.unfold.UnfoldTransitionHandler.FINISH_ANIMATI
import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.RETURNS_SELF;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.graphics.Rect;
import android.os.Binder;
@@ -161,19 +165,11 @@ public class UnfoldTransitionHandlerTest extends ShellTestCase {
        TransitionFinishCallback finishCallback = mock(TransitionFinishCallback.class);
        // Starts the animation, the handler should wait for mShellUnfoldProgressProvider to
        // notify about the end of the animation
        mUnfoldTransitionHandler.startAnimation(
                mTransition,
                mock(TransitionInfo.class),
                mock(SurfaceControl.Transaction.class),
                mock(SurfaceControl.Transaction.class),
                finishCallback
        );
        startAnimation(finishCallback);

        // Send fold transition request
        TransitionFinishCallback mergeFinishCallback = mock(TransitionFinishCallback.class);
        mUnfoldTransitionHandler.mergeAnimation(new Binder(), createFoldTransitionInfo(),
                mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class),
                mTransition, mergeFinishCallback);
        mergeAnimation(createFoldTransitionInfo(), mergeFinishCallback);
        mTestLooper.dispatchAll();

        // Verify that fold transition is merged into unfold and that unfold is finished
@@ -182,19 +178,61 @@ public class UnfoldTransitionHandlerTest extends ShellTestCase {
        inOrder.verify(finishCallback).onTransitionFinished(any());
    }

    @Test
    public void handleCloseTransitionMergeRequestWithTaskFragmentClose_acceptsMerge() {
        TransitionRequestInfo requestInfo = createUnfoldTransitionRequestInfo();
        mUnfoldTransitionHandler.handleRequest(mTransition, requestInfo);
        TransitionFinishCallback finishCallback = mock(TransitionFinishCallback.class);
        // Starts the animation, the handler should wait for mShellUnfoldProgressProvider to
        // notify about the end of the animation
        startAnimation(finishCallback);

        // Create CLOSE transition request with task fragment TRANSIT_CLOSE change
        final TransitionInfo.Change closeTaskFragmentChange =
                new TransitionInfo.Change(/* container= */ null, /* leash= */ null);
        closeTaskFragmentChange.setTaskFragmentToken(new Binder());
        closeTaskFragmentChange.setMode(TRANSIT_CLOSE);
        final TransitionInfo closeTransitionInfo =
                createCloseTransitionInfo(closeTaskFragmentChange);

        TransitionFinishCallback mergeFinishCallback = mock(TransitionFinishCallback.class);
        mergeAnimation(closeTransitionInfo, mergeFinishCallback);
        mTestLooper.dispatchAll();

        verify(mergeFinishCallback).onTransitionFinished(any());
    }

    @Test
    public void handleCloseTransitionMergeRequestWithoutTaskFragmentClose_doesNotAcceptMerge() {
        TransitionRequestInfo requestInfo = createUnfoldTransitionRequestInfo();
        mUnfoldTransitionHandler.handleRequest(mTransition, requestInfo);
        TransitionFinishCallback finishCallback = mock(TransitionFinishCallback.class);
        // Starts the animation, the handler should wait for mShellUnfoldProgressProvider to
        // notify about the end of the animation
        startAnimation(finishCallback);

        // Create CLOSE transition without task fragment change (task fragment token is null)
        final TransitionInfo.Change closeTaskFragmentChange =
                new TransitionInfo.Change(/* container= */ null, /* leash= */ null);
        closeTaskFragmentChange.setTaskFragmentToken(null);
        closeTaskFragmentChange.setMode(TRANSIT_CLOSE);
        final TransitionInfo closeTransitionInfo =
                createCloseTransitionInfo(closeTaskFragmentChange);

        TransitionFinishCallback mergeFinishCallback = mock(TransitionFinishCallback.class);
        mergeAnimation(closeTransitionInfo, mergeFinishCallback);
        mTestLooper.dispatchAll();

        verify(mergeFinishCallback, never()).onTransitionFinished(any());
    }

    @Test
    public void startAnimation_animationHasNotFinishedYet_doesNotFinishTheTransition() {
        TransitionRequestInfo requestInfo = createUnfoldTransitionRequestInfo();
        mUnfoldTransitionHandler.handleRequest(mTransition, requestInfo);
        TransitionFinishCallback finishCallback = mock(TransitionFinishCallback.class);

        mUnfoldTransitionHandler.startAnimation(
                mTransition,
                mock(TransitionInfo.class),
                mock(SurfaceControl.Transaction.class),
                mock(SurfaceControl.Transaction.class),
                finishCallback
        );
        startAnimation(finishCallback);

        verify(finishCallback, never()).onTransitionFinished(any());
    }
@@ -205,11 +243,9 @@ public class UnfoldTransitionHandlerTest extends ShellTestCase {
        mUnfoldTransitionHandler.handleRequest(mTransition, requestInfo);
        TransitionFinishCallback finishCallback = mock(TransitionFinishCallback.class);

        boolean animationStarted = mUnfoldTransitionHandler.startAnimation(
        boolean animationStarted = startAnimation(
                mTransition,
                mock(TransitionInfo.class),
                mock(SurfaceControl.Transaction.class),
                mock(SurfaceControl.Transaction.class),
                finishCallback
        );

@@ -221,13 +257,7 @@ public class UnfoldTransitionHandlerTest extends ShellTestCase {
        TransitionRequestInfo requestInfo = createUnfoldTransitionRequestInfo();
        mUnfoldTransitionHandler.handleRequest(mTransition, requestInfo);
        TransitionFinishCallback finishCallback = mock(TransitionFinishCallback.class);
        mUnfoldTransitionHandler.startAnimation(
                mTransition,
                mock(TransitionInfo.class),
                mock(SurfaceControl.Transaction.class),
                mock(SurfaceControl.Transaction.class),
                finishCallback
        );
        startAnimation(finishCallback);

        mTestLooper.moveTimeForward(FINISH_ANIMATION_TIMEOUT_MILLIS + 1);
        mTestLooper.dispatchAll();
@@ -240,13 +270,8 @@ public class UnfoldTransitionHandlerTest extends ShellTestCase {
        mUnfoldTransitionHandler.handleRequest(new Binder(), createNoneTransitionInfo());
        TransitionFinishCallback finishCallback = mock(TransitionFinishCallback.class);

        boolean animationStarted = mUnfoldTransitionHandler.startAnimation(
                mTransition,
                createUnfoldTransitionInfo(),
                mock(SurfaceControl.Transaction.class),
                mock(SurfaceControl.Transaction.class),
                finishCallback
        );
        boolean animationStarted = startAnimation(mTransition, createUnfoldTransitionInfo(),
                finishCallback);

        assertThat(animationStarted).isFalse();
    }
@@ -256,13 +281,8 @@ public class UnfoldTransitionHandlerTest extends ShellTestCase {
        mUnfoldTransitionHandler.handleRequest(new Binder(), createNoneTransitionInfo());
        TransitionFinishCallback finishCallback = mock(TransitionFinishCallback.class);

        boolean animationStarted = mUnfoldTransitionHandler.startAnimation(
                mTransition,
                createNonUnfoldTransitionInfo(),
                mock(SurfaceControl.Transaction.class),
                mock(SurfaceControl.Transaction.class),
                finishCallback
        );
        boolean animationStarted = startAnimation(mTransition, createNonUnfoldTransitionInfo(),
                finishCallback);

        assertThat(animationStarted).isFalse();
    }
@@ -273,13 +293,7 @@ public class UnfoldTransitionHandlerTest extends ShellTestCase {
        mUnfoldTransitionHandler.handleRequest(mTransition, requestInfo);
        TransitionFinishCallback finishCallback = mock(TransitionFinishCallback.class);

        mUnfoldTransitionHandler.startAnimation(
                mTransition,
                mock(TransitionInfo.class),
                mock(SurfaceControl.Transaction.class),
                mock(SurfaceControl.Transaction.class),
                finishCallback
        );
        startAnimation(finishCallback);
        mShellUnfoldProgressProvider.onStateChangeStarted();
        mShellUnfoldProgressProvider.onStateChangeFinished();

@@ -295,13 +309,7 @@ public class UnfoldTransitionHandlerTest extends ShellTestCase {
        mShellUnfoldProgressProvider.onStateChangeStarted();
        mShellUnfoldProgressProvider.onStateChangeProgress(0.5f);
        mShellUnfoldProgressProvider.onStateChangeFinished();
        mUnfoldTransitionHandler.startAnimation(
                mTransition,
                mock(TransitionInfo.class),
                mock(SurfaceControl.Transaction.class),
                mock(SurfaceControl.Transaction.class),
                finishCallback
        );
        startAnimation(finishCallback);

        verify(finishCallback).onTransitionFinished(any());
    }
@@ -316,13 +324,7 @@ public class UnfoldTransitionHandlerTest extends ShellTestCase {
        mShellUnfoldProgressProvider.onStateChangeStarted();
        mShellUnfoldProgressProvider.onStateChangeFinished();
        mUnfoldTransitionHandler.handleRequest(mTransition, requestInfo);
        mUnfoldTransitionHandler.startAnimation(
                mTransition,
                mock(TransitionInfo.class),
                mock(SurfaceControl.Transaction.class),
                mock(SurfaceControl.Transaction.class),
                finishCallback
        );
        startAnimation(finishCallback);
        clearInvocations(finishCallback);

        // Fold
@@ -354,13 +356,7 @@ public class UnfoldTransitionHandlerTest extends ShellTestCase {
        // Unfold
        mShellUnfoldProgressProvider.onFoldStateChanged(/* isFolded= */ false);
        mUnfoldTransitionHandler.handleRequest(mTransition, requestInfo);
        mUnfoldTransitionHandler.startAnimation(
                mTransition,
                mock(TransitionInfo.class),
                mock(SurfaceControl.Transaction.class),
                mock(SurfaceControl.Transaction.class),
                finishCallback
        );
        startAnimation(finishCallback);

        // Start animation but don't finish it
        mShellUnfoldProgressProvider.onStateChangeStarted();
@@ -379,31 +375,16 @@ public class UnfoldTransitionHandlerTest extends ShellTestCase {
        TransitionFinishCallback finishCallback = mock(TransitionFinishCallback.class);
        TransitionFinishCallback mergeCallback = mock(TransitionFinishCallback.class);

        mUnfoldTransitionHandler.startAnimation(
                mTransition,
                mock(TransitionInfo.class),
                mock(SurfaceControl.Transaction.class),
                mock(SurfaceControl.Transaction.class),
                finishCallback);
        startAnimation(finishCallback);

        // Offer a keyguard unlock transition - this should NOT merge
        mUnfoldTransitionHandler.mergeAnimation(
                new Binder(),
        mergeAnimation(
                new TransitionInfoBuilder(TRANSIT_CHANGE, TRANSIT_FLAG_KEYGUARD_GOING_AWAY).build(),
                mock(SurfaceControl.Transaction.class),
                mock(SurfaceControl.Transaction.class),
                mTransition,
                mergeCallback);
        verify(finishCallback, never()).onTransitionFinished(any());

        // Offer a CHANGE-only transition - this SHOULD merge (b/278064943)
        mUnfoldTransitionHandler.mergeAnimation(
                new Binder(),
                new TransitionInfoBuilder(TRANSIT_CHANGE).build(),
                mock(SurfaceControl.Transaction.class),
                mock(SurfaceControl.Transaction.class),
                mTransition,
                mergeCallback);
        mergeAnimation(new TransitionInfoBuilder(TRANSIT_CHANGE).build(), mergeCallback);
        verify(mergeCallback).onTransitionFinished(any());

        // We should never have finished the original transition.
@@ -434,6 +415,38 @@ public class UnfoldTransitionHandlerTest extends ShellTestCase {
        return transitionInfo;
    }

    private TransitionInfo createCloseTransitionInfo(@Nullable TransitionInfo.Change changeToAdd) {
        final TransitionInfo transitionInfo = new TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0);
        transitionInfo.addChange(changeToAdd);
        return transitionInfo;
    }

    private boolean startAnimation(@NonNull IBinder transition,
            @NonNull TransitionInfo transitionInfo,
            @NonNull TransitionFinishCallback finishCallback) {
        return mUnfoldTransitionHandler.startAnimation(
                transition,
                transitionInfo,
                mTransactionPool.acquire(),
                mTransactionPool.acquire(),
                finishCallback);
    }

    private boolean startAnimation(@NonNull TransitionFinishCallback finishCallback) {
        return startAnimation(mTransition, mock(TransitionInfo.class), finishCallback);
    }

    private void mergeAnimation(@NonNull TransitionInfo transitionInfo,
            @NonNull TransitionFinishCallback mergeCallback) {
        mUnfoldTransitionHandler.mergeAnimation(
                new Binder(),
                transitionInfo,
                mTransactionPool.acquire(),
                mTransactionPool.acquire(),
                mTransition,
                mergeCallback);
    }

    private TransitionRequestInfo createNoneTransitionInfo() {
        return new TransitionRequestInfo(TRANSIT_NONE,
                /* triggerTask= */ null, /* remoteTransition= */ null,
@@ -474,7 +487,7 @@ public class UnfoldTransitionHandlerTest extends ShellTestCase {
    private static class TestTransactionPool extends TransactionPool {
        @Override
        public SurfaceControl.Transaction acquire() {
            return mock(SurfaceControl.Transaction.class);
            return mock(SurfaceControl.Transaction.class, RETURNS_SELF);
        }

        @Override