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

Commit cf5da718 authored by Ikram Gabiyev's avatar Ikram Gabiyev
Browse files

Implement content overlay in PiP2

Make sure the direct enter PiP and auto-enter PiP
with invalid source-rect-hint use app icon overlay
during the enter-animation.

Once the enter animation is done, we start fading the overlay out.

Note: with PiP2, we can finish the transition and set the state to
ENTERED_PIP right away even before overlay fadeout is done.
This allows PiP interactions earlier than in PiP1.

Bug: 373946868
Flag: com.android.wm.shell.enable_pip2
Test: auto-enter PiP in btn-nav with invalid src-rect-hint
Change-Id: I2156b7890243c13f278d6023802e41e69db09683
parent c7f83672
Loading
Loading
Loading
Loading
+13 −2
Original line number Diff line number Diff line
@@ -63,8 +63,19 @@ public abstract class PipContentOverlay {
     * @param currentBounds {@link Rect} of the current animation bounds.
     * @param fraction progress of the animation ranged from 0f to 1f.
     */
    public abstract void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
            Rect currentBounds, float fraction);
    public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
            Rect currentBounds, float fraction) {}

    /**
     * Animates the internal {@link #mLeash} by a given fraction for a config-at-end transition.
     * @param atomicTx {@link SurfaceControl.Transaction} to operate, you should not explicitly
     *                 call apply on this transaction, it should be applied on the caller side.
     * @param scale scaling to apply onto the overlay.
     * @param fraction progress of the animation ranged from 0f to 1f.
     * @param endBounds the final bounds PiP is animating into.
     */
    public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
            float scale, float fraction, Rect endBounds) {}

    /** A {@link PipContentOverlay} uses solid color. */
    public static final class PipColorOverlay extends PipContentOverlay {
+57 −3
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import android.animation.Animator;
import android.animation.RectEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.Rect;
@@ -33,10 +34,13 @@ import android.window.TransitionInfo;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.R;
import com.android.wm.shell.common.pip.PipUtils;
import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
import com.android.wm.shell.pip2.phone.PipAppIconOverlay;
import com.android.wm.shell.shared.animation.Interpolators;
import com.android.wm.shell.shared.pip.PipContentOverlay;

/**
 * Animator that handles bounds animations for entering PIP.
@@ -59,6 +63,10 @@ public class PipEnterAnimator extends ValueAnimator

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


    // Internal state representing initial transform - cached to avoid recalculation.
    private final PointF mInitScale = new PointF();
@@ -67,9 +75,6 @@ public class PipEnterAnimator extends ValueAnimator
    private final PointF mInitActivityScale = new PointF();
    private final PointF mInitActivityPos = new PointF();

    Matrix mTransformTensor = new Matrix();
    final float[] mMatrixTmp = new float[9];

    public PipEnterAnimator(Context context,
            @NonNull SurfaceControl leash,
            SurfaceControl.Transaction startTransaction,
@@ -161,10 +166,15 @@ public class PipEnterAnimator extends ValueAnimator
        mRectEvaluator.evaluate(fraction, initCrop, endCrop);
        tx.setCrop(mLeash, mAnimatedRect);

        mTransformTensor.reset();
        mTransformTensor.setScale(scaleX, scaleY);
        mTransformTensor.postTranslate(posX, posY);
        mTransformTensor.postRotate(degrees);
        tx.setMatrix(mLeash, mTransformTensor, mMatrixTmp);

        if (mContentOverlay != null) {
            mContentOverlay.onAnimationUpdate(tx, 1f / scaleX, fraction, mEndBounds);
        }
    }

    // no-ops
@@ -200,4 +210,48 @@ public class PipEnterAnimator extends ValueAnimator
        }
        PipUtils.calcStartTransform(pipChange, mInitScale, mInitPos, mInitCrop);
    }

    /**
     * Initializes and attaches an app icon overlay on top of the PiP layer.
     */
    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.attach(tx, mLeash);
    }

    /**
     * Clears the {@link #mContentOverlay}, this should be done after the content overlay is
     * faded out.
     */
    public void clearAppIconOverlay() {
        if (mContentOverlay == null) {
            return;
        }
        SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction();
        mContentOverlay.detach(tx);
        mContentOverlay = null;
    }

    /**
     * @return the app icon overlay leash; null if no overlay is attached.
     */
    @Nullable
    public SurfaceControl getContentOverlayLeash() {
        if (mContentOverlay == null) {
            return null;
        }
        return mContentOverlay.getLeash();
    }
}
+144 −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.phone;

import static android.util.TypedValue.COMPLEX_UNIT_DIP;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.TypedValue;
import android.view.SurfaceControl;

import com.android.wm.shell.shared.pip.PipContentOverlay;

/** A {@link PipContentOverlay} shows app icon on solid color background. */
public final class PipAppIconOverlay extends PipContentOverlay {
    private static final String TAG = PipAppIconOverlay.class.getSimpleName();
    // The maximum size for app icon in pixel.
    private static final int MAX_APP_ICON_SIZE_DP = 72;

