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

Commit 5282b2b0 authored by Nick Chameyev's avatar Nick Chameyev
Browse files

Finish unfold Shell transition when fold is merged in

This CL addresses an issue with UnfoldTransitionHandler
that it doesn't finish the animation when the following order of the events happens:
    - [unfolded the device]
    - handleRequest(unfold transition) -> returns WCT
    - [folded the device]
    - handleRequest(fold transition) -> returns null
    - onStateChangeFinished() -> mAnimationFinished is set to 'true'
    - onFoldedStateChanged(true) -> mAnimationFinished is set to 'false'
    - startAnimation() -> waits for the onStateChangeFinished which
      will never be executed as it has been fired already,
      onFoldedStateChanged() doesn't finish the transition because
      it was invoked before startAnimation()

This is fixed by finishing the unfold transition when merge
request with fold Shell transition is received.

Bug: 372319646
Test: atest UnfoldTransitionHandlerTest
Test: fold multiple times quickly using a script
  => verify all transitions are finished
Flag: EXEMPT bugfix
Change-Id: I1069e3a1b8ba607a0a6f5f3e697f2579966ea146
parent 20377350
Loading
Loading
Loading
Loading
+42 −28
Original line number Diff line number Diff line
@@ -16,9 +16,9 @@

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_FLAG_PHYSICAL_DISPLAY_SWITCH;

import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TRANSITIONS;

@@ -30,6 +30,7 @@ import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
import android.window.WindowContainerTransaction;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

@@ -45,6 +46,8 @@ import com.android.wm.shell.unfold.animation.FullscreenUnfoldTaskAnimator;
import com.android.wm.shell.unfold.animation.SplitTaskUnfoldAnimator;
import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
@@ -56,6 +59,18 @@ import java.util.concurrent.Executor;
 */
public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListener {

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({
            DefaultDisplayChange.DEFAULT_DISPLAY_NO_CHANGE,
            DefaultDisplayChange.DEFAULT_DISPLAY_UNFOLD,
            DefaultDisplayChange.DEFAULT_DISPLAY_FOLD,
    })
    private @interface DefaultDisplayChange {
        int DEFAULT_DISPLAY_NO_CHANGE = 0;
        int DEFAULT_DISPLAY_UNFOLD = 1;
        int DEFAULT_DISPLAY_FOLD = 2;
    }

    private final ShellUnfoldProgressProvider mUnfoldProgressProvider;
    private final Transitions mTransitions;
    private final Executor mExecutor;
@@ -110,16 +125,6 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene
            @NonNull SurfaceControl.Transaction startTransaction,
            @NonNull SurfaceControl.Transaction finishTransaction,
            @NonNull TransitionFinishCallback finishCallback) {
        if (shouldPlayUnfoldAnimation(info) && transition != mTransition) {
            // Take over transition that has unfold, we might receive it if no other handler
            // accepted request in handleRequest, e.g. for rotation + unfold or
            // TRANSIT_NONE + unfold transitions
            mTransition = transition;

            ProtoLog.v(WM_SHELL_TRANSITIONS, "UnfoldTransitionHandler: "
                    + "take over startAnimation");
        }

        if (transition != mTransition) return false;

        for (int i = 0; i < mAnimators.size(); i++) {
@@ -222,6 +227,12 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene
        // Apply changes happening during the unfold animation immediately
        t.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();
        }
    }

    /** Whether `request` contains an unfold action. */
