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

Commit 1b259fc1 authored by Hongwei Wang's avatar Hongwei Wang Committed by Android (Google) Code Review
Browse files

Merge "[PiP2] Add PipEnterAnimatorTest" into main

parents 25260b2e f7bad050
Loading
Loading
Loading
Loading
+70 −54
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.RectEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
@@ -34,6 +35,7 @@ import android.window.TransitionInfo;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.internal.annotations.VisibleForTesting;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.R;
import com.android.wm.shell.common.pip.PipUtils;
@@ -45,8 +47,7 @@ import com.android.wm.shell.shared.pip.PipContentOverlay;
/**
 * Animator that handles bounds animations for entering PIP.
 */
public class PipEnterAnimator extends ValueAnimator
        implements ValueAnimator.AnimatorUpdateListener, ValueAnimator.AnimatorListener {
public class PipEnterAnimator extends ValueAnimator {
    @NonNull private final SurfaceControl mLeash;
    private final SurfaceControl.Transaction mStartTransaction;
    private final SurfaceControl.Transaction mFinishTransaction;
@@ -56,49 +57,82 @@ public class PipEnterAnimator extends ValueAnimator

    private final RectEvaluator mRectEvaluator;
    private final Rect mEndBounds = new Rect();
    @Nullable private final Rect mSourceRectHint;
    private final @Surface.Rotation int mRotation;
    @Nullable private Runnable mAnimationStartCallback;
    @Nullable private Runnable mAnimationEndCallback;

    private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
    private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
            mSurfaceControlTransactionFactory;
    Matrix mTransformTensor = new Matrix();
    final float[] mMatrixTmp = new float[9];
    @Nullable private PipContentOverlay mContentOverlay;

    private PipAppIconOverlaySupplier mPipAppIconOverlaySupplier;

    // Internal state representing initial transform - cached to avoid recalculation.
    private final PointF mInitScale = new PointF();
    private final PointF mInitPos = new PointF();
    private final Rect mInitCrop = new Rect();
    private final PointF mInitActivityScale = new PointF();
    private final PointF mInitActivityPos = new PointF();

    private final Animator.AnimatorListener mAnimatorListener = new AnimatorListenerAdapter() {
        @Override
        public void onAnimationStart(Animator animation) {
            super.onAnimationStart(animation);
            if (mAnimationStartCallback != null) {
                mAnimationStartCallback.run();
            }
            if (mStartTransaction != null) {
                onEnterAnimationUpdate(0f /* fraction */, mStartTransaction);
                mStartTransaction.apply();
            }
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            super.onAnimationEnd(animation);
            if (mFinishTransaction != null) {
                onEnterAnimationUpdate(1f /* fraction */, mFinishTransaction);
            }
            if (mAnimationEndCallback != null) {
                mAnimationEndCallback.run();
            }
        }
    };

    private final AnimatorUpdateListener mAnimatorUpdateListener = new AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(@NonNull ValueAnimator animation) {
            final SurfaceControl.Transaction tx =
                    mSurfaceControlTransactionFactory.getTransaction();
            final float fraction = getAnimatedFraction();
            onEnterAnimationUpdate(fraction, tx);
            tx.apply();
        }
    };

    public PipEnterAnimator(Context context,
            @NonNull SurfaceControl leash,
            SurfaceControl.Transaction startTransaction,
            SurfaceControl.Transaction finishTransaction,
            @NonNull Rect endBounds,
            @Nullable Rect sourceRectHint,
            @Surface.Rotation int rotation) {
        mLeash = leash;
        mStartTransaction = startTransaction;
        mFinishTransaction = finishTransaction;
        mRectEvaluator = new RectEvaluator(mAnimatedRect);
        mEndBounds.set(endBounds);
        mSourceRectHint = sourceRectHint != null ? new Rect(sourceRectHint) : null;
        mRotation = rotation;
        mSurfaceControlTransactionFactory =
                new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
        mPipAppIconOverlaySupplier = this::getAppIconOverlay;

        final int enterAnimationDuration = context.getResources()
                .getInteger(R.integer.config_pipEnterAnimationDuration);
        setDuration(enterAnimationDuration);
        setFloatValues(0f, 1f);
        setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
        addListener(this);
        addUpdateListener(this);
        addListener(mAnimatorListener);
        addUpdateListener(mAnimatorUpdateListener);
    }

    public void setAnimationStartCallback(@NonNull Runnable runnable) {
@@ -109,35 +143,6 @@ public class PipEnterAnimator extends ValueAnimator
        mAnimationEndCallback = runnable;
    }

    @Override
    public void onAnimationStart(@NonNull Animator animation) {
        if (mAnimationStartCallback != null) {
            mAnimationStartCallback.run();
        }
        if (mStartTransaction != null) {
            onEnterAnimationUpdate(0f /* fraction */, mStartTransaction);
            mStartTransaction.apply();
        }
    }

    @Override
    public void onAnimationEnd(@NonNull Animator animation) {
        if (mFinishTransaction != null) {
            onEnterAnimationUpdate(1f /* fraction */, mFinishTransaction);
        }
        if (mAnimationEndCallback != null) {
            mAnimationEndCallback.run();
        }
    }

    @Override
    public void onAnimationUpdate(@NonNull ValueAnimator animation) {
        final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction();
        final float fraction = getAnimatedFraction();
        onEnterAnimationUpdate(fraction, tx);
        tx.apply();
    }

    /**
     * Updates the transaction to reflect the state of PiP leash at a certain fraction during enter.
     *
@@ -177,14 +182,6 @@ public class PipEnterAnimator extends ValueAnimator
        }
    }

    // no-ops

    @Override
    public void onAnimationCancel(@NonNull Animator animation) {}

    @Override
    public void onAnimationRepeat(@NonNull Animator animation) {}

    /**
     * Caches the initial transform relevant values for the bounds enter animation.
     *
@@ -201,18 +198,13 @@ public class PipEnterAnimator extends ValueAnimator
     */
    public void setAppIconContentOverlay(Context context, Rect appBounds, Rect destinationBounds,
            ActivityInfo activityInfo, int appIconSizePx) {
        reattachAppIconOverlay(
                new PipAppIconOverlay(context, appBounds, destinationBounds,
                        new IconProvider(context).getIcon(activityInfo), appIconSizePx));
    }

    private void reattachAppIconOverlay(PipAppIconOverlay overlay) {
        final SurfaceControl.Transaction tx =
                mSurfaceControlTransactionFactory.getTransaction();
        if (mContentOverlay != null) {
            mContentOverlay.detach(tx);
        }
        mContentOverlay = overlay;
        mContentOverlay = mPipAppIconOverlaySupplier.get(context, appBounds, destinationBounds,
                activityInfo, appIconSizePx);
        mContentOverlay.attach(tx, mLeash);
    }

@@ -229,6 +221,13 @@ public class PipEnterAnimator extends ValueAnimator
        mContentOverlay = null;
    }

    private PipAppIconOverlay getAppIconOverlay(
            Context context, Rect appBounds, Rect destinationBounds,
            ActivityInfo activityInfo, int iconSize) {
        return new PipAppIconOverlay(context, appBounds, destinationBounds,
                new IconProvider(context).getIcon(activityInfo), iconSize);
    }

    /**
     * @return the app icon overlay leash; null if no overlay is attached.
     */