    private final Context mContext;
    private final int mAppIconSizePx;
    private final Rect mAppBounds;
    private final int mOverlayHalfSize;
    private final Matrix mTmpTransform = new Matrix();
    private final float[] mTmpFloat9 = new float[9];

    private Bitmap mBitmap;

    public PipAppIconOverlay(Context context, Rect appBounds, Rect destinationBounds,
            Drawable appIcon, int appIconSizePx) {
        mContext = context;
        final int maxAppIconSizePx = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP,
                MAX_APP_ICON_SIZE_DP, context.getResources().getDisplayMetrics());
        mAppIconSizePx = Math.min(maxAppIconSizePx, appIconSizePx);

        final int overlaySize = getOverlaySize(appBounds, destinationBounds);
        mOverlayHalfSize = overlaySize >> 1;

        // When the activity is in the secondary split, make sure the scaling center is not
        // offset.
        mAppBounds = new Rect(0, 0, appBounds.width(), appBounds.height());

        mBitmap = Bitmap.createBitmap(overlaySize, overlaySize, Bitmap.Config.ARGB_8888);
        prepareAppIconOverlay(appIcon);
        mLeash = new SurfaceControl.Builder()
                .setCallsite(TAG)
                .setName(LAYER_NAME)
                .build();
    }

    /**
     * Returns the size of the app icon overlay.
     *
     * In order to have the overlay always cover the pip window during the transition,
     * the overlay will be drawn with the max size of the start and end bounds in different
     * rotation.
     */
    public static int getOverlaySize(Rect appBounds, Rect destinationBounds) {
        final int appWidth = appBounds.width();
        final int appHeight = appBounds.height();

        return Math.max(Math.max(appWidth, appHeight),
                Math.max(destinationBounds.width(), destinationBounds.height())) + 1;
    }

    @Override
    public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) {
        tx.show(mLeash);
        tx.setLayer(mLeash, Integer.MAX_VALUE);
        tx.setBuffer(mLeash, mBitmap.getHardwareBuffer());
        tx.setAlpha(mLeash, 0f);
        tx.reparent(mLeash, parentLeash);
        tx.apply();
    }

    @Override
    public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
            float scale, float fraction, Rect endBounds) {
        mTmpTransform.reset();
        // Scale back the bitmap with the pivot at parent origin
        mTmpTransform.setScale(scale, scale);
        // We are negative-cropping away from the final bounds crop in config-at-end enter PiP;
        // this means that the overlay shift depends on the final bounds.
        // Note: translation is also dependent on the scaling of the parent.
        mTmpTransform.postTranslate(endBounds.width() / 2f - mOverlayHalfSize * scale,
                endBounds.height() / 2f - mOverlayHalfSize * scale);
        atomicTx.setMatrix(mLeash, mTmpTransform, mTmpFloat9)
                .setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2);
    }



    @Override
    public void detach(SurfaceControl.Transaction tx) {
        super.detach(tx);
        if (mBitmap != null && !mBitmap.isRecycled()) {
            mBitmap.recycle();
        }
    }

    private void prepareAppIconOverlay(Drawable appIcon) {
        final Canvas canvas = new Canvas();
        canvas.setBitmap(mBitmap);
        final TypedArray ta = mContext.obtainStyledAttributes(new int[] {
                android.R.attr.colorBackground });
        try {
            int colorAccent = ta.getColor(0, 0);
            canvas.drawRGB(
                    Color.red(colorAccent),
                    Color.green(colorAccent),
                    Color.blue(colorAccent));
        } finally {
            ta.recycle();
        }
        final Rect appIconBounds = new Rect(
                mOverlayHalfSize - mAppIconSizePx / 2,
                mOverlayHalfSize - mAppIconSizePx / 2,
                mOverlayHalfSize + mAppIconSizePx / 2,
                mOverlayHalfSize + mAppIconSizePx / 2);
        appIcon.setBounds(appIconBounds);
        appIcon.draw(canvas);
        mBitmap = mBitmap.copy(Bitmap.Config.HARDWARE, false /* mutable */);
    }
}
+8 −1
Original line number Diff line number Diff line
@@ -456,6 +456,10 @@ public class PipController implements ConfigurationChangeListener,
        }
    }

    private void setLauncherAppIconSize(int iconSizePx) {
        mPipBoundsState.getLauncherState().setAppIconSizePx(iconSizePx);
    }

    /**
     * The interface for calls from outside the Shell, within the host process.
     */