@@ -230,18 +241,25 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene
        if (!ValueAnimator.areAnimatorsEnabled()) return false;

        return (request.getType() == TRANSIT_CHANGE
                && request.getDisplayChange() != null
                && isUnfoldDisplayChange(request.getDisplayChange()));
                && getDefaultDisplayChange(request.getDisplayChange())
                == DefaultDisplayChange.DEFAULT_DISPLAY_UNFOLD);
    }

    @DefaultDisplayChange
    private int getDefaultDisplayChange(
            @Nullable TransitionRequestInfo.DisplayChange displayChange) {
        if (displayChange == null) return DefaultDisplayChange.DEFAULT_DISPLAY_NO_CHANGE;

        if (displayChange.getDisplayId() != DEFAULT_DISPLAY) {
            return DefaultDisplayChange.DEFAULT_DISPLAY_NO_CHANGE;
        }

    private boolean isUnfoldDisplayChange(
            @NonNull TransitionRequestInfo.DisplayChange displayChange) {
        if (!displayChange.isPhysicalDisplayChanged()) {
            return false;
            return DefaultDisplayChange.DEFAULT_DISPLAY_NO_CHANGE;
        }

        if (displayChange.getStartAbsBounds() == null || displayChange.getEndAbsBounds() == null) {
            return false;
            return DefaultDisplayChange.DEFAULT_DISPLAY_NO_CHANGE;
        }

        // Handle only unfolding, currently we don't have an animation when folding
@@ -250,17 +268,11 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene
        final int startArea = displayChange.getStartAbsBounds().width()
                * displayChange.getStartAbsBounds().height();

        return endArea > startArea;
        return endArea > startArea ? DefaultDisplayChange.DEFAULT_DISPLAY_UNFOLD
                : DefaultDisplayChange.DEFAULT_DISPLAY_FOLD;
    }

    /** Whether `transitionInfo` contains an unfold action. */
    public boolean shouldPlayUnfoldAnimation(@NonNull TransitionInfo transitionInfo) {
        // Unfold animation won't play when animations are disabled
        if (!ValueAnimator.areAnimatorsEnabled()) return false;
        // Only handle transitions that are marked as physical display switch
        // See PhysicalDisplaySwitchTransitionLauncher for the conditions
        if ((transitionInfo.getFlags() & TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH) == 0) return false;

    private int getDefaultDisplayChange(@NonNull TransitionInfo transitionInfo) {
        for (int i = 0; i < transitionInfo.getChanges().size(); i++) {
            final TransitionInfo.Change change = transitionInfo.getChanges().get(i);
            // We are interested only in display container changes
@@ -279,11 +291,13 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene
                    * change.getStartAbsBounds().height();

            if (afterArea > beforeArea) {
                return true;
                return DefaultDisplayChange.DEFAULT_DISPLAY_UNFOLD;
            } else {
                return DefaultDisplayChange.DEFAULT_DISPLAY_FOLD;
            }
        }

        return false;
        return DefaultDisplayChange.DEFAULT_DISPLAY_NO_CHANGE;
    }

    @Nullable
+35 −22
Original line number Diff line number Diff line
@@ -18,13 +18,13 @@ package com.android.wm.shell.unfold;

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

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
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;
@@ -50,6 +50,7 @@ import com.android.wm.shell.unfold.animation.SplitTaskUnfoldAnimator;

import org.junit.Before;
import org.junit.Test;
import org.mockito.InOrder;

import java.util.ArrayList;
import java.util.List;
@@ -140,11 +141,12 @@ public class UnfoldTransitionHandlerTest {
    }

    @Test
    public void startAnimation_animationHasNotFinishedYet_doesNotFinishTheTransition() {
    public void handleFoldMergeRequest_finishesTheTransition() {
        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
        mUnfoldTransitionHandler.startAnimation(
                mTransition,
                mock(TransitionInfo.class),
@@ -153,16 +155,24 @@ public class UnfoldTransitionHandlerTest {
                finishCallback
        );

        verify(finishCallback, never()).onTransitionFinished(any());
        // Send fold transition request
        TransitionFinishCallback mergeFinishCallback = mock(TransitionFinishCallback.class);
        mUnfoldTransitionHandler.mergeAnimation(new Binder(), createFoldTransitionInfo(),
                mock(SurfaceControl.Transaction.class), mTransition, mergeFinishCallback);

        // Verify that fold transition is merged into unfold and that unfold is finished
        final InOrder inOrder = inOrder(mergeFinishCallback, finishCallback);
        inOrder.verify(mergeFinishCallback).onTransitionFinished(any());
        inOrder.verify(finishCallback).onTransitionFinished(any());
    }

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

        boolean animationStarted = mUnfoldTransitionHandler.startAnimation(
        mUnfoldTransitionHandler.startAnimation(
                mTransition,
                mock(TransitionInfo.class),
                mock(SurfaceControl.Transaction.class),
@@ -170,17 +180,18 @@ public class UnfoldTransitionHandlerTest {
                finishCallback
        );

        assertThat(animationStarted).isTrue();
        verify(finishCallback, never()).onTransitionFinished(any());
    }

    @Test
    public void startAnimation_differentTransitionFromRequestWithUnfold_startsAnimation() {
        mUnfoldTransitionHandler.handleRequest(new Binder(), createNoneTransitionInfo());
    public void startAnimation_sameTransitionAsHandleRequest_startsAnimation() {
        TransitionRequestInfo requestInfo = createUnfoldTransitionRequestInfo();
        mUnfoldTransitionHandler.handleRequest(mTransition, requestInfo);
        TransitionFinishCallback finishCallback = mock(TransitionFinishCallback.class);

        boolean animationStarted = mUnfoldTransitionHandler.startAnimation(
                mTransition,
                createUnfoldTransitionInfo(),
                mock(TransitionInfo.class),
                mock(SurfaceControl.Transaction.class),
                mock(SurfaceControl.Transaction.class),
                finishCallback
@@ -196,7 +207,7 @@ public class UnfoldTransitionHandlerTest {

        boolean animationStarted = mUnfoldTransitionHandler.startAnimation(
                mTransition,
                createDisplayResizeTransitionInfo(),
                createUnfoldTransitionInfo(),
                mock(SurfaceControl.Transaction.class),
                mock(SurfaceControl.Transaction.class),
                finishCallback
@@ -373,6 +384,19 @@ public class UnfoldTransitionHandlerTest {
                triggerTaskInfo, /* remoteTransition= */ null, displayChange, 0 /* flags */);
    }

    private TransitionInfo createFoldTransitionInfo() {
        final TransitionInfo transitionInfo = new TransitionInfo(TRANSIT_CHANGE, /* flags= */ 0);

        final TransitionInfo.Change change = new TransitionInfo.Change(/* container= */ null,
                /* leash= */ null);
        change.setFlags(TransitionInfo.FLAG_IS_DISPLAY);
        change.setStartAbsBounds(new Rect(0, 0, 200, 200));
        change.setEndAbsBounds(new Rect(0, 0, 100, 100));
        transitionInfo.addChange(change);

        return transitionInfo;
    }

    private TransitionRequestInfo createNoneTransitionInfo() {
        return new TransitionRequestInfo(TRANSIT_NONE,
                /* triggerTask= */ null, /* remoteTransition= */ null,
@@ -443,17 +467,6 @@ public class UnfoldTransitionHandlerTest {
    }

    private TransitionInfo createUnfoldTransitionInfo() {
        TransitionInfo transitionInfo = new TransitionInfo(TRANSIT_CHANGE, /* flags= */ 0);
        TransitionInfo.Change change = new TransitionInfo.Change(null, mock(SurfaceControl.class));
        change.setStartAbsBounds(new Rect(0, 0, 10, 10));
        change.setEndAbsBounds(new Rect(0, 0, 100, 100));
        change.setFlags(TransitionInfo.FLAG_IS_DISPLAY);
        transitionInfo.addChange(change);
        transitionInfo.setFlags(TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH);
        return transitionInfo;
    }

    private TransitionInfo createDisplayResizeTransitionInfo() {
        TransitionInfo transitionInfo = new TransitionInfo(TRANSIT_CHANGE, /* flags= */ 0);
        TransitionInfo.Change change = new TransitionInfo.Change(null, mock(SurfaceControl.class));
        change.setStartAbsBounds(new Rect(0, 0, 10, 10));