@@ -239,4 +238,21 @@ public class PipEnterAnimator extends ValueAnimator
        }
        return mContentOverlay.getLeash();
    }

    @VisibleForTesting
    void setSurfaceControlTransactionFactory(
            @NonNull PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) {
        mSurfaceControlTransactionFactory = factory;
    }

    @VisibleForTesting
    interface PipAppIconOverlaySupplier {
        PipAppIconOverlay get(Context context, Rect appBounds, Rect destinationBounds,
                ActivityInfo activityInfo, int iconSize);
    }

    @VisibleForTesting
    void setPipAppIconOverlaySupplier(@NonNull PipAppIconOverlaySupplier supplier) {
        mPipAppIconOverlaySupplier = supplier;
    }
}
+2 −8
Original line number Diff line number Diff line
@@ -352,17 +352,11 @@ public class PipTransition extends PipTransitionController implements
            handleBoundsTypeFixedRotation(pipChange, pipActivityChange, endRotation);
        }

        Rect sourceRectHint = null;
        if (pipChange.getTaskInfo() != null
                && pipChange.getTaskInfo().pictureInPictureParams != null) {
            sourceRectHint = pipChange.getTaskInfo().pictureInPictureParams.getSourceRectHint();
        }

        prepareConfigAtEndActivity(startTransaction, finishTransaction, pipChange,
                pipActivityChange);
        startTransaction.merge(finishTransaction);
        PipEnterAnimator animator = new PipEnterAnimator(mContext, pipLeash,
                startTransaction, finishTransaction, destinationBounds, sourceRectHint, delta);
                startTransaction, finishTransaction, destinationBounds, delta);
        animator.setEnterStartState(pipChange);
        animator.onEnterAnimationUpdate(1.0f /* fraction */, startTransaction);
        startTransaction.apply();