@@ -571,7 +575,10 @@ public class PipController implements ConfigurationChangeListener,
        }

        @Override
        public void setLauncherAppIconSize(int iconSizePx) {}
        public void setLauncherAppIconSize(int iconSizePx) {
            executeRemoteCallWithTaskPermission(mController, "setLauncherAppIconSize",
                    (controller) -> controller.setLauncherAppIconSize(iconSizePx));
        }

        @Override
        public void setPipAnimationListener(IPipAnimationListener listener) {
+44 −38
Original line number Diff line number Diff line
@@ -29,9 +29,6 @@ import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
import static com.android.wm.shell.transition.Transitions.TRANSIT_RESIZE_PIP;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.PictureInPictureParams;
@@ -361,31 +358,17 @@ public class PipTransition extends PipTransitionController implements
        animator.setEnterStartState(pipChange, pipActivityChange);
        animator.onEnterAnimationUpdate(1.0f /* fraction */, startTransaction);
        startTransaction.apply();
        finishInner();
        return true;
    }

    private void startOverlayFadeoutAnimation() {
        ValueAnimator animator = ValueAnimator.ofFloat(1f, 0f);
        animator.setDuration(CONTENT_OVERLAY_FADE_OUT_DELAY_MS);
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
        if (swipePipToHomeOverlay != null) {
            // fadeout the overlay if needed.
            startOverlayFadeoutAnimation(swipePipToHomeOverlay, () -> {
                SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
                tx.remove(mPipTransitionState.getSwipePipToHomeOverlay());
                tx.remove(swipePipToHomeOverlay);
                tx.apply();

                // We have fully completed enter-PiP animation after the overlay is gone.
                mPipTransitionState.setState(PipTransitionState.ENTERED_PIP);
            }
            });
        animator.addUpdateListener(animation -> {
            float alpha = (float) animation.getAnimatedValue();
            SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
            tx.setAlpha(mPipTransitionState.getSwipePipToHomeOverlay(), alpha).apply();
        });
        animator.start();
        }
        finishInner();
        return true;
    }

    private boolean startBoundsTypeEnterAnimation(@NonNull TransitionInfo info,
@@ -404,15 +387,18 @@ public class PipTransition extends PipTransitionController implements
            return false;
        }

        Rect endBounds = pipChange.getEndAbsBounds();
        SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash;
        Preconditions.checkNotNull(pipLeash, "Leash is null for bounds transition.");
        final Rect startBounds = pipChange.getStartAbsBounds();
        final Rect endBounds = pipChange.getEndAbsBounds();

        Rect sourceRectHint = null;
        if (pipChange.getTaskInfo() != null
                && pipChange.getTaskInfo().pictureInPictureParams != null) {
            sourceRectHint = pipChange.getTaskInfo().pictureInPictureParams.getSourceRectHint();
        }
        final PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams;
        final float aspectRatio = mPipBoundsAlgorithm.getAspectRatioOrDefault(params);

        final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, startBounds,
                endBounds);

        final SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash;
        final Rect adjustedSourceRectHint = sourceRectHint != null ? new Rect(sourceRectHint)
                : PipUtils.getEnterPipWithOverlaySrcRectHint(startBounds, aspectRatio);

        // For opening type transitions, if there is a change of mode TO_FRONT/OPEN,
        // make sure that change has alpha of 1f, since it's init state might be set to alpha=0f
@@ -440,14 +426,36 @@ public class PipTransition extends PipTransitionController implements
        }

        PipEnterAnimator animator = new PipEnterAnimator(mContext, pipLeash,
                startTransaction, finishTransaction, endBounds, sourceRectHint, delta);
                startTransaction, finishTransaction, endBounds, adjustedSourceRectHint, delta);
        if (sourceRectHint == null) {
            // update the src-rect-hint in params in place, to set up initial animator transform.
            params.getSourceRectHint().set(adjustedSourceRectHint);
            animator.setAppIconContentOverlay(
                    mContext, startBounds, endBounds, pipChange.getTaskInfo().topActivityInfo,
                    mPipBoundsState.getLauncherState().getAppIconSizePx());
        }
        animator.setAnimationStartCallback(() -> animator.setEnterStartState(pipChange,
                pipActivityChange));
        animator.setAnimationEndCallback(this::finishInner);
        animator.setAnimationEndCallback(() -> {
            if (animator.getContentOverlayLeash() != null) {
                startOverlayFadeoutAnimation(animator.getContentOverlayLeash(),
                        animator::clearAppIconOverlay);
            }
            finishInner();
        });
        animator.start();
        return true;
    }

    private void startOverlayFadeoutAnimation(@NonNull SurfaceControl overlayLeash,
            @NonNull Runnable onAnimationEnd) {
        PipAlphaAnimator animator = new PipAlphaAnimator(mContext, overlayLeash,
                null /* startTx */, PipAlphaAnimator.FADE_OUT);
        animator.setDuration(CONTENT_OVERLAY_FADE_OUT_DELAY_MS);
        animator.setAnimationEndCallback(onAnimationEnd);
        animator.start();
    }

    private void handleBoundsTypeFixedRotation(TransitionInfo.Change pipTaskChange,
            TransitionInfo.Change pipActivityChange, int endRotation) {
        final Rect endBounds = pipTaskChange.getEndAbsBounds();
@@ -692,9 +700,7 @@ public class PipTransition extends PipTransitionController implements

    private void finishInner() {
        finishTransition(null /* tx */);
        if (mPipTransitionState.getSwipePipToHomeOverlay() != null) {
            startOverlayFadeoutAnimation();
        } else if (mPipTransitionState.getState() == PipTransitionState.ENTERING_PIP) {
        if (mPipTransitionState.getState() == PipTransitionState.ENTERING_PIP) {
            // If we were entering PiP (i.e. playing the animation) with a valid srcRectHint,
            // and then we get a signal on client finishing its draw after the transition
            // has ended, then we have fully entered PiP.