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

Commit c146239d authored by Ikram Gabiyev's avatar Ikram Gabiyev Committed by Android (Google) Code Review
Browse files

Merge "Implement content overlay in PiP2" into main

parents 4d6c262c cf5da718
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
@@ -30,9 +30,6 @@ import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_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;
@@ -362,31 +359,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,
@@ -405,15 +388,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
@@ -441,14 +427,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();
@@ -696,9 +704,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.