@@ -433,7 +427,7 @@ public class PipTransition extends PipTransitionController implements
        }

        PipEnterAnimator animator = new PipEnterAnimator(mContext, pipLeash,
                startTransaction, finishTransaction, endBounds, adjustedSourceRectHint, delta);
                startTransaction, finishTransaction, endBounds, delta);
        if (sourceRectHint == null) {
            // update the src-rect-hint in params in place, to set up initial animator transform.
            params.getSourceRectHint().set(adjustedSourceRectHint);
+201 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.pip2.animation;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.res.Resources;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.Surface;
import android.view.SurfaceControl;

import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;

import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
import com.android.wm.shell.pip2.phone.PipAppIconOverlay;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

/**
 * Unit test again {@link PipEnterAnimator}.
 */
@SmallTest
@TestableLooper.RunWithLooper
@RunWith(AndroidTestingRunner.class)
public class PipEnterAnimatorTest {

    @Mock private Context mMockContext;

    @Mock private Resources mMockResources;

    @Mock private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mMockFactory;

    @Mock private SurfaceControl.Transaction mMockAnimateTransaction;

    @Mock private SurfaceControl.Transaction mMockStartTransaction;

    @Mock private SurfaceControl.Transaction mMockFinishTransaction;

    @Mock private Runnable mMockStartCallback;

    @Mock private Runnable mMockEndCallback;

    @Mock private PipAppIconOverlay mMockPipAppIconOverlay;

    @Mock private SurfaceControl mMockAppIconOverlayLeash;

    @Mock private ActivityInfo mMockActivityInfo;

    @Surface.Rotation private int mRotation;
    private SurfaceControl mTestLeash;
    private Rect mEndBounds;
    private PipEnterAnimator mPipEnterAnimator;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        when(mMockContext.getResources()).thenReturn(mMockResources);
        when(mMockResources.getInteger(anyInt())).thenReturn(0);
        when(mMockFactory.getTransaction()).thenReturn(mMockAnimateTransaction);
        when(mMockAnimateTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any()))
                .thenReturn(mMockAnimateTransaction);
        when(mMockStartTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any()))
                .thenReturn(mMockStartTransaction);
        when(mMockFinishTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any()))
                .thenReturn(mMockFinishTransaction);
        when(mMockPipAppIconOverlay.getLeash()).thenReturn(mMockAppIconOverlayLeash);

        mTestLeash = new SurfaceControl.Builder()
                .setContainerLayer()
                .setName("PipExpandAnimatorTest")
                .setCallsite("PipExpandAnimatorTest")
                .build();
    }

    @Test
    public void setAnimationStartCallback_enter_callbackStartCallback() {
        mRotation = Surface.ROTATION_0;
        mEndBounds = new Rect(100, 100, 500, 500);
        mPipEnterAnimator = new PipEnterAnimator(mMockContext, mTestLeash,
                mMockStartTransaction, mMockFinishTransaction,
                mEndBounds, mRotation);
        mPipEnterAnimator.setSurfaceControlTransactionFactory(mMockFactory);

        mPipEnterAnimator.setAnimationStartCallback(mMockStartCallback);
        mPipEnterAnimator.setAnimationEndCallback(mMockEndCallback);
        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
            mPipEnterAnimator.start();
            mPipEnterAnimator.pause();
        });

        verify(mMockStartCallback).run();
        verifyZeroInteractions(mMockEndCallback);
    }

    @Test
    public void setAnimationEndCallback_enter_callbackStartAndEndCallback() {
        mRotation = Surface.ROTATION_0;
        mEndBounds = new Rect(100, 100, 500, 500);
        mPipEnterAnimator = new PipEnterAnimator(mMockContext, mTestLeash,
                mMockStartTransaction, mMockFinishTransaction,
                mEndBounds, mRotation);
        mPipEnterAnimator.setSurfaceControlTransactionFactory(mMockFactory);

        mPipEnterAnimator.setAnimationStartCallback(mMockStartCallback);
        mPipEnterAnimator.setAnimationEndCallback(mMockEndCallback);
        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
            mPipEnterAnimator.start();
            mPipEnterAnimator.end();
        });

        verify(mMockStartCallback).run();
        verify(mMockEndCallback).run();
    }

    @Test
    public void setAppIconContentOverlay_thenGetContentOverlayLeash_returnOverlayLeash() {
        mRotation = Surface.ROTATION_0;
        mEndBounds = new Rect(100, 100, 500, 500);
        mPipEnterAnimator = new PipEnterAnimator(mMockContext, mTestLeash,
                mMockStartTransaction, mMockFinishTransaction,
                mEndBounds, mRotation);
        mPipEnterAnimator.setSurfaceControlTransactionFactory(mMockFactory);
        mPipEnterAnimator.setPipAppIconOverlaySupplier(
                (context, appBounds, endBounds, icon, iconSize) -> mMockPipAppIconOverlay);

        mPipEnterAnimator.setAppIconContentOverlay(mMockContext, mEndBounds, mEndBounds,
                mMockActivityInfo, 64 /* iconSize */);

        assertEquals(mPipEnterAnimator.getContentOverlayLeash(), mMockAppIconOverlayLeash);
    }

    @Test
    public void setAppIconContentOverlay_thenClearAppIconOverlay_returnNullLeash() {
        mRotation = Surface.ROTATION_0;
        mEndBounds = new Rect(100, 100, 500, 500);
        mPipEnterAnimator = new PipEnterAnimator(mMockContext, mTestLeash,
                mMockStartTransaction, mMockFinishTransaction,
                mEndBounds, mRotation);
        mPipEnterAnimator.setSurfaceControlTransactionFactory(mMockFactory);
        mPipEnterAnimator.setPipAppIconOverlaySupplier(
                (context, appBounds, endBounds, icon, iconSize) -> mMockPipAppIconOverlay);

        mPipEnterAnimator.setAppIconContentOverlay(mMockContext, mEndBounds, mEndBounds,
                mMockActivityInfo, 64 /* iconSize */);
        mPipEnterAnimator.clearAppIconOverlay();

        assertNull(mPipEnterAnimator.getContentOverlayLeash());
    }

    @Test
    public void onEnterAnimationUpdate_withContentOverlay_animateOverlay() {
        mRotation = Surface.ROTATION_0;
        mEndBounds = new Rect(100, 100, 500, 500);
        mPipEnterAnimator = new PipEnterAnimator(mMockContext, mTestLeash,
                mMockStartTransaction, mMockFinishTransaction,
                mEndBounds, mRotation);
        mPipEnterAnimator.setSurfaceControlTransactionFactory(mMockFactory);
        mPipEnterAnimator.setPipAppIconOverlaySupplier(
                (context, appBounds, endBounds, icon, iconSize) -> mMockPipAppIconOverlay);

        float fraction = 0.5f;
        mPipEnterAnimator.setAppIconContentOverlay(mMockContext, mEndBounds, mEndBounds,
                mMockActivityInfo, 64 /* iconSize */);
        mPipEnterAnimator.onEnterAnimationUpdate(fraction, mMockAnimateTransaction);

        verify(mMockPipAppIconOverlay).onAnimationUpdate(
                eq(mMockAnimateTransaction), anyFloat(), eq(fraction), eq(mEndBounds));
    }